squad-kit 1.3.10 → 1.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -222,7 +222,8 @@ ${yaml21}`},ms2+SLACK_MS);if(a.isError)throw failureOf(textOf(a));let said=textO
222
222
  `}else{let esc2=key.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),line3=form==="env"?new RegExp(`^(\\s*(?:export\\s+)?)${esc2}\\s*=.*$`):new RegExp(`^(\\s*)${esc2}\\s*[=:].*$`),text3=form==="env"?`${key}=${envValue(value)}`:`${key}=${value.replace(/\\/g,"\\\\")}`,lines3=before?before.replace(/\r?\n$/,"").split(/\r?\n/):[],set2=!1,out20=[];for(let l of lines3){let m=line3.exec(l);m?set2||(out20.push(`${m[1]}${text3}`),set2=!0):out20.push(l)}set2||out20.push(text3),after=`${out20.join(`
223
223
  `)}
224
224
  `}after!==before&&(fs51.mkdirSync(path43.dirname(file3),{recursive:!0}),fs51.writeFileSync(file3,after,{mode:384}))}import fs55 from"fs";import os7 from"os";import path47 from"path";import fs53 from"fs";import path45 from"path";import{createHash,randomUUID as randomUUID3}from"crypto";import yaml6 from"js-yaml";var BOARD_STAGES=["tickets","specs","implement","test","review","release","bugs"],DECISION_AREAS=["Structure","Data","Security","Interface","Rules"],NO_NEW_TESTS_LABEL="Built without new tests",TECH_ROLES=["Language","Framework","Interface","Data","Testing","Tooling","Services"];import fs52 from"fs";import path44 from"path";var KEEP=20,LEARN_AFTER=3,DEFAULT_USUAL={questions:[60,180],spec:[60,180],design:[120,300],build:[120,240],fix:[120,300],checks:[60,180],review:[120,240],proof:[60,180]};function usualKey(who4,step2,track2){return`${who4}.${step2}.${track2??"full"}`}function usualFile(bp){return path44.join(bp.localDir,"usual-times.json")}function readUsual(bp){try{let raw=JSON.parse(fs52.readFileSync(usualFile(bp),"utf8"));if(!raw||typeof raw!="object")return{};let book={};for(let[k,v]of Object.entries(raw))Array.isArray(v)&&(book[k]=v.filter(n=>typeof n=="number"&&Number.isFinite(n)&&n>0));return book}catch{return{}}}function recordUsual(bp,key,seconds2){if(!(!Number.isFinite(seconds2)||seconds2<1))try{let book=readUsual(bp);book[key]=[...book[key]??[],Math.round(seconds2)].slice(-KEEP),fs52.mkdirSync(path44.dirname(usualFile(bp)),{recursive:!0});let tmp=`${usualFile(bp)}.${process.pid}.tmp`;fs52.writeFileSync(tmp,`${JSON.stringify(book,null,1)}
225
- `),fs52.renameSync(tmp,usualFile(bp))}catch{}}function quantile(sorted,q5){let at3=(sorted.length-1)*q5,lo=Math.floor(at3),hi=Math.ceil(at3);return sorted[lo]+(sorted[hi]-sorted[lo])*(at3-lo)}function usualOf(book,who4,step2,track2){let runs2=book[usualKey(who4,step2,track2)]??[];if(runs2.length<LEARN_AFTER){let[low,high]=DEFAULT_USUAL[step2];return{low,high,learned:!1,runs:runs2.length}}let sorted=[...runs2].sort((a,b)=>a-b);return{low:quantile(sorted,.25),high:quantile(sorted,.75),learned:!0,runs:runs2.length}}function middle(u){return(u.low+u.high)/2}function rangeWords(u){if(u.high<60)return"under 1 min";let low=Math.max(1,Math.round(u.low/60)),high=Math.max(low,Math.round(u.high/60));return low===high?`about ${low} min`:`${low} to ${high} min`}function usualWords(u){return`usually ${rangeWords(u)}`}function durationWords(seconds2){let s=Math.max(0,seconds2);if(s<60)return`${Math.max(1,Math.floor(s))} s`;if(s<3600)return`${Math.floor(s/60)} min`;let h=Math.floor(s/3600),m=Math.floor(s%3600/60);return m?`${h} h ${m} min`:`${h} h`}function minutesWords(seconds2){if(seconds2<60){let s=Math.max(1,Math.floor(seconds2));return`${s} second${s===1?"":"s"}`}let m=Math.floor(seconds2/60);return`${m} minute${m===1?"":"s"}`}var WHO={analyst:"Analyst",architect:"Architect",builder:"Builder",reviewer:"Reviewer",tester:"Tester",squad:"squad-kit"};function iso(at3){return new Date(at3).toISOString()}function firstOpenStep(t){let steps=t.steps??[];if(!steps.length)return;let i=steps.findIndex(s=>s.status!=="done");return i>=0?i+1:steps.length}var TIMED=new Set(["questions","spec","design","build","fix","checks","review","proof"]);function timedStep(step2){return step2&&TIMED.has(step2)?step2:null}function startStep(t,c,at3=Date.now()){if(t.waiting)return;let same5=t.clock?.who===c.who&&t.clock.step===c.step?t.clock:void 0,part=c.part??same5?.part??(c.who==="builder"&&c.step==="build"?firstOpenStep(t):void 0),limit=c.limit??same5?.limit,waited=same5?.paused?Math.max(0,at3-Date.parse(same5.paused)):0,since=iso(same5?Date.parse(same5.since)+waited:at3),{paused:_paused,...goesOn}=same5??{};t.clock={...goesOn,who:c.who,step:c.step,since,began:same5?.began?iso(Date.parse(same5.began)+waited):since,...part?{part}:{},...limit?{limit}:{}}}function waitsNow(t){return!!t.waiting||!t.live&&!t.squad}function pauseClock(t,at3=Date.now()){t.clock&&!t.clock.paused&&(t.clock.paused=iso(at3))}function stepSeconds(c,at3=Date.now()){let end=c.paused?Math.min(at3,Date.parse(c.paused)):at3;return Math.max(0,(end-Date.parse(c.since))/1e3)}var HISTORY=80;function resumeClock(t,at3=Date.now()){t.clock&&(t.clock.resumed=iso(at3))}function endStep(bp,t,at3=Date.now(),more3={}){let c=t.clock;c&&(c.resumed||recordUsual(bp,usualKey(c.who,c.step,t.track),stepSeconds(c,at3)),t.history=[...t.history??[],{who:c.who,step:c.step,from:c.began??c.since,to:iso(c.paused?Math.min(at3,Date.parse(c.paused)):at3),stage:t.stage,...more3}].slice(-HISTORY),delete t.clock)}function runSeconds(c,at3=Date.now()){let secs=((c.paused?Math.min(at3,Date.parse(c.paused)):at3)-Date.parse(c.began??c.since))/1e3;return Number.isFinite(secs)?Math.max(0,secs):0}function nextPart(bp,t,part,at3=Date.now()){let c=t.clock;!c||c.step!=="build"||c.part===part||(c.part!==void 0&&part>c.part&&!c.resumed&&recordUsual(bp,usualKey(c.who,c.step,t.track),stepSeconds(c,at3)),c.part=part,c.since=iso(at3),delete c.resumed,delete c.paused)}function stepDoing(t,line3,at3=Date.now()){!t.clock||!line3||(t.clock.doing=line3,t.clock.doingAt=iso(at3))}function trackStage(t,before,at3=Date.now()){let st=t.stageTimes??{stage:before,since:before==="tickets"||before==="bugs"?t.createdAt:iso(at3),spent:{}},secsSince=from=>{let secs=Math.max(0,Math.round((at3-Date.parse(from))/1e3));return Number.isFinite(secs)?secs:0},yours=!!(t.waiting||t.takenOver);st.yoursSince&&(!yours||st.stage!==t.stage)&&(st.yours={...st.yours,[st.stage]:(st.yours?.[st.stage]??0)+secsSince(st.yoursSince)},delete st.yoursSince),st.stage!==t.stage&&(st.spent={...st.spent,[st.stage]:(st.spent[st.stage]??0)+secsSince(st.since)},st.stage=t.stage,st.since=iso(at3)),yours&&!st.yoursSince&&(st.yoursSince=iso(at3)),t.stageTimes=st}var SLOW_FLOOR_S=300;function usualSentence(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 slowAfter(u){return Math.max(2*u.high,SLOW_FLOOR_S)}function slowStep(t,book,at3=Date.now()){let c=t.clock;if(!c||t.waiting||c.paused)return null;let u=usualOf(book,c.who,c.step,t.track),n=t.steps?.length??0,heard=t.steps?.some(s=>s.status==="doing"||s.status==="done");if(c.who==="builder"&&c.step==="build"&&n>1&&!heard){let all={low:u.low*n,high:u.high*n},secs2=Math.max(0,(at3-Date.parse(c.began??c.since))/1e3);return secs2<slowAfter(all)?null:{since:c.began??c.since,kind:"long",detail:`The Builder has been building for ${minutesWords(secs2)}. ${n} steps usually take ${usualSentence(all)}.`}}let secs=stepSeconds(c,at3);if(secs<slowAfter(u))return null;let detail=c.who==="squad"?`The checks have been running for ${minutesWords(secs)}. They usually take ${usualSentence(u)}.`:`The ${WHO[c.who]} has been on this step for ${minutesWords(secs)}. It usually takes ${usualSentence(u)}.`;return{since:c.since,kind:"long",detail}}function oldStop(t){return t.stuck?t.stage==="done"||t.mergedAt?!0:t.stage!=="release"?!1:t.waiting?.kind!=="stuck"||["checks","review","proof"].includes(t.stuck.gate??""):!1}function clearStaleStop(t){t.stuck&&(t.stuck=void 0),t.waiting?.kind==="stuck"&&(t.waiting=null)}var ORDER=["tickets","specs","implement","test","review"];function hasSpec(t){return!!(t.story?.trim()||t.criteria?.length)}function hasDesign(t){return!!(t.decision||t.steps?.length)}function leavingAt(t){return t.stage==="implement"?"the build":t.stage==="test"?"the checks":t.stage==="review"?t.waiting?.kind==="proof"?"the proof":"the review":t.stage==="specs"?t.specApproved?"the design":"the spec":hasSpec(t)?"the spec":void 0}function outsideWords(o){return o.from?`Continued outside from ${o.from}`:"Worked outside squad-kit"}function showOutsideStep(t){let left2=t.takenOver?.free;if(!left2)return;let reached=t.worktree?"implement":hasSpec(t)||hasDesign(t)?"specs":"tickets",rank=s=>Math.max(ORDER.indexOf(s),0);t.stage=rank(left2.stage)>=rank(reached)?left2.stage:reached;let doing=t.worktree?"building":hasDesign(t)?"the design is written":hasSpec(t)?"the spec is written":"not started yet";t.status=`In a conversation outside squad-kit: ${doing}`}var OUTSIDE_WAITS="In a conversation outside squad-kit. Bring it back when you are done",NO_DOCS="none";function leaveForOutside(t,by2,at3,seen){let from=leavingAt(t),phase=t.squad?.phase,left2={stage:t.stage,status:t.status,waiting:t.waiting,...phase?{phase}:{},...seen,...from?{from}:{}};t.takenOver={by:by2,at:at3,session:t.sessions?.build??"",cwd:t.storyDir??t.worktree??"",free:left2},t.live=!1,t.squad=null,t.waiting={kind:"yours",summary:OUTSIDE_WAITS},showOutsideStep(t)}function gatesPassed(g){return g?.review==="pass"&&g.proof==="pass"&&(g.checks==="pass"||g.checks==="unchecked")}function markCheckedOutside(t){if(!t.outside)return;let g=t.gates;t.outside={...t.outside,...gatesPassed(g)?{checked:{at:new Date().toISOString(),by:"squad-kit",...g?.checks==="unchecked"?{noTests:!0}:{}}}:{checked:void 0}}}function dropOldChecked(t){t.outside?.checked&&!gatesPassed(t.gates)&&(t.outside={...t.outside,checked:void 0})}function changesScreens(t){return(t.work?.screens??0)>0||!!(t.outside&&!t.work)}var IS={text:x=>typeof x=="string",texts:x=>Array.isArray(x)&&x.every(y=>typeof y=="string"),object:x=>isPlain(x),objects:x=>Array.isArray(x)&&x.every(isPlain),"yes-no":x=>typeof x=="boolean",number:x=>typeof x=="number"&&Number.isFinite(x)};function isPlain(x){return typeof x=="object"&&x!==null&&!Array.isArray(x)}function wrongField(o,fields){for(let[key,shape2]of Object.entries(fields))if(o[key]!==void 0&&o[key]!==null&&!IS[shape2](o[key]))return`"${key}" is not in the right form.`;return null}function hasMergeMarks(text3){return/^<{7}( |$)[\s\S]*?^={7}\s*$[\s\S]*?^>{7}( |$)/m.test(text3)}import yaml5 from"js-yaml";function enc(v){return v.includes(`
225
+ `),fs52.renameSync(tmp,usualFile(bp))}catch{}}function quantile(sorted,q5){let at3=(sorted.length-1)*q5,lo=Math.floor(at3),hi=Math.ceil(at3);return sorted[lo]+(sorted[hi]-sorted[lo])*(at3-lo)}function usualOf(book,who4,step2,track2){let runs2=book[usualKey(who4,step2,track2)]??[];if(runs2.length<LEARN_AFTER){let[low,high]=DEFAULT_USUAL[step2];return{low,high,learned:!1,runs:runs2.length}}let sorted=[...runs2].sort((a,b)=>a-b);return{low:quantile(sorted,.25),high:quantile(sorted,.75),learned:!0,runs:runs2.length}}function middle(u){return(u.low+u.high)/2}function rangeWords(u){if(u.high<60)return"under 1 min";let low=Math.max(1,Math.round(u.low/60)),high=Math.max(low,Math.round(u.high/60));return low===high?`about ${low} min`:`${low} to ${high} min`}function usualWords(u){return`usually ${rangeWords(u)}`}function durationWords(seconds2){let s=Math.max(0,seconds2);if(s<60)return`${Math.max(1,Math.floor(s))} s`;if(s<3600)return`${Math.floor(s/60)} min`;let h=Math.floor(s/3600),m=Math.floor(s%3600/60);return m?`${h} h ${m} min`:`${h} h`}function minutesWords(seconds2){if(seconds2<60){let s=Math.max(1,Math.floor(seconds2));return`${s} second${s===1?"":"s"}`}let m=Math.floor(seconds2/60);return`${m} minute${m===1?"":"s"}`}var DOING={tests:"Running the tests",check:"Checking the code",copy:"Making a copy of the code. On a big project this takes a while.",setup:"Setting up this story's copy",start:"Starting the app",build:"Building the app",read:"Reading the code",plan:"Reading the plan",shots:"Looking at the screenshots",write:"Writing the code",lookup:"Looking something up",other:"Working\u2026"},CHECK_DOING={test:DOING.tests,lint:DOING.check,build:DOING.build,start:DOING.start},OWN_CHECK_DOING={test:DOING.tests,lint:DOING.check,build:"Checking that the app builds"},FIXING={test:"Fixing a failed test",lint:"Fixing the code check",build:"Fixing the build"},PASSED={test:"The tests passed",lint:"The code check passed",build:"The build passed"},WRITING_TESTS="Writing tests";function writesTests(title2){return!!(title2&&/\b(tests?|testing|e2e)\b/i.test(title2))}var PLAIN_WORK=new Set([DOING.write,DOING.read,DOING.plan,DOING.lookup,DOING.other]);function stepWords(plain,t){let c=t.clock;if(c?.who!=="builder")return plain;if(c.failed&&!c.failed.fixed&&PLAIN_WORK.has(plain))return FIXING[c.failed.check];if(c.step==="fix"&&(t.tries??0)>0&&PLAIN_WORK.has(plain))return`Fixing the checks (try ${(t.tries??0)+1} of 3)`;if(c.step==="build"&&c.part&&writesTests(t.steps?.[c.part-1]?.title)){if(plain===DOING.write)return WRITING_TESTS;if(plain===DOING.build)return OWN_CHECK_DOING.build}return plain}var WINS=["tests","setup","start","build","check","read","other"];function simpleCommands(line3){let cut=line3.replace(/<<-?\s*['"]?\w+['"]?[\s\S]*$/,""),out20=[],words2=[],word2="",quote="",endWord=()=>{word2&&words2.push(word2),word2=""},endCommand=()=>{endWord(),words2.length&&out20.push(words2),words2=[]};for(let i=0;i<cut.length;i+=1){let c=cut[i];quote?c===quote?quote="":word2+=c:c==='"'||c==="'"?quote=c:c===";"||c===`
226
+ `||c==="|"||c==="("||c===")"?endCommand():c==="&"?cut[i-1]===">"?word2+=c:endCommand():c===" "||c===" "?endWord():word2+=c}return endCommand(),out20}var WRAPPERS=new Set(["time","timeout","gtimeout","nice","nohup","exec","env","sudo","command","do","then","else"]),READERS=new Set(["cat","head","tail","less","more","grep","egrep","fgrep","rg","ag","find","ls","tree","wc","stat","file","pwd","which","diff","jq","awk","sed","cut","sort","uniq","tr","xargs","echo","printf","true","basename","dirname","realpath"]),TEST_TOOLS=new Set(["vitest","jest","mocha","ava","pytest","py.test","rspec","phpunit","tox","nox","karma","wdio"]),CHECK_TOOLS=new Set(["tsc","eslint","prettier","biome","stylelint","ruff","mypy","flake8","black","pylint","pyright","isort","rubocop","golangci-lint","shellcheck","git","gh"]),BUILD_TOOLS2=new Set(["tsup","webpack","rollup","esbuild","parcel","turbo","make","gmake","cmake","ng"]),START_TOOLS=new Set(["nodemon","uvicorn","gunicorn","hypercorn","flask","streamlit"]),INSTALL_WORDS=new Set(["install","i","ci","add","update","up","sync","restore","fetch","upgrade","remove","rm","uninstall","link","rebuild","bootstrap","dedupe"]),INSTALLERS=new Set(["pip","pip3","pipx","poetry","uv","pipenv","bundle","gem","composer","brew","apt","apt-get","conda"]),MIGRATORS=new Set(["prisma","drizzle-kit","knex","sequelize","typeorm","alembic","flyway","liquibase"]);function scriptKind(name){return/test|e2e|spec|cypress|playwright/i.test(name)?"tests":/^(dev|start|serve|preview|watch)\b/i.test(name)?"start":/lint|check|type|format|fmt|prettier/i.test(name)?"check":/build|compile|bundle|dist/i.test(name)?"build":/install|setup|prepare|migrate|seed|bootstrap|generate/i.test(name)?"setup":"other"}function toolKind(tool5,rest){let first2=rest.find(w=>!w.startsWith("-"))??"";return tool5==="playwright"||tool5==="cypress"?first2==="install"?"setup":first2==="test"||first2==="run"?"tests":first2==="open"||first2==="show-report"?"other":"tests":TEST_TOOLS.has(tool5)?"tests":CHECK_TOOLS.has(tool5)?"check":BUILD_TOOLS2.has(tool5)?first2==="test"?"tests":first2==="serve"||first2==="dev"?"start":"build":START_TOOLS.has(tool5)?"start":MIGRATORS.has(tool5)?"setup":INSTALLERS.has(tool5)?INSTALL_WORDS.has(first2)?"setup":first2==="run"||first2==="exec"?commandKind(rest.slice(rest.indexOf(first2)+1)):"other":tool5==="next"||tool5==="vite"||tool5==="nuxt"||tool5==="astro"||tool5==="remix"?first2==="build"?"build":"start":null}function packageKind(tool5,rest){let words2=rest.filter(w=>!w.startsWith("-")),first2=words2[0]??"";return first2?tool5==="npx"||tool5==="pnpx"||tool5==="bunx"?commandKind(words2):INSTALL_WORDS.has(first2)?"setup":first2==="run"||first2==="run-script"?scriptKind(words2[1]??""):first2==="exec"||first2==="dlx"||first2==="x"?commandKind(words2.slice(1)):first2==="t"?"tests":tool5==="npm"&&!["test","start","stop","restart"].includes(first2)?"other":toolKind(first2,words2.slice(1))??scriptKind(first2):tool5==="yarn"?"setup":"other"}function commandKind(all){let words2=all;for(;words2.length;){let w=words2[0];if(/^\w+=/.test(w)||/^\d+[smh]?$/.test(w)||WRAPPERS.has(w)||w.startsWith("-")&&words2!==all)words2=words2.slice(1);else break}let tool5=(words2[0]??"").split("/").pop()??"",rest=words2.slice(1);if(!tool5)return"other";if(tool5==="bash"||tool5==="sh"||tool5==="zsh"){let at3=rest.indexOf("-c");return at3>=0&&rest[at3+1]?plainKind(rest[at3+1]):"other"}if(tool5==="npm"||tool5==="pnpm"||tool5==="yarn"||tool5==="bun"||tool5==="npx"||tool5==="pnpx"||tool5==="bunx")return packageKind(tool5,rest);if(tool5==="sed")return rest.some(r=>/^-i/.test(r))?"other":"read";if(tool5==="cat"&&rest.some(r=>r.startsWith(">")))return"other";if(READERS.has(tool5))return"read";if(/^python[\d.]*$/.test(tool5)){let at3=rest.indexOf("-m");if(at3>=0&&rest[at3+1]){let mod=rest[at3+1];return mod==="unittest"?"tests":mod==="venv"?"setup":toolKind(mod,rest.slice(at3+2))??"other"}if(/(^|\/)manage\.py$/.test(rest[0]??"")){let what=rest[1]??"";return what==="test"?"tests":what==="runserver"?"start":what==="migrate"||what==="makemigrations"?"setup":"other"}return"other"}let known=toolKind(tool5,rest);if(known)return known;if(["go","cargo","dotnet","mvn","mvnw","gradle","gradlew","rails","rake","swift","php","artisan"].includes(tool5)){let first2=rest.find(w=>!w.startsWith("-"))??"";if(/^(test|spec)/.test(first2))return"tests";if(/^(run|serve|server|s|bootRun)$/.test(first2))return"start";if(/^(vet|clippy|check|fmt)$/.test(first2))return"check";if(/^(build|package|compile|publish|assemble)$/.test(first2))return"build";if(/^(mod|restore|fetch|get|migrate)$/.test(first2))return"setup"}return"other"}function plainKind(command){let kinds=simpleCommands(command).filter(w=>w[0]!=="cd"&&w[0]!=="pushd"&&w[0]!=="popd"&&w[0]!=="export").map(commandKind);return WINS.find(k=>kinds.includes(k))??"other"}function plainCommand(command){return DOING[plainKind(command)]}function plainDoing(line3){let reading=/^Reading\s+(.*)$/.exec(line3);return reading?/\.(png|jpe?g|gif|webp)$/i.test(reading[1].trim())?DOING.shots:/(^|\/)\.squad\//.test(reading[1])?DOING.plan:DOING.read:/^(Searching for|Looking for|Listing)\b/.test(line3)?DOING.read:/^(Editing|Writing)\b/.test(line3)?DOING.write:/^Looking up\b/.test(line3)?DOING.lookup:/^Running\s/.test(line3)?plainCommand(line3.replace(/^Running\s+/,"")):DOING.other}function plainToolLine(e,cwd){let line3=describeToolEvent(e,cwd);return line3?typeof e.input.command=="string"&&line3.startsWith("Running ")?plainCommand(e.input.command):plainDoing(line3):null}var WHO={analyst:"Analyst",architect:"Architect",builder:"Builder",reviewer:"Reviewer",tester:"Tester",squad:"squad-kit"};function iso(at3){return new Date(at3).toISOString()}function firstOpenStep(t){let steps=t.steps??[];if(!steps.length)return;let i=steps.findIndex(s=>s.status!=="done");return i>=0?i+1:steps.length}var TIMED=new Set(["questions","spec","design","build","fix","checks","review","proof"]);function timedStep(step2){return step2&&TIMED.has(step2)?step2:null}function startStep(t,c,at3=Date.now()){if(t.waiting)return;let same5=t.clock?.who===c.who&&t.clock.step===c.step?t.clock:void 0,part=c.part??same5?.part??(c.who==="builder"&&c.step==="build"?firstOpenStep(t):void 0),limit=c.limit??same5?.limit,waited=same5?.paused?Math.max(0,at3-Date.parse(same5.paused)):0,since=iso(same5?Date.parse(same5.since)+waited:at3),{paused:_paused,running:_running,...goesOn}=same5??{};t.clock={...goesOn,who:c.who,step:c.step,since,began:same5?.began?iso(Date.parse(same5.began)+waited):since,...part?{part}:{},...limit?{limit}:{}}}function waitsNow(t){return!!t.waiting||!t.live&&!t.squad}function pauseClock(t,at3=Date.now()){t.clock&&!t.clock.paused&&(t.clock.paused=iso(at3))}function stepSeconds(c,at3=Date.now()){let end=c.paused?Math.min(at3,Date.parse(c.paused)):at3;return Math.max(0,(end-Date.parse(c.since))/1e3)}var HISTORY=80;function resumeClock(t,at3=Date.now()){t.clock&&(t.clock.resumed=iso(at3))}function endStep(bp,t,at3=Date.now(),more3={}){let c=t.clock;c&&(c.resumed||recordUsual(bp,usualKey(c.who,c.step,t.track),stepSeconds(c,at3)),t.history=[...t.history??[],{who:c.who,step:c.step,from:c.began??c.since,to:iso(c.paused?Math.min(at3,Date.parse(c.paused)):at3),stage:t.stage,...more3}].slice(-HISTORY),delete t.clock)}function runSeconds(c,at3=Date.now()){let secs=((c.paused?Math.min(at3,Date.parse(c.paused)):at3)-Date.parse(c.began??c.since))/1e3;return Number.isFinite(secs)?Math.max(0,secs):0}function nextPart(bp,t,part,at3=Date.now()){let c=t.clock;!c||c.step!=="build"||c.part===part||(c.part!==void 0&&part>c.part&&!c.resumed&&recordUsual(bp,usualKey(c.who,c.step,t.track),stepSeconds(c,at3)),c.part=part,c.since=iso(at3),delete c.resumed,delete c.paused,delete c.failed)}function stepDoing(t,line3,at3=Date.now()){!t.clock||!line3||(t.clock.doing=line3,t.clock.doingAt=iso(at3),delete t.clock.running)}function runsFor(t,what,line3,at3=Date.now()){stepDoing(t,line3,at3),t.clock&&(t.clock.running=what)}function ranFor(t,what,at3=Date.now()){t.clock?.running===what&&(delete t.clock.running,t.clock.doingAt=iso(at3))}function ownCheckEnded(t,check,ok2,alone=!1,at3=Date.now()){let c=t.clock;if(!c||c.who!=="builder")return ranFor(t,check,at3);let was=c.failed?.check===check?c.failed:void 0;ok2?was&&!was.fixed&&!alone&&(was.fixed=iso(at3)):c.failed={check,times:(was?.times??0)+1,at:iso(at3)};let open3=c.failed&&!c.failed.fixed?c.failed.check:void 0;stepDoing(t,open3?FIXING[open3]:PASSED[check],at3)}function trackStage(t,before,at3=Date.now()){let st=t.stageTimes??{stage:before,since:before==="tickets"||before==="bugs"?t.createdAt:iso(at3),spent:{}},secsSince=from=>{let secs=Math.max(0,Math.round((at3-Date.parse(from))/1e3));return Number.isFinite(secs)?secs:0},yours=!!(t.waiting||t.takenOver);st.yoursSince&&(!yours||st.stage!==t.stage)&&(st.yours={...st.yours,[st.stage]:(st.yours?.[st.stage]??0)+secsSince(st.yoursSince)},delete st.yoursSince),st.stage!==t.stage&&(st.spent={...st.spent,[st.stage]:(st.spent[st.stage]??0)+secsSince(st.since)},st.stage=t.stage,st.since=iso(at3)),yours&&!st.yoursSince&&(st.yoursSince=iso(at3)),t.stageTimes=st}var SLOW_FLOOR_S=300;function usualSentence(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 slowAfter(u){return Math.max(2*u.high,SLOW_FLOOR_S)}function slowStep(t,book,at3=Date.now()){let c=t.clock;if(!c||t.waiting||c.paused)return null;let u=usualOf(book,c.who,c.step,t.track),n=t.steps?.length??0,heard=t.steps?.some(s=>s.status==="doing"||s.status==="done");if(c.who==="builder"&&c.step==="build"&&n>1&&!heard){let all={low:u.low*n,high:u.high*n},secs2=Math.max(0,(at3-Date.parse(c.began??c.since))/1e3);return secs2<slowAfter(all)?null:{since:c.began??c.since,kind:"long",detail:`The Builder has been building for ${minutesWords(secs2)}. ${n} steps usually take ${usualSentence(all)}.`}}let secs=stepSeconds(c,at3);if(secs<slowAfter(u))return null;let detail=c.who==="squad"?`The checks have been running for ${minutesWords(secs)}. They usually take ${usualSentence(u)}.`:`The ${WHO[c.who]} has been on this step for ${minutesWords(secs)}. It usually takes ${usualSentence(u)}.`;return{since:c.since,kind:"long",detail}}function oldStop(t){return t.stuck?t.stage==="done"||t.mergedAt?!0:t.stage!=="release"?!1:t.waiting?.kind!=="stuck"||["checks","review","proof"].includes(t.stuck.gate??""):!1}function clearStaleStop(t){t.stuck&&(t.stuck=void 0),t.waiting?.kind==="stuck"&&(t.waiting=null)}var ORDER=["tickets","specs","implement","test","review"];function hasSpec(t){return!!(t.story?.trim()||t.criteria?.length)}function hasDesign(t){return!!(t.decision||t.steps?.length)}function leavingAt(t){return t.stage==="implement"?"the build":t.stage==="test"?"the checks":t.stage==="review"?t.waiting?.kind==="proof"?"the proof":"the review":t.stage==="specs"?t.specApproved?"the design":"the spec":hasSpec(t)?"the spec":void 0}function outsideWords(o){return o.from?`Continued outside from ${o.from}`:"Worked outside squad-kit"}function showOutsideStep(t){let left2=t.takenOver?.free;if(!left2)return;let reached=t.worktree?"implement":hasSpec(t)||hasDesign(t)?"specs":"tickets",rank=s=>Math.max(ORDER.indexOf(s),0);t.stage=rank(left2.stage)>=rank(reached)?left2.stage:reached;let doing=t.worktree?"building":hasDesign(t)?"the design is written":hasSpec(t)?"the spec is written":"not started yet";t.status=`In a conversation outside squad-kit: ${doing}`}var OUTSIDE_WAITS="In a conversation outside squad-kit. Bring it back when you are done",NO_DOCS="none";function leaveForOutside(t,by2,at3,seen){let from=leavingAt(t),phase=t.squad?.phase,left2={stage:t.stage,status:t.status,waiting:t.waiting,...phase?{phase}:{},...seen,...from?{from}:{}};t.takenOver={by:by2,at:at3,session:t.sessions?.build??"",cwd:t.storyDir??t.worktree??"",free:left2},t.live=!1,t.squad=null,t.waiting={kind:"yours",summary:OUTSIDE_WAITS},showOutsideStep(t)}function gatesPassed(g){return g?.review==="pass"&&g.proof==="pass"&&(g.checks==="pass"||g.checks==="unchecked")}function markCheckedOutside(t){if(!t.outside)return;let g=t.gates;t.outside={...t.outside,...gatesPassed(g)?{checked:{at:new Date().toISOString(),by:"squad-kit",...g?.checks==="unchecked"?{noTests:!0}:{}}}:{checked:void 0}}}function dropOldChecked(t){t.outside?.checked&&!gatesPassed(t.gates)&&(t.outside={...t.outside,checked:void 0})}function changesScreens(t){return(t.work?.screens??0)>0||!!(t.outside&&!t.work)}var IS={text:x=>typeof x=="string",texts:x=>Array.isArray(x)&&x.every(y=>typeof y=="string"),object:x=>isPlain(x),objects:x=>Array.isArray(x)&&x.every(isPlain),"yes-no":x=>typeof x=="boolean",number:x=>typeof x=="number"&&Number.isFinite(x)};function isPlain(x){return typeof x=="object"&&x!==null&&!Array.isArray(x)}function wrongField(o,fields){for(let[key,shape2]of Object.entries(fields))if(o[key]!==void 0&&o[key]!==null&&!IS[shape2](o[key]))return`"${key}" is not in the right form.`;return null}function hasMergeMarks(text3){return/^<{7}( |$)[\s\S]*?^={7}\s*$[\s\S]*?^>{7}( |$)/m.test(text3)}import yaml5 from"js-yaml";function enc(v){return v.includes(`
226
227
  `)||v.startsWith('"')||v!==v.trim()?JSON.stringify(v):v}function dec(s){if(s.startsWith('"')&&s.endsWith('"')&&s.length>=2)try{return JSON.parse(s)}catch{return s}return s}var MARKER=/^(\\*)(#|- \[|\d+\. |Answer:|Chosen:|To:|- (Area|Saved as|Replaces|Check with the client|Before|After|Decision|Where|More):)/;function escText(text3){return text3.split(`
227
228
  `).map(l=>MARKER.test(l)?`\\${l}`:l).join(`
228
229
  `)}function unescText(text3){return text3.split(`
@@ -337,11 +338,11 @@ ${patterns.join(`
337
338
  `),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(`
338
339
  `))}};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(fs53.readFileSync(path45.join(squadDir,"config.yaml"),"utf8"));if(raw?.board?.id)return String(raw.board.id)}catch{}let root=path45.resolve(path45.dirname(squadDir));return`${path45.basename(root).replace(/[^\w.-]+/g,"-")||"project"}-${hash(root)}`}function boardPaths(squadDir){let squad=path45.resolve(squadDir),projectId=projectIdOf(squad),localDir=path45.join(squadHome(),"projects",projectId);return{root:path45.dirname(squad),squadDir:squad,configFile:path45.join(squad,"config.yaml"),ticketsDir:path45.join(squad,"tickets"),decisionsDir:path45.join(squad,"decisions"),releasesDir:path45.join(squad,"releases"),rulesDir:path45.join(squad,"rules"),knowledgeDir:path45.join(squad,"knowledge"),wikiDir:path45.join(squad,"knowledge","wiki"),projectId,localDir,localMetaFile:path45.join(localDir,"board.yaml"),stateDir:path45.join(localDir,"state"),chatsDir:path45.join(localDir,"chats"),proofDir:path45.join(localDir,"proof"),mediaDir:path45.join(localDir,"files"),consoleFile:path45.join(localDir,"console.json"),pidFile:path45.join(localDir,"squad.pid"),secretsFile:path45.join(localDir,"secrets.yaml"),legacyDir:path45.join(squad,"board")}}function hasLegacyBoard(bp){return fs53.existsSync(path45.join(bp.legacyDir,"board.yaml"))&&!configBoard(bp)}function boardExists(bp){return!!configBoard(bp)}function now(){return new Date().toISOString()}function writeFile(file3,text3){if(fs53.existsSync(file3)&&fs53.readFileSync(file3,"utf8")===text3)return;fs53.mkdirSync(path45.dirname(file3),{recursive:!0});let tmp=`${file3}.${process.pid}.tmp`;fs53.writeFileSync(tmp,text3,"utf8"),fs53.renameSync(tmp,file3)}function dumpYaml(data){return yaml6.dump(JSON.parse(JSON.stringify(data)),{lineWidth:110,noRefs:!0})}function readYaml(file3){return fs53.existsSync(file3)?yaml6.load(fs53.readFileSync(file3,"utf8")):void 0}function readText(file3){return fs53.existsSync(file3)?fs53.readFileSync(file3,"utf8"):void 0}var cache2=new Map;function stampOf(files){return files.map(f=>fs53.existsSync(f)?`${fs53.statSync(f).mtimeMs}:${fs53.statSync(f).size}`:"-").join("|")}function cached2(key,files,read7){let stamp=stampOf(files),hit=cache2.get(key);if(hit&&hit.stamp===stamp)return structuredClone(hit.value);let value=read7();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(fs53.readFileSync(bp.configFile,"utf8"))?.board;return isPlain(board6)&&!wrongField(board6,BOARD_FIELDS)?board6:void 0}catch{return}}function settingsProblem(bp){if(!fs53.existsSync(bp.configFile))return null;try{let raw=yaml6.load(fs53.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:path45.join(bp.knowledgeDir,"project.md"),lessons:path45.join(bp.knowledgeDir,"lessons.md"),stack:path45.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=fs53.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)))&&(fs53.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||fs53.existsSync(k.project))&&writeFile(k.project,projectDoc(summary,full.map??[])),(full.lessons?.length||fs53.existsSync(k.lessons))&&writeFile(k.lessons,lessonsDoc(full.lessons??[])),(stack?.length||patterns?.length||fs53.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",picturesNote:"local",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 path45.join(bp.ticketsDir,id2.toUpperCase())}function ticketFilesDir(bp,id2){return path45.join(ticketDir(bp,id2),"files")}function ticketMediaDir(bp,id2){return path45.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 path45.join(bp.stateDir,`${id2.toUpperCase()}.yaml`)}function ticketStateFile(bp,id2){return stateFile(bp,id2)}function inside2(dir,file3){let rel=path45.relative(dir,file3);return!!rel&&!rel.startsWith("..")&&!path45.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(q5=>q5.thread!==void 0||q5.thinking!==void 0).map(q5=>[q5.id,{...q5.thread!==void 0?{thread:q5.thread}:{},...q5.thinking!==void 0?{thinking:q5.thinking}:{}}]));Object.keys(talk2).length&&(state.questionState=talk2);let files=(t.attachments??[]).filter(f=>inside2(folder,f)).map(f=>path45.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)?path45.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(q5=>({...q5,...questionState?.[q5.id]??{}})));let files=[...(p.files??[]).map(f=>path45.join(folder,f)),...localFiles??[]];return delete t.files,files.length&&(t.attachments=files),t.mockup&&!path45.isAbsolute(t.mockup)&&(t.mockup=path45.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,read7]of Object.entries(says))if(docs[file3]?.replace(/^# .*$/m,"").trim()&&!read7())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=>path45.join(folder,f)),stateFile(bp,id2)];if(!fs53.existsSync(files[0]))return null;try{return cached2(`ticket:${folder}`,files,()=>{let docs=Object.fromEntries(DOC_FILES.map(f=>[f,readText(path45.join(folder,f))?.replace(/\r\n/g,`
339
340
  `)]).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 fs53.existsSync(bp.ticketsDir)?fs53.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(!fs53.existsSync(bp.ticketsDir))return[];let read7=new Set(listed3.map(t=>t.id)),out20=[];for(let d of fs53.readdirSync(bp.ticketsDir,{withFileTypes:!0}))if(!(!d.isDirectory()||read7.has(d.name.toUpperCase())))try{readTicketFiles(bp,d.name)}catch(err2){let e=err2;out20.push({id:e.id,problem:e.message,file:path45.relative(bp.root,path45.join(ticketDir(bp,e.id),e.file)),detail:e.detail})}return out20.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=path45.join(folder,f);docs[f]!==void 0?writeFile(file3,docs[f]):fs53.existsSync(file3)&&fs53.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){fs53.rmSync(ticketDir(bp,id2),{recursive:!0,force:!0}),fs53.rmSync(stateFile(bp,id2),{force:!0}),fs53.rmSync(ticketMediaDir(bp,id2),{recursive:!0,force:!0})}function branchNumbers(bp){if(!fs53.existsSync(path45.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(`
340
- `).map(r=>r.split("/squad/")[1]??"").filter(Boolean))}function nextTicketId(bp){let meta=readMeta(bp),id2=`${meta.key}-${meta.nextNumber}`,taken=branchNumbers(bp);for(;fs53.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=fs53.readFileSync(file3,"utf8"),name=path45.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 fs53.existsSync(dir)?fs53.readdirSync(dir).filter(f=>f.endsWith(".md")).map(f=>path45.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=path45.basename(f,".md");return detail?[{kind:kind.kind,id:id2,problem:recordProblemWords(kind.kind,id2),file:path45.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(path45.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 path45.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=path45.join(bp.releasesDir,`${v}.md`);if(!fs53.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(path45.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}:{}})):fs53.rmSync(releaseStateFile(bp,release.version),{force:!0})}function removeRelease(bp,version){fs53.rmSync(path45.join(bp.releasesDir,`${version}.md`),{force:!0}),fs53.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])fs53.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(host3,pattern){let h=host3.toLowerCase().replace(/\.$/,""),p=pattern.toLowerCase();return p==="*"?!0:p.startsWith("*.")?h.endsWith(p.slice(1)):h===p}function knownBadGroup(host3){let bare=host3.toLowerCase().replace(/^\*\./,"");return KNOWN_BAD_GROUPS.find(g=>g.hosts.some(p=>hostMatches(bare,p)))?.name??null}import fs54 from"fs";import path46 from"path";function flutterOnPath(env2=process.env){for(let dir of(env2.PATH??"").split(path46.delimiter)){if(!dir||!path46.isAbsolute(dir))continue;let program2=path46.join(dir,process.platform==="win32"?"flutter.bat":"flutter");try{if(!fs54.statSync(program2).isFile())continue;return{program:program2,root:path46.dirname(path46.dirname(fs54.realpathSync(program2)))}}catch{}}return null}var FLUTTER_STAMPS=[{file:"bin/cache/engine.stamp",regex:String.raw`bin/cache/engine\.stamp`,glob:"bin/cache/engine.stamp",why:"the engine's version, which Flutter writes on every run"},{file:"bin/cache/engine.stamp.tmp.<number>",regex:String.raw`bin/cache/engine\.stamp\.tmp\.[0-9]+`,glob:"bin/cache/engine.stamp.tmp.*",why:"the same, first written under the number of the run, then moved onto engine.stamp"},{file:"bin/cache/engine.realm",regex:String.raw`bin/cache/engine\.realm`,glob:"bin/cache/engine.realm",why:"where the engine comes from, which Flutter writes on every run (empty for its own)"}];function flutterStampRegexes(root){let base2=root.replace(/[.*+?^${}()|[\]\\]/g,"\\$&").replace(/\/$/,"");return FLUTTER_STAMPS.map(s=>`^${base2}/${s.regex}$`)}function flutterStampGlobs(root){return FLUTTER_STAMPS.map(s=>path46.join(root,s.glob))}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(path47.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 path47.join(bp.localDir,"blocked-sites.json")}function blockedSites(bp){try{let list6=JSON.parse(fs55.readFileSync(blockedSitesFile(bp),"utf8"));return Array.isArray(list6)?list6.filter(b=>b&&typeof b.host=="string").reverse():[]}catch{return[]}}function noteBlockedSite(bp,site){try{let list6=blockedSites(bp).reverse().filter(b=>b.host!==site.host||b.story!==site.story);list6.push({host:site.host,story:site.story,level:site.level,at:site.at??new Date().toISOString()}),fs55.mkdirSync(bp.localDir,{recursive:!0}),fs55.writeFileSync(blockedSitesFile(bp),`${JSON.stringify(list6.slice(-MAX_BLOCKED_SITES),null,1)}
341
+ `).map(r=>r.split("/squad/")[1]??"").filter(Boolean))}function nextTicketId(bp){let meta=readMeta(bp),id2=`${meta.key}-${meta.nextNumber}`,taken=branchNumbers(bp);for(;fs53.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=fs53.readFileSync(file3,"utf8"),name=path45.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 fs53.existsSync(dir)?fs53.readdirSync(dir).filter(f=>f.endsWith(".md")).map(f=>path45.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=path45.basename(f,".md");return detail?[{kind:kind.kind,id:id2,problem:recordProblemWords(kind.kind,id2),file:path45.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(path45.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 path45.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=path45.join(bp.releasesDir,`${v}.md`);if(!fs53.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(path45.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}:{}})):fs53.rmSync(releaseStateFile(bp,release.version),{force:!0})}function removeRelease(bp,version){fs53.rmSync(path45.join(bp.releasesDir,`${version}.md`),{force:!0}),fs53.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])fs53.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(host3,pattern){let h=host3.toLowerCase().replace(/\.$/,""),p=pattern.toLowerCase();return p==="*"?!0:p.startsWith("*.")?h.endsWith(p.slice(1)):h===p}function knownBadGroup(host3){let bare=host3.toLowerCase().replace(/^\*\./,"");return KNOWN_BAD_GROUPS.find(g=>g.hosts.some(p=>hostMatches(bare,p)))?.name??null}import fs54 from"fs";import path46 from"path";function flutterOnPath(env2=process.env){for(let dir of(env2.PATH??"").split(path46.delimiter)){if(!dir||!path46.isAbsolute(dir))continue;let program2=path46.join(dir,process.platform==="win32"?"flutter.bat":"flutter");try{if(!fs54.statSync(program2).isFile())continue;return{program:program2,root:path46.dirname(path46.dirname(fs54.realpathSync(program2)))}}catch{}}return null}var FLUTTER_STAMPS=[{file:"bin/cache/engine.stamp",regex:String.raw`bin/cache/engine\.stamp`,glob:"bin/cache/engine.stamp",why:"the engine's version, which Flutter writes on every run"},{file:"bin/cache/engine.stamp.tmp.<number>",regex:String.raw`bin/cache/engine\.stamp\.tmp\.[0-9]+`,glob:"bin/cache/engine.stamp.tmp.*",why:"the same, first written under the number of the run, then moved onto engine.stamp"},{file:"bin/cache/engine.realm",regex:String.raw`bin/cache/engine\.realm`,glob:"bin/cache/engine.realm",why:"where the engine comes from, which Flutter writes on every run (empty for its own)"}];function flutterStampRegexes(root){let base2=root.replace(/[.*+?^${}()|[\]\\]/g,"\\$&").replace(/\/$/,"");return FLUTTER_STAMPS.map(s=>`^${base2}/${s.regex}$`)}function flutterStampGlobs(root){return FLUTTER_STAMPS.map(s=>path46.join(root,s.glob))}function flutterStampFolders(root){return[...new Set(FLUTTER_STAMPS.map(s=>path46.join(root,path46.dirname(s.file))))]}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(path47.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 path47.join(bp.localDir,"blocked-sites.json")}function blockedSites(bp){try{let list6=JSON.parse(fs55.readFileSync(blockedSitesFile(bp),"utf8"));return Array.isArray(list6)?list6.filter(b=>b&&typeof b.host=="string").reverse():[]}catch{return[]}}function noteBlockedSite(bp,site){try{let list6=blockedSites(bp).reverse().filter(b=>b.host!==site.host||b.story!==site.story);list6.push({host:site.host,story:site.story,level:site.level,at:site.at??new Date().toISOString()}),fs55.mkdirSync(bp.localDir,{recursive:!0}),fs55.writeFileSync(blockedSitesFile(bp),`${JSON.stringify(list6.slice(-MAX_BLOCKED_SITES),null,1)}
341
342
  `)}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,out20={GIT_CONFIG_COUNT:String(from+PUSH_ADDRESSES.length)};return PUSH_ADDRESSES.forEach((address,i)=>{out20[`GIT_CONFIG_KEY_${from+i}`]="url.squad-kit-no-push://.pushInsteadOf",out20[`GIT_CONFIG_VALUE_${from+i}`]=address}),out20}function ticketCacheDir(bp,id2){return path47.join(bp.localDir,"caches",id2.toLowerCase())}function projectCacheDir(bp){return path47.join(bp.localDir,"caches","project")}function dropTicketCache(bp,id2){fs55.rmSync(ticketCacheDir(bp,id2),{recursive:!0,force:!0})}function packageCacheEnv(dir,env2=process.env){let at3=(...p)=>path47.join(dir,...p),gems=[env2.GEM_HOME,...(env2.GEM_PATH??"").split(path47.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(path47.delimiter)}${path47.delimiter}`,GEM_SPEC_CACHE:at3("ruby","specs"),BUNDLE_PATH:at3("ruby","bundle"),BUNDLE_USER_CACHE:at3("ruby","bundle-cache")}}function gitPlaces(cwd){let link=path47.join(cwd,".git");try{let own2=path47.resolve(cwd,/^gitdir: (.*)$/m.exec(fs55.readFileSync(link,"utf8"))[1]);return[link,own2,path47.resolve(own2,fs55.readFileSync(path47.join(own2,"commondir"),"utf8").trim())]}catch{return[link]}}function withReal(dirs){let out20=new Set;for(let d of dirs){out20.add(d);try{out20.add(fs55.realpathSync(d))}catch{}}return[...out20]}function sandboxAsk(o){let home2=o.home??os7.homedir(),temp=withReal([os7.tmpdir(),...process.platform==="win32"?[]:["/tmp"]]),caches=PACKAGE_CACHES.map(c=>path47.join(home2,c)).filter(c=>fs55.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??[]),flutter2=o.flutter===void 0?flutterOnPath()?.root:o.flutter;return{write:[...new Set([...withReal([o.proof]),...own2,...temp,...caches,...flutter2?flutterStampGlobs(flutter2):[]])],noRead:[...new Set([...PRIVATE.map(p=>path47.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}${path47.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=path47.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(list6){let hosts=[],bad2=[],closed=[];for(let raw of list6.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 fs56 from"fs";import os8 from"os";import path48 from"path";function storiesCacheDir(bp){return path48.join(bp.localDir,"caches","stories")}function ownFolder(dir){let at3=fs56.lstatSync(dir,{throwIfNoEntry:!1});at3&&!at3.isDirectory()&&fs56.rmSync(dir,{force:!0}),fs56.mkdirSync(dir,{recursive:!0})}function inside3(dir,p){try{let home2=fs56.realpathSync(dir),at3=path48.dirname(p);for(;!fs56.existsSync(at3);)at3=path48.dirname(at3);let real2=fs56.realpathSync(at3);return real2===home2||real2.startsWith(`${home2}${path48.sep}`)}catch{return!1}}function toolCacheEnv(dir){let at3=p=>path48.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"),CP_REPOS_DIR:at3(path48.join("cocoapods","repos")),CP_CACHE_DIR:at3(path48.join("cocoapods","cache")),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",COCOAPODS_DISABLE_STATS:"true"}}function pnpmStoreEnv(dir){return{pnpm_config_store_dir:path48.join(dir,"pnpm-store"),npm_config_store_dir:path48.join(dir,"pnpm-store")}}function npmCacheEnv(dir,home2){let npm=path48.join(dir,"npm");try{if(!inside3(dir,npm))return{};fs56.mkdirSync(npm,{recursive:!0});let shared=path48.join(home2,".npm","_cacache"),link=path48.join(npm,"_cacache");return fs56.existsSync(shared)&&!fs56.lstatSync(link,{throwIfNoEntry:!1})&&fs56.symlinkSync(shared,link),{npm_config_cache:npm}}catch{return{}}}function linkVersions(from,to,name){fs56.mkdirSync(to,{recursive:!0});for(let entry of fs56.existsSync(from)?fs56.readdirSync(from):[])if(name.test(entry))try{fs56.symlinkSync(path48.join(from,entry),path48.join(to,entry))}catch{}}function theirCaches(home2,env2,mac=process.platform==="darwin"){return mac?path48.join(home2,"Library","Caches"):env2.XDG_CACHE_HOME??path48.join(home2,".cache")}function browserCacheEnv(dir,home2,env2){let set2=env2.PLAYWRIGHT_BROWSERS_PATH;if(set2==="0"||process.platform==="win32")return{};let playwright=path48.join(dir,"ms-playwright");try{if(!inside3(dir,playwright))return{};linkVersions(set2?path48.resolve(set2):path48.join(theirCaches(home2,env2),"ms-playwright"),playwright,/^[a-z_]+-\d+$/);let puppeteer=path48.join(home2,".cache","puppeteer");for(let kind of fs56.existsSync(puppeteer)?fs56.readdirSync(puppeteer):[])/^chrome(-headless-shell)?$|^chromedriver$|^firefox$/.test(kind)&&linkVersions(path48.join(puppeteer,kind),path48.join(dir,"puppeteer",kind),/^[\w.-]+-[\w.]+$/);return{PLAYWRIGHT_BROWSERS_PATH:playwright}}catch{return{}}}function theirPub(home2,env2,win=process.platform==="win32"){return env2.PUB_CACHE?path48.resolve(env2.PUB_CACHE):win?path48.join(env2.LOCALAPPDATA??path48.join(home2,"AppData","Local"),"Pub","Cache"):path48.join(home2,".pub-cache")}function pubLinks(dir,cwd,home2,env2){if(!cwd||!fs56.existsSync(path48.join(cwd,"pubspec.yaml")))return;let theirs=theirPub(home2,env2),mine2=path48.join(dir,"pub");try{if(!inside3(dir,mine2)||path48.resolve(theirs)===path48.resolve(mine2))return;for(let host3 of fs56.existsSync(path48.join(theirs,"hosted"))?fs56.readdirSync(path48.join(theirs,"hosted")):[])host3.startsWith(".")||(linkVersions(path48.join(theirs,"hosted",host3),path48.join(mine2,"hosted",host3),/^[a-z0-9_]+-\d[\w.+-]*$/),linkVersions(path48.join(theirs,"hosted-hashes",host3),path48.join(mine2,"hosted-hashes",host3),/^[a-z0-9_]+-\d[\w.+-]*\.sha256$/))}catch{}}function specRepoLinks(dir,home2,env2){let theirs=env2.CP_REPOS_DIR?path48.resolve(env2.CP_REPOS_DIR):path48.join(env2.CP_HOME_DIR?path48.resolve(env2.CP_HOME_DIR):path48.join(home2,".cocoapods"),"repos"),mine2=path48.join(dir,"cocoapods","repos");try{if(!fs56.existsSync(theirs)||!inside3(dir,mine2)||theirs===path48.resolve(mine2))return;linkVersions(theirs,mine2,/^(?!trunk$|Spec_Lock$)[^.]/)}catch{}}var START_AS_COPY=[{mine:()=>path48.join("xdg","prisma"),theirs:(home2,env2)=>path48.join(env2.XDG_CACHE_HOME??path48.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 fs56.readdirSync(at3,{withFileTypes:!0})){if(total>most)return;let full=path48.join(at3,e.name);e.isDirectory()?walk3(full):e.isFile()&&(total+=fs56.statSync(full).size)}};return walk3(p),total}function madeWhole(to,make){let part=`${to}.making-${process.pid}`;fs56.rmSync(part,{recursive:!0,force:!0});try{make(part),fs56.renameSync(part,to)}catch{fs56.rmSync(part,{recursive:!0,force:!0})}}function copyWhole(from,to){madeWhole(to,part=>fs56.cpSync(from,part,{recursive:!0,verbatimSymlinks:!0,preserveTimestamps:!0,mode:fs56.constants.COPYFILE_FICLONE}))}function startHome(dir,home2,env2){for(let c of START_AS_COPY){let to=path48.join(dir,c.mine(home2,env2)),from=c.theirs(home2,env2);try{if(fs56.lstatSync(to,{throwIfNoEntry:!1})||!fs56.existsSync(from)||!inside3(dir,to)||sizeOf2(from,c.most)>c.most)continue;fs56.mkdirSync(path48.dirname(to),{recursive:!0}),copyWhole(from,to)}catch{}}for(let s of LINKED_SETTINGS){let to=path48.join(dir,s.mine),from=path48.join(home2,s.theirs);try{if(fs56.lstatSync(to,{throwIfNoEntry:!1})||!fs56.existsSync(from)||!inside3(dir,to))continue;fs56.mkdirSync(path48.dirname(to),{recursive:!0}),fs56.symlinkSync(from,to)}catch{}}}function gradleWrapper(dir,cwd,home2,env2){let said="";try{said=fs56.readFileSync(path48.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=path48.basename(url).replace(/\.zip$/,""),hash2=BigInt(`0x${createHash2("md5").update(url).digest("hex")}`).toString(36),rel=path48.join("wrapper","dists",name,hash2),theirs=path48.join(env2.GRADLE_USER_HOME??path48.join(home2,".gradle"),rel),mine2=path48.join(dir,"gradle",rel),ok2=`${name}.zip.ok`;if(fs56.lstatSync(mine2,{throwIfNoEntry:!1})||!fs56.existsSync(path48.join(theirs,ok2))||!inside3(dir,mine2))return;let unpacked=fs56.readdirSync(theirs,{withFileTypes:!0}).filter(e=>e.isDirectory());unpacked.length===1&&(fs56.mkdirSync(path48.dirname(mine2),{recursive:!0}),madeWhole(mine2,part=>{fs56.mkdirSync(part),fs56.symlinkSync(path48.join(theirs,unpacked[0].name),path48.join(part,unpacked[0].name)),fs56.copyFileSync(path48.join(theirs,ok2),path48.join(part,ok2))}))}function theirsToRead(home2,env2,mavenOpts){let out20={},gradle=path48.join(env2.GRADLE_USER_HOME??path48.join(home2,".gradle"),"caches");!env2.GRADLE_RO_DEP_CACHE&&fs56.existsSync(path48.join(gradle,"modules-2"))&&(out20.GRADLE_RO_DEP_CACHE=gradle);let nuget=env2.NUGET_PACKAGES??path48.join(home2,".nuget","packages");fs56.existsSync(nuget)&&(out20.NUGET_FALLBACK_PACKAGES=[env2.NUGET_FALLBACK_PACKAGES?.trim(),nuget].filter(Boolean).join(";"));let maven=path48.join(home2,".m2","repository");return fs56.existsSync(maven)&&!/-Dmaven\.repo\.local\.tail=/.test(mavenOpts)&&(out20.MAVEN_OPTS=`${mavenOpts} -Dmaven.repo.local.tail=${maven}`),out20}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),pubLinks(dir,o.cwd,home2,env2),process.platform==="darwin"&&specRepoLinks(dir,home2,env2)}catch{}let packages2=packageCacheEnv(dir,env2);return{...packages2,...theirsToRead(home2,env2,packages2.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=path48.relative(home2,target);if(!rel||rel.startsWith("..")||path48.isAbsolute(rel))return null;let parts2=rel.split(path48.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=path48.join(home2,...parts2.slice(0,Math.min(depth,parts2.length))),near=(a,b)=>a===b||a.startsWith(`${b}${path48.sep}`);return hidden.some(h=>near(place,h)||near(h,place))?null:place}function realFolderAt(p){let at3=fs56.lstatSync(p,{throwIfNoEntry:!1});at3?.isDirectory()||(at3&&fs56.rmSync(p,{force:!0}),fs56.mkdirSync(p))}function linkEntries(from,to,skip,hidden){let names2=[];try{names2=fs56.readdirSync(from)}catch{return}for(let name of names2){let theirs=path48.join(from,name);if(skip.has(name)||hidden.has(theirs))continue;let mine2=path48.join(to,name),at3=fs56.lstatSync(mine2,{throwIfNoEntry:!1});at3&&!at3.isSymbolicLink()||at3&&fs56.readlinkSync(mine2)===theirs||(at3&&fs56.rmSync(mine2,{force:!0}),fs56.symlinkSync(theirs,mine2))}}var COPY_MOST=200*2**20;function ownPlace(from,to,hidden){let at3=fs56.lstatSync(to,{throwIfNoEntry:!1});if(at3&&!at3.isSymbolicLink())return;at3&&fs56.rmSync(to,{force:!0});let theirs=fs56.statSync(from,{throwIfNoEntry:!1});if(theirs){if(theirs.isFile())return fs56.copyFileSync(from,to);if(theirs.isDirectory()){if(sizeOf2(from,COPY_MOST)<=COPY_MOST)return copyWhole(from,to);fs56.mkdirSync(to),linkEntries(from,to,new Set,hidden)}}}function healHome(dir,places,o){let own2=path48.join(dir,"home");ownFolder(dir),realFolderAt(own2);let hidden=new Set(o.hidden),ways=new Map;for(let place of places){let parts2=path48.relative(o.home,place).split(path48.sep);parts2.forEach((part,i)=>{let at3=parts2.slice(0,i).join(path48.sep);ways.set(at3,(ways.get(at3)??new Set).add(part))})}let placed=new Set(places.map(p=>path48.relative(o.home,p))),walk3=rel=>{let mine2=path48.join(own2,rel);linkEntries(path48.join(o.home,rel),mine2,ways.get(rel)??new Set,hidden);for(let name of ways.get(rel)??[]){let next=rel?path48.join(rel,name):name;placed.has(next)?ownPlace(path48.join(o.home,next),path48.join(own2,next),hidden):(realFolderAt(path48.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}),java2=places.some(p=>ON_JAVA.has(path48.relative(home2,p).split(path48.sep)[0])),opts=(o.env??process.env).JAVA_TOOL_OPTIONS?.trim();return{HOME:own2,...java2?{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(fs56.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(fs56.readFileSync(file3,"utf8"))}catch{}all[command]=[...new Set(places)];try{fs56.mkdirSync(path48.dirname(file3),{recursive:!0}),fs56.writeFileSync(file3,`${JSON.stringify(all,null,2)}
342
- `)}catch{}}var SEATBELT="/usr/bin/sandbox-exec";function writableCaches(home2){return PACKAGE_CACHES.map(c=>path49.join(home2,c))}function browserProfiles(home2,platform){if(platform==="darwin"){let support3=path49.join(home2,"Library","Application Support");return["Google/Chrome","Chromium","Firefox","Microsoft Edge","BraveSoftware","Arc","Vivaldi","com.operasoftware.Opera"].map(b=>path49.join(support3,b)).concat(path49.join(home2,"Library","Safari"),path49.join(home2,"Library","Application Support","Firefox"))}return[".config/google-chrome",".config/chromium",".config/BraveSoftware",".config/microsoft-edge",".mozilla"].map(b=>path49.join(home2,b))}var FINCH_ON_MAC="/Applications/Finch/lima/data";function runtimeSockets(home2,platform,env2=process.env){let inHome2=[".docker",".colima",".config/colima",".lima",".rd",".orbstack",".finch",".local/share/containers/podman"];if(platform==="darwin")for(let app of["Containers/com.docker.docker","Group Containers/group.com.docker","Group Containers/HUAUUMT7XC.dev.orbstack","Application Support/rancher-desktop"])inHome2.push(path49.join("Library",app));let inRun=["docker.sock","podman.sock","podman","finch.sock","containerd"],uid=process.getuid?.(),own2=[env2.XDG_RUNTIME_DIR,...platform==="linux"&&uid!==void 0?[`/run/user/${uid}`]:[]].filter(d=>!!d),named=/^unix:\/\/(\/.+)$/.exec(env2.DOCKER_HOST?.trim()??"")?.[1];return[...inHome2.map(p=>path49.join(home2,p)),...["/var/run","/run",...own2].flatMap(d=>inRun.map(p=>path49.join(d,p))),...[os9.tmpdir(),"/tmp"].map(d=>path49.join(d,"podman")),...platform==="darwin"?[path49.join(FINCH_ON_MAC,"finch/sock")]:[],...named?[named]:[]]}var RUNTIME_KEYS=[".lima/_config",".colima/_lima/_config",".orbstack/ssh",".local/share/containers/podman/machine/machine",".finch/lima/data/_config",".local/share/rancher-desktop/lima/_config","Library/Application Support/rancher-desktop/lima/_config"],RUNTIME_PORTS=[2375,2376];function tokenFiles(home2){let out20=[path49.join(home2,".pypirc"),path49.join(home2,".composer","auth.json"),path49.join(home2,".config","composer","auth.json"),path49.join(home2,".gem","credentials")],npmrc=path49.join(home2,".npmrc");try{/(_authToken|_auth|_password)\s*=/.test(fs57.readFileSync(npmrc,"utf8"))&&out20.push(npmrc)}catch{}return out20}function aiToolPrivate(home2){return AI_TOOLS.flatMap(t=>t.privateFiles?.(home2)??[])}function squadOwnFiles(){return BROWSERS.flatMap(b=>b.ownFiles?.()??[])}function ticketGuard(bp,o){let home2=o.home??os9.homedir(),cache4=storiesCacheDir(bp);return ownFolder(cache4),{...buildGuard({cwd:o.cwd,root:bp.root,cache:cache4,extraWrite:[bp.proofDir,cache4],extraRead:[cache4,...o.read??[]],home:home2,platform:o.platform??process.platform,...o.allow?{allow:o.allow}:{}}),noPush:!0}}function projectGuardFor(bp,o){let home2=o.home??os9.homedir(),cache4=projectCacheDir(bp);return ownFolder(cache4),buildGuard({cwd:o.cwd,root:bp.root,cache:cache4,extraWrite:[bp.proofDir,cache4],extraRead:[cache4,...o.read??[]],home:home2,platform:o.platform??process.platform,...o.allow?{allow:o.allow}:{}})}function realPlace(p){let rest=[];for(let at3=p;;at3=path49.dirname(at3))try{return path49.join(fs57.realpathSync(at3),...rest)}catch{if(at3===path49.dirname(at3))return p;rest.unshift(path49.basename(at3))}}function withReal2(dirs){let out20=new Set;for(let d of dirs)d&&(out20.add(d),out20.add(realPlace(d)));return[...out20]}function allowPath(p,home2=os9.homedir()){return path49.resolve(p.trim().replace(/^~(?=$|\/)/,home2))}function sharedPath(p,home2=os9.homedir()){let full=allowPath(p,home2);return full===home2||full.startsWith(`${home2}${path49.sep}`)?`~${full.slice(home2.length).split(path49.sep).join("/")}`:full}function privatePlaces(home2,platform){return[...PRIVATE.map(p=>path49.join(home2,p)),path49.join(home2,".config","gcloud"),path49.join(home2,".docker"),...RUNTIME_KEYS.map(p=>path49.join(home2,p)),...platform==="darwin"?[path49.join(FINCH_ON_MAC,"_config")]:[],path49.join(home2,"Library","Keychains"),...browserProfiles(home2,platform),...tokenFiles(home2),...aiToolPrivate(home2)]}function agentPrivatePlaces(home2=os9.homedir(),platform=process.platform){let own2=squadHome();return[...privatePlaces(home2,platform),...["secrets.yaml","app.json","console.json"].map(f=>path49.join(own2,f)),...["secrets.yaml","console.json"].map(f=>path49.join(own2,"projects","*",f))]}function buildGuard(o){let home2=o.home,flutter2=o.flutter===void 0?flutterOnPath()?.root:o.flutter,temp=withReal2([os9.tmpdir(),...o.platform==="win32"?[]:["/tmp","/private/tmp","/private/var/folders"]]),noRead=[...privatePlaces(home2,o.platform),...withReal2([squadHome()])],real2=p=>{try{return fs57.realpathSync(p)}catch{return p}};return{cwd:real2(o.cwd),root:o.root,write:[...new Set(withReal2([...o.extraWrite??[],...temp,...writableCaches(home2)]))],read:[...new Set(withReal2([...o.extraRead??[]]))],noRead:[...new Set(noRead)],noWrite:[...new Set(withReal2([...squadOwnFiles(),...gitPlaces(real2(o.cwd))]))],noConnect:[...new Set(withReal2(runtimeSockets(home2,o.platform)))],...o.cache?{cache:real2(o.cache)}:{},...flutter2?{writeExactly:flutterStampRegexes(real2(flutter2))}:{},...o.allow?{allow:{...o.allow.allowRead?{allowRead:withReal2(o.allow.allowRead.map(p=>allowPath(p,home2)))}:{},...o.allow.allowWrite?{allowWrite:withReal2(o.allow.allowWrite.map(p=>allowPath(p,home2)))}:{}}}:{},...networkPolicy(o.network??projectNetwork(o.root),[],o.platform).commands==="this-computer"?{offline:!0}:{}}}function sb(s){return`"${s.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`}function sbRegex(s){return`#"${s.replace(/"/g,'\\"')}"`}function ancestorsOf(paths){let out20=new Set;for(let p of paths){let dir=path49.dirname(p);for(;dir&&dir!==path49.dirname(dir);)out20.add(dir),dir=path49.dirname(dir)}return[...out20]}function envRegexFor(root){return`^${root.replace(/[.*+?^${}()|[\]\\]/g,"\\$&").replace(/\/$/,"")}/(.*/)?\\.env(\\.[^/]*)?$`}function profileText(spec){let lines3=["(version 1)","(allow default)"],allowRead=spec.allow?.allowRead??[],allowWrite=spec.allow?.allowWrite??[],reAllow=[spec.cwd,...spec.read,...allowRead,...spec.hookGit?[spec.hookGit]:[]],envRule=`(deny file-read* (regex ${sbRegex(envRegexFor(realPlace(spec.root)))}))`;lines3.push(";; reads: hide keys, sign-ins and squad-kit, then allow the copy back");for(let d of spec.noRead)lines3.push(`(deny file-read* (subpath ${sb(d)}))`);lines3.push(envRule);let writable=[spec.cwd,...spec.write,...allowWrite];for(let a of ancestorsOf([...reAllow,...writable]))lines3.push(`(allow file-read-metadata (literal ${sb(a)}))`);for(let r of reAllow)lines3.push(`(allow file-read* (subpath ${sb(r)}))`);let realRoot=realPlace(spec.root);if(!spec.ownEnv&&(spec.cwd===realRoot||spec.cwd.startsWith(`${realRoot}/`))){lines3.push(envRule);for(let a of allowRead)lines3.push(`(allow file-read* (subpath ${sb(a)}))`)}lines3.push(";; writes: only the copy and the caches"),lines3.push('(deny file-write* (subpath "/"))');for(let w of writable)lines3.push(`(allow file-write* (subpath ${sb(w)}))`);for(let dev of["/dev/null","/dev/zero","/dev/dtracehelper","/dev/tty","/dev/random","/dev/urandom","/dev/fd","/dev/stdout","/dev/stderr"])lines3.push(`(allow file-write* (subpath ${sb(dev)}))`);for(let s of spec.noConnect)lines3.push(`(deny file-write* (subpath ${sb(s)}))`);for(let a of allowWrite)lines3.push(`(allow file-write* (subpath ${sb(a)}))`);for(let r of spec.writeExactly??[])lines3.push(`(allow file-write* (regex ${sbRegex(r)}))`);for(let a of[spec.cwd,...ancestorsOf([spec.cwd,...spec.noWrite])])lines3.push(`(deny file-write-unlink (literal ${sb(a)}))`,`(deny file-write-create (literal ${sb(a)}))`);for(let n of spec.noWrite)lines3.push(`(deny file-write* (subpath ${sb(n)}))`);spec.hookGit&&lines3.push(`(allow file-write* (subpath ${sb(spec.hookGit)}))`),lines3.push(";; connections: nothing in a hidden place, and no container runtime, unless an engineer allowed its folder");for(let d of spec.noRead)lines3.push(`(deny network-outbound (subpath ${sb(d)}))`);for(let a of[spec.cwd,...spec.read,...spec.write,...spec.hookGit?[spec.hookGit]:[]])lines3.push(`(allow network-outbound (subpath ${sb(a)}))`);for(let s of spec.noConnect)lines3.push(`(deny network-outbound (subpath ${sb(s)}))`);spec.offline&&lines3.push('(deny network-outbound (remote ip "*:*"))','(allow network-outbound (remote ip "localhost:*"))');for(let port of RUNTIME_PORTS)lines3.push(`(deny network-outbound (remote tcp "localhost:${port}"))`);for(let a of[...allowRead,...allowWrite])lines3.push(`(allow network-outbound (subpath ${sb(a)}))`);return spec.openSocket&&lines3.push(`(allow network-outbound (literal ${sb(spec.openSocket)}))`),lines3.push("(deny signal)","(allow signal (target same-sandbox))"),lines3.push("(deny job-creation)"),`${lines3.join(`
343
+ `)}catch{}}var SEATBELT="/usr/bin/sandbox-exec";function writableCaches(home2){return PACKAGE_CACHES.map(c=>path49.join(home2,c))}function browserProfiles(home2,platform){if(platform==="darwin"){let support3=path49.join(home2,"Library","Application Support");return["Google/Chrome","Chromium","Firefox","Microsoft Edge","BraveSoftware","Arc","Vivaldi","com.operasoftware.Opera"].map(b=>path49.join(support3,b)).concat(path49.join(home2,"Library","Safari"),path49.join(home2,"Library","Application Support","Firefox"))}return[".config/google-chrome",".config/chromium",".config/BraveSoftware",".config/microsoft-edge",".mozilla"].map(b=>path49.join(home2,b))}var FINCH_ON_MAC="/Applications/Finch/lima/data";function runtimeSockets(home2,platform,env2=process.env){let inHome2=[".docker",".colima",".config/colima",".lima",".rd",".orbstack",".finch",".local/share/containers/podman"];if(platform==="darwin")for(let app of["Containers/com.docker.docker","Group Containers/group.com.docker","Group Containers/HUAUUMT7XC.dev.orbstack","Application Support/rancher-desktop"])inHome2.push(path49.join("Library",app));let inRun=["docker.sock","podman.sock","podman","finch.sock","containerd"],uid=process.getuid?.(),own2=[env2.XDG_RUNTIME_DIR,...platform==="linux"&&uid!==void 0?[`/run/user/${uid}`]:[]].filter(d=>!!d),named=/^unix:\/\/(\/.+)$/.exec(env2.DOCKER_HOST?.trim()??"")?.[1];return[...inHome2.map(p=>path49.join(home2,p)),...["/var/run","/run",...own2].flatMap(d=>inRun.map(p=>path49.join(d,p))),...[os9.tmpdir(),"/tmp"].map(d=>path49.join(d,"podman")),...platform==="darwin"?[path49.join(FINCH_ON_MAC,"finch/sock")]:[],...named?[named]:[]]}var RUNTIME_KEYS=[".lima/_config",".colima/_lima/_config",".orbstack/ssh",".local/share/containers/podman/machine/machine",".finch/lima/data/_config",".local/share/rancher-desktop/lima/_config","Library/Application Support/rancher-desktop/lima/_config"],RUNTIME_PORTS=[2375,2376];function tokenFiles(home2){let out20=[path49.join(home2,".pypirc"),path49.join(home2,".composer","auth.json"),path49.join(home2,".config","composer","auth.json"),path49.join(home2,".gem","credentials")],npmrc=path49.join(home2,".npmrc");try{/(_authToken|_auth|_password)\s*=/.test(fs57.readFileSync(npmrc,"utf8"))&&out20.push(npmrc)}catch{}return out20}function aiToolPrivate(home2){return AI_TOOLS.flatMap(t=>t.privateFiles?.(home2)??[])}function squadOwnFiles(){return BROWSERS.flatMap(b=>b.ownFiles?.()??[])}function ticketGuard(bp,o){let home2=o.home??os9.homedir(),cache4=storiesCacheDir(bp);return ownFolder(cache4),{...buildGuard({cwd:o.cwd,root:bp.root,cache:cache4,extraWrite:[bp.proofDir,cache4],extraRead:[cache4,...o.read??[]],home:home2,platform:o.platform??process.platform,...o.allow?{allow:o.allow}:{}}),noPush:!0}}function projectGuardFor(bp,o){let home2=o.home??os9.homedir(),cache4=projectCacheDir(bp);return ownFolder(cache4),buildGuard({cwd:o.cwd,root:bp.root,cache:cache4,extraWrite:[bp.proofDir,cache4],extraRead:[cache4,...o.read??[]],home:home2,platform:o.platform??process.platform,...o.allow?{allow:o.allow}:{}})}function realPlace(p){let rest=[];for(let at3=p;;at3=path49.dirname(at3))try{return path49.join(fs57.realpathSync(at3),...rest)}catch{if(at3===path49.dirname(at3))return p;rest.unshift(path49.basename(at3))}}function withReal2(dirs){let out20=new Set;for(let d of dirs)d&&(out20.add(d),out20.add(realPlace(d)));return[...out20]}function allowPath(p,home2=os9.homedir()){return path49.resolve(p.trim().replace(/^~(?=$|\/)/,home2))}function sharedPath(p,home2=os9.homedir()){let full=allowPath(p,home2);return full===home2||full.startsWith(`${home2}${path49.sep}`)?`~${full.slice(home2.length).split(path49.sep).join("/")}`:full}function privatePlaces(home2,platform){return[...PRIVATE.map(p=>path49.join(home2,p)),path49.join(home2,".config","gcloud"),path49.join(home2,".docker"),...RUNTIME_KEYS.map(p=>path49.join(home2,p)),...platform==="darwin"?[path49.join(FINCH_ON_MAC,"_config")]:[],path49.join(home2,"Library","Keychains"),...browserProfiles(home2,platform),...tokenFiles(home2),...aiToolPrivate(home2)]}function agentPrivatePlaces(home2=os9.homedir(),platform=process.platform){let own2=squadHome();return[...privatePlaces(home2,platform),...["secrets.yaml","app.json","console.json"].map(f=>path49.join(own2,f)),...["secrets.yaml","console.json"].map(f=>path49.join(own2,"projects","*",f))]}function buildGuard(o){let home2=o.home,flutter2=o.flutter===void 0?flutterOnPath()?.root:o.flutter,temp=withReal2([os9.tmpdir(),...o.platform==="win32"?[]:["/tmp","/private/tmp","/private/var/folders"]]),noRead=[...privatePlaces(home2,o.platform),...withReal2([squadHome()])],real2=p=>{try{return fs57.realpathSync(p)}catch{return p}};return{cwd:real2(o.cwd),root:o.root,write:[...new Set(withReal2([...o.extraWrite??[],...temp,...writableCaches(home2)]))],read:[...new Set(withReal2([...o.extraRead??[]]))],noRead:[...new Set(noRead)],noWrite:[...new Set(withReal2([...squadOwnFiles(),...gitPlaces(real2(o.cwd))]))],noConnect:[...new Set(withReal2(runtimeSockets(home2,o.platform)))],...o.cache?{cache:real2(o.cache)}:{},...flutter2?{writeExactly:flutterStampRegexes(real2(flutter2)),writeExactlyIn:flutterStampFolders(real2(flutter2))}:{},...o.allow?{allow:{...o.allow.allowRead?{allowRead:withReal2(o.allow.allowRead.map(p=>allowPath(p,home2)))}:{},...o.allow.allowWrite?{allowWrite:withReal2(o.allow.allowWrite.map(p=>allowPath(p,home2)))}:{}}}:{},...networkPolicy(o.network??projectNetwork(o.root),[],o.platform).commands==="this-computer"?{offline:!0}:{}}}function sb(s){return`"${s.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`}function sbRegex(s){return`#"${s.replace(/"/g,'\\"')}"`}function ancestorsOf(paths){let out20=new Set;for(let p of paths){let dir=path49.dirname(p);for(;dir&&dir!==path49.dirname(dir);)out20.add(dir),dir=path49.dirname(dir)}return[...out20]}function envRegexFor(root){return`^${root.replace(/[.*+?^${}()|[\]\\]/g,"\\$&").replace(/\/$/,"")}/(.*/)?\\.env(\\.[^/]*)?$`}function profileText(spec){let lines3=["(version 1)","(allow default)"],allowRead=spec.allow?.allowRead??[],allowWrite=spec.allow?.allowWrite??[],reAllow=[spec.cwd,...spec.read,...allowRead,...spec.hookGit?[spec.hookGit]:[]],envRule=`(deny file-read* (regex ${sbRegex(envRegexFor(realPlace(spec.root)))}))`;lines3.push(";; reads: hide keys, sign-ins and squad-kit, then allow the copy back");for(let d of spec.noRead)lines3.push(`(deny file-read* (subpath ${sb(d)}))`);lines3.push(envRule);let writable=[spec.cwd,...spec.write,...allowWrite];for(let a of ancestorsOf([...reAllow,...writable]))lines3.push(`(allow file-read-metadata (literal ${sb(a)}))`);for(let r of reAllow)lines3.push(`(allow file-read* (subpath ${sb(r)}))`);let realRoot=realPlace(spec.root);if(!spec.ownEnv&&(spec.cwd===realRoot||spec.cwd.startsWith(`${realRoot}/`))){lines3.push(envRule);for(let a of allowRead)lines3.push(`(allow file-read* (subpath ${sb(a)}))`)}lines3.push(";; writes: only the copy and the caches"),lines3.push('(deny file-write* (subpath "/"))');for(let w of writable)lines3.push(`(allow file-write* (subpath ${sb(w)}))`);for(let dev of["/dev/null","/dev/zero","/dev/dtracehelper","/dev/tty","/dev/random","/dev/urandom","/dev/fd","/dev/stdout","/dev/stderr"])lines3.push(`(allow file-write* (subpath ${sb(dev)}))`);for(let s of spec.noConnect)lines3.push(`(deny file-write* (subpath ${sb(s)}))`);for(let a of allowWrite)lines3.push(`(allow file-write* (subpath ${sb(a)}))`);for(let r of spec.writeExactly??[])lines3.push(`(allow file-write* (regex ${sbRegex(r)}))`);for(let a of[spec.cwd,...ancestorsOf([spec.cwd,...spec.noWrite])])lines3.push(`(deny file-write-unlink (literal ${sb(a)}))`,`(deny file-write-create (literal ${sb(a)}))`);for(let n of spec.noWrite)lines3.push(`(deny file-write* (subpath ${sb(n)}))`);spec.hookGit&&lines3.push(`(allow file-write* (subpath ${sb(spec.hookGit)}))`),lines3.push(";; connections: nothing in a hidden place, and no container runtime, unless an engineer allowed its folder");for(let d of spec.noRead)lines3.push(`(deny network-outbound (subpath ${sb(d)}))`);for(let a of[spec.cwd,...spec.read,...spec.write,...spec.hookGit?[spec.hookGit]:[]])lines3.push(`(allow network-outbound (subpath ${sb(a)}))`);for(let s of spec.noConnect)lines3.push(`(deny network-outbound (subpath ${sb(s)}))`);spec.offline&&lines3.push('(deny network-outbound (remote ip "*:*"))','(allow network-outbound (remote ip "localhost:*"))');for(let port of RUNTIME_PORTS)lines3.push(`(deny network-outbound (remote tcp "localhost:${port}"))`);for(let a of[...allowRead,...allowWrite])lines3.push(`(allow network-outbound (subpath ${sb(a)}))`);return spec.openSocket&&lines3.push(`(allow network-outbound (literal ${sb(spec.openSocket)}))`),lines3.push("(deny signal)","(allow signal (target same-sandbox))"),lines3.push("(deny job-creation)"),`${lines3.join(`
343
344
  `)}
344
- `}function bubblewrapArgs(spec,command){let args=["--ro-bind","/","/","--dev","/dev","--proc","/proc","--tmpfs","/tmp","--die-with-parent","--new-session","--unshare-pid","--unshare-uts","--unshare-ipc"],at3=places=>[...new Set(places.map(realPlace))],hide=places=>{for(let d of at3(places))fs57.existsSync(d)&&(looksLikeFile(d)?args.push("--ro-bind-try","/dev/null",d):args.push("--tmpfs",d))};hide(spec.noRead),args.push("--bind",spec.cwd,spec.cwd);for(let r of at3(spec.read))args.push("--ro-bind-try",r,r);for(let w of at3(spec.write))args.push("--bind-try",w,w);let own2=realPlace(spec.cwd);hide([...spec.noConnect,...envFilesIn(spec.root).filter(f=>!spec.ownEnv||!realPlace(f).startsWith(`${own2}/`))]);for(let r of at3(spec.allow?.allowRead??[]))args.push("--ro-bind-try",r,r);for(let w of at3(spec.allow?.allowWrite??[]))args.push("--bind-try",w,w);for(let n of at3(spec.noWrite))args.push("--ro-bind-try",n,n);return spec.hookGit&&(args.push("--bind",spec.hookGit,spec.hookGit),fs57.lstatSync(path49.join(spec.cwd,".git"),{throwIfNoEntry:!1})?.isFile()&&args.push("--ro-bind-try",path49.join(spec.hookGit,HOOK_GIT_LINK),path49.join(spec.cwd,".git"))),args.push("/bin/sh","-c",command),args}var NO_ENV_IN=new Set(["node_modules","vendor","dist","build","target","venv",".venv",".git",".squad"]);function envFilesIn(root,depth=3){let entries;try{entries=fs57.readdirSync(root,{withFileTypes:!0})}catch{return[]}return entries.flatMap(e=>{let at3=path49.join(root,e.name);return e.isDirectory()?depth>0&&!NO_ENV_IN.has(e.name)?envFilesIn(at3,depth-1):[]:/^\.env(\.|$)/.test(e.name)?[at3]:[]})}function looksLikeFile(p){try{return!fs57.statSync(p).isDirectory()}catch{return/\.[A-Za-z0-9]{1,8}$/.test(path49.basename(p))}}function guardSupportOn(h){return h.platform==="darwin"?h.has(SEATBELT)?{on:!0,why:"squad-kit's guard (macOS Seatbelt)"}:{on:!1,why:"macOS sandbox-exec is missing on this Mac"}:h.platform==="win32"?{on:!1,why:"squad-kit's guard does not run on Windows yet"}:h.platform!=="linux"?{on:!1,why:"squad-kit's guard does not run on this system"}:/-Microsoft$/i.test(h.release)?{on:!1,why:"WSL 1 has no guard",fix:"Move this Linux to WSL 2: wsl --set-version <name> 2"}:h.has("bwrap")?h.bwrapWorks()?{on:!0,why:"squad-kit's guard (bubblewrap)"}:{on:!1,why:"bubblewrap cannot make a guard on this Linux",fix:"Allow unprivileged user namespaces for bwrap (on Ubuntu 24.04 this needs an AppArmor profile for it)"}:{on:!1,why:"bubblewrap is not installed",fix:"Install it, for example: sudo apt install bubblewrap"}}var support2=null;function guardSupport(){return support2??=guardSupportOn({platform:process.platform,release:os9.release(),has:onPath2,bwrapWorks:bwrapWorks2})}function onPath2(program2){return path49.isAbsolute(program2)?fs57.existsSync(program2):(process.env.PATH??"").split(path49.delimiter).some(d=>d&&fs57.existsSync(path49.join(d,program2)))}function bwrapWorks2(){try{return execFileSync6("bwrap",["--ro-bind","/","/","--dev","/dev","true"],{stdio:"ignore",timeout:5e3}),!0}catch{return!1}}function secretEnvNames(){return[...new Set(["SQUAD_*",...AI_TOOLS.flatMap(t=>t.secretEnv??[]),"ANTHROPIC_*","OPENAI_*","CLAUDE_*","GITHUB_TOKEN","GH_TOKEN","GH_ENTERPRISE_TOKEN","GITHUB_ENTERPRISE_TOKEN","AWS_*","AZURE_*","GOOGLE_*","GCLOUD_*","GCP_*","JIRA_*","NPM_TOKEN","NODE_AUTH_TOKEN"])]}function isSecretEnv(name,names2=secretEnvNames()){let upper=name.toUpperCase();return names2.some(n=>n.endsWith("*")?upper.startsWith(n.slice(0,-1).toUpperCase()):upper===n.toUpperCase())}function projectEnv(extra={}){let names2=secretEnvNames(),env2=childEnv();for(let key of Object.keys(env2))isSecretEnv(key,names2)&&delete env2[key];return{...env2,...extra}}function guardEnv(guard,cwd){return{...guard?.cache?cacheHomeEnv(guard.cache,{wall:"project",cwd}):{},...guard?.noPush?noPushEnv():{}}}function spawnProject(command,o){let env2={...projectEnv(),...guardEnv(o.guard,o.cwd),...o.env??{},...copyDbEnv(o.cwd)},stdio=o.stdio??["ignore","pipe","pipe"],base2={cwd:o.cwd,detached:OWN_GROUP,stdio,env:env2};return o.guard&&process.platform==="darwin"?track(spawn7(SEATBELT,["-p",profileText(o.guard),"/bin/sh","-c",command],base2)):o.guard&&process.platform==="linux"?(makeOwnFolders(o.guard),track(spawn7("bwrap",bubblewrapArgs(o.guard,command),base2))):track(spawn7(command,{...base2,shell:!0}))}var HOOK_GIT_LINK="dotgit";function commitGuard(guard,at3){return{...guard,noWrite:[...new Set([...guard.noWrite,at3.common,at3.own])],hookGit:at3.gitDir}}function makeOwnFolders(guard,home2=squadHome()){for(let w of guard.write)if(!path49.relative(home2,w).startsWith(".."))try{fs57.mkdirSync(w,{recursive:!0})}catch{}}var BLOCKED_LINES=[/(?:EPERM|EROFS)[^\n]*?['"]([^'"\n]+)['"]/i,/(?:operation not permitted|read-only file system)[^\n]*?['"]([^'"\n]+)['"]/i,/['"]([^'"\n]+)['"]: (?:Operation not permitted|Read-only file system)/i,/([^\s'":]+): (?:Operation not permitted|Read-only file system)/i,/(?:Operation not permitted|Read-only file system)[^\n]*?(\/[^\s'":]+)/i,/['"](\/[^'"\n]+)['"][^\n]*?(?:Operation not permitted|Read-only file system)/i],NPM_REFUSED=/npm (?:error|ERR!) code (?:EPERM|EROFS|EACCES)[\s\S]*?npm (?:error|ERR!) path (\/[^\n]+)/,SOCKET_REFUSED=[/\bunix:\/\/(\/[^\s'":]+?)\.?(?=[\s'":]|$)/,/dial unix (\/[^\s'":]+)/,/connect E[A-Z]+ (\/[^\s'":]+)/],PORT_REFUSED=/(?:dial tcp |connect E[A-Z]+ |daemon at tcp:\/\/)(?:127\.0\.0\.1|\[?::1\]?|localhost):(2375|2376)\b/,RUNTIME_NOT_FOUND=/Could not find a (?:working container runtime|valid Docker environment)|Error while fetching server API version|docker: (?:unknown command: docker compose|'compose' is not a docker command)/;function runtimeRefused(output,guard){let under=(place,base2)=>place===base2||place.startsWith(`${base2.replace(/\/$/,"")}/`),allowed=[...guard.allow?.allowRead??[],...guard.allow?.allowWrite??[]],open3=guard.noConnect.some(s=>allowed.some(a=>under(s,a)||under(a,s)));for(let line3 of output.split(`
345
+ `}function bubblewrapArgs(spec,command){let args=["--ro-bind","/","/","--dev","/dev","--proc","/proc","--tmpfs","/tmp","--die-with-parent","--new-session","--unshare-pid","--unshare-uts","--unshare-ipc"],at3=places=>[...new Set(places.map(realPlace))],hide=places=>{for(let d of at3(places))fs57.existsSync(d)&&(looksLikeFile(d)?args.push("--ro-bind-try","/dev/null",d):args.push("--tmpfs",d))};for(let dir of at3(spec.writeExactlyIn??[]))args.push(...throwawayFolder(dir,spec.writeExactly??[]));hide(spec.noRead),args.push("--bind",spec.cwd,spec.cwd);for(let r of at3(spec.read))args.push("--ro-bind-try",r,r);for(let w of at3(spec.write))args.push("--bind-try",w,w);let own2=realPlace(spec.cwd);hide([...spec.noConnect,...envFilesIn(spec.root).filter(f=>!spec.ownEnv||!realPlace(f).startsWith(`${own2}/`))]);for(let r of at3(spec.allow?.allowRead??[]))args.push("--ro-bind-try",r,r);for(let w of at3(spec.allow?.allowWrite??[]))args.push("--bind-try",w,w);for(let n of at3(spec.noWrite))args.push("--ro-bind-try",n,n);return spec.hookGit&&(args.push("--bind",spec.hookGit,spec.hookGit),fs57.lstatSync(path49.join(spec.cwd,".git"),{throwIfNoEntry:!1})?.isFile()&&args.push("--ro-bind-try",path49.join(spec.hookGit,HOOK_GIT_LINK),path49.join(spec.cwd,".git"))),args.push("/bin/sh","-c",command),args}function throwawayFolder(dir,leaveOut2){let names2;try{if(!fs57.statSync(dir).isDirectory())return[];names2=fs57.readdirSync(dir)}catch{return[]}let out20=["--tmpfs",dir];for(let name of names2){let at3=path49.join(dir,name);if(!leaveOut2.some(r=>new RegExp(r).test(at3)))try{fs57.lstatSync(at3).isSymbolicLink()?out20.push("--symlink",fs57.readlinkSync(at3),at3):out20.push("--ro-bind-try",at3,at3)}catch{}}return out20}var NO_ENV_IN=new Set(["node_modules","vendor","dist","build","target","venv",".venv",".git",".squad"]);function envFilesIn(root,depth=3){let entries;try{entries=fs57.readdirSync(root,{withFileTypes:!0})}catch{return[]}return entries.flatMap(e=>{let at3=path49.join(root,e.name);return e.isDirectory()?depth>0&&!NO_ENV_IN.has(e.name)?envFilesIn(at3,depth-1):[]:/^\.env(\.|$)/.test(e.name)?[at3]:[]})}function looksLikeFile(p){try{return!fs57.statSync(p).isDirectory()}catch{return/\.[A-Za-z0-9]{1,8}$/.test(path49.basename(p))}}function guardSupportOn(h){return h.platform==="darwin"?h.has(SEATBELT)?{on:!0,why:"squad-kit's guard (macOS Seatbelt)"}:{on:!1,why:"macOS sandbox-exec is missing on this Mac"}:h.platform==="win32"?{on:!1,why:"squad-kit's guard does not run on Windows yet"}:h.platform!=="linux"?{on:!1,why:"squad-kit's guard does not run on this system"}:/-Microsoft$/i.test(h.release)?{on:!1,why:"WSL 1 has no guard",fix:"Move this Linux to WSL 2: wsl --set-version <name> 2"}:h.has("bwrap")?h.bwrapWorks()?{on:!0,why:"squad-kit's guard (bubblewrap)"}:{on:!1,why:"bubblewrap cannot make a guard on this Linux",fix:"Allow unprivileged user namespaces for bwrap (on Ubuntu 24.04 this needs an AppArmor profile for it)"}:{on:!1,why:"bubblewrap is not installed",fix:"Install it, for example: sudo apt install bubblewrap"}}var support2=null;function guardSupport(){return support2??=guardSupportOn({platform:process.platform,release:os9.release(),has:onPath2,bwrapWorks:bwrapWorks2})}function onPath2(program2){return path49.isAbsolute(program2)?fs57.existsSync(program2):(process.env.PATH??"").split(path49.delimiter).some(d=>d&&fs57.existsSync(path49.join(d,program2)))}function bwrapWorks2(){try{return execFileSync6("bwrap",["--ro-bind","/","/","--dev","/dev","true"],{stdio:"ignore",timeout:5e3}),!0}catch{return!1}}function secretEnvNames(){return[...new Set(["SQUAD_*",...AI_TOOLS.flatMap(t=>t.secretEnv??[]),"ANTHROPIC_*","OPENAI_*","CLAUDE_*","GITHUB_TOKEN","GH_TOKEN","GH_ENTERPRISE_TOKEN","GITHUB_ENTERPRISE_TOKEN","AWS_*","AZURE_*","GOOGLE_*","GCLOUD_*","GCP_*","JIRA_*","NPM_TOKEN","NODE_AUTH_TOKEN"])]}function isSecretEnv(name,names2=secretEnvNames()){let upper=name.toUpperCase();return names2.some(n=>n.endsWith("*")?upper.startsWith(n.slice(0,-1).toUpperCase()):upper===n.toUpperCase())}function projectEnv(extra={}){let names2=secretEnvNames(),env2=childEnv();for(let key of Object.keys(env2))isSecretEnv(key,names2)&&delete env2[key];return{...env2,...extra}}function guardEnv(guard,cwd){return{...guard?.cache?cacheHomeEnv(guard.cache,{wall:"project",cwd}):{},...guard?.noPush?noPushEnv():{}}}function spawnProject(command,o){let env2={...projectEnv(),...guardEnv(o.guard,o.cwd),...o.env??{},...copyDbEnv(o.cwd)},stdio=o.stdio??["ignore","pipe","pipe"],base2={cwd:o.cwd,detached:OWN_GROUP,stdio,env:env2};return o.guard&&process.platform==="darwin"?track(spawn7(SEATBELT,["-p",profileText(o.guard),"/bin/sh","-c",command],base2)):o.guard&&process.platform==="linux"?(makeOwnFolders(o.guard),track(spawn7("bwrap",bubblewrapArgs(o.guard,command),base2))):track(spawn7(command,{...base2,shell:!0}))}var HOOK_GIT_LINK="dotgit";function commitGuard(guard,at3){return{...guard,noWrite:[...new Set([...guard.noWrite,at3.common,at3.own])],hookGit:at3.gitDir}}function makeOwnFolders(guard,home2=squadHome()){for(let w of guard.write)if(!path49.relative(home2,w).startsWith(".."))try{fs57.mkdirSync(w,{recursive:!0})}catch{}}var BLOCKED_LINES=[/(?:EPERM|EROFS)[^\n]*?['"]([^'"\n]+)['"]/i,/(?:operation not permitted|read-only file system)[^\n]*?['"]([^'"\n]+)['"]/i,/['"]([^'"\n]+)['"]: (?:Operation not permitted|Read-only file system)/i,/([^\s'":]+): (?:Operation not permitted|Read-only file system)/i,/(?:Operation not permitted|Read-only file system)[^\n]*?(\/[^\s'":]+)/i,/['"](\/[^'"\n]+)['"][^\n]*?(?:Operation not permitted|Read-only file system)/i],NPM_REFUSED=/npm (?:error|ERR!) code (?:EPERM|EROFS|EACCES)[\s\S]*?npm (?:error|ERR!) path (\/[^\n]+)/,SOCKET_REFUSED=[/\bunix:\/\/(\/[^\s'":]+?)\.?(?=[\s'":]|$)/,/dial unix (\/[^\s'":]+)/,/connect E[A-Z]+ (\/[^\s'":]+)/],PORT_REFUSED=/(?:dial tcp |connect E[A-Z]+ |daemon at tcp:\/\/)(?:127\.0\.0\.1|\[?::1\]?|localhost):(2375|2376)\b/,RUNTIME_NOT_FOUND=/Could not find a (?:working container runtime|valid Docker environment)|Error while fetching server API version|docker: (?:unknown command: docker compose|'compose' is not a docker command)/;function runtimeRefused(output,guard){let under=(place,base2)=>place===base2||place.startsWith(`${base2.replace(/\/$/,"")}/`),allowed=[...guard.allow?.allowRead??[],...guard.allow?.allowWrite??[]],open3=guard.noConnect.some(s=>allowed.some(a=>under(s,a)||under(a,s)));for(let line3 of output.split(`
345
346
  `)){for(let re of SOCKET_REFUSED){let place=re.exec(line3)?.[1];if(place&&guard.noConnect.some(s=>under(place,s))&&!allowed.some(a=>under(place,a)))return place}let port=PORT_REFUSED.exec(line3)?.[1];if(port)return`localhost:${port}`;if(!open3&&RUNTIME_NOT_FOUND.test(line3))return"Docker"}return null}function guardBlockedPath(output,guard){if(!output)return null;let runtime=runtimeRefused(output,guard);if(runtime)return runtime;let npm=NPM_REFUSED.exec(output)?.[1]?.trim();if(npm&&isBlocked(npm,guard))return npm;for(let line3 of output.split(`
346
347
  `))for(let re of BLOCKED_LINES){let said=re.exec(line3)?.[1]?.trim(),target=said&&path49.resolve(guard.cwd,said);if(target&&isBlocked(target,guard))return target}return null}function homeCachesRefused(output,guard,home2=os9.homedir()){if(!output)return[];let places=[NPM_REFUSED.exec(output)?.[1]?.trim(),...output.split(`
347
348
  `).flatMap(line3=>BLOCKED_LINES.map(re=>re.exec(line3)?.[1]?.trim()))].flatMap(p=>{let target=p&&path49.resolve(guard.cwd,p),place=target&&isBlocked(target,guard)?homeCachePlace(target,home2,guard.noRead):null;return place?[place]:[]});return[...new Set(places)]}function isBlocked(target,guard){let under=base2=>target===base2||target.startsWith(`${base2.replace(/\/$/,"")}/`);return guard.noWrite.some(under)?!0:(guard.writeExactly??[]).some(r=>new RegExp(r).test(target))||[guard.cwd,...guard.read,...guard.cache?[guard.cache]:[],...guard.allow?.allowRead??[],...guard.allow?.allowWrite??[]].some(under)?!1:guard.noRead.some(under)||under(guard.root)&&/\/\.env(\.|$)/.test(target)?!0:!guard.write.some(under)&&!/^\/(?:dev|proc|sys)\//.test(target)}var GUARD_BLOCKED="A check needed a file outside its copy, and squad-kit blocked it. An engineer can allow it in Settings.",GUARD_BLOCKED_DOCKER="A check tried to use Docker, which is not open to the project's own commands. An engineer can allow it in Settings.";function dockerBlockedWords(output,guard){if(!guard)return null;let target=guardBlockedPath(output,guard);return target&&blockedDocker(target,guard)?GUARD_BLOCKED_DOCKER:null}function blockedDocker(target,guard){return!target.startsWith("/")||guard.noConnect.some(s=>target===s||target.startsWith(`${s}/`))}function guardBlockedDetail(target,command,guard){if(!guard||!blockedDocker(target,guard))return`squad-kit's guard blocked ${target} for: ${command}
@@ -425,8 +426,7 @@ ${parts2.join(`
425
426
  `).some(l=>/^[ADRC]/.test(l))})}var SKIP=new Set(["node_modules",".git",".squad","dist","build",".next","vendor"]);function findNestedRepos(root){return fs65.existsSync(root)?fs65.readdirSync(root,{withFileTypes:!0}).filter(d=>d.isDirectory()&&!SKIP.has(d.name)&&!d.name.startsWith(".")).filter(d=>fs65.existsSync(path58.join(root,d.name,".git"))).map(d=>{let dir=path58.join(root,d.name);return{path:d.name,base:currentBranch(dir),remote:remotes(dir)[0]??"",commands:{}}}):[]}var HELPERS=new Set(["scripts","tools","docs"]);function isOwnCode(file3){if(!LANG[path58.extname(file3).toLowerCase()])return!1;let parts2=file3.split("/");return!HELPERS.has(parts2[0])&&!parts2.some(p=>p.startsWith(".")||SKIP.has(p))}function workspaceRepos(root){let nested=findNestedRepos(root);if(!nested.length||!isRepo(root))return nested;let files=(...args)=>git(root,["ls-files","-z",...args],{allowFail:!0}).split("\0").filter(Boolean),inside5=(file3,r)=>file3===r.path||file3.startsWith(`${r.path}/`),tracked=files("--cached"),alone=nested.filter(r=>fs65.statSync(path58.join(root,r.path,".git")).isDirectory()&&!tracked.some(f=>inside5(f,r)));return[...tracked,...files("--others","--exclude-standard")].filter(f=>!alone.some(r=>inside5(f,r))).some(isOwnCode)?[]:alone}function rulesFiles(root){let found2=[];for(let f of["CLAUDE.md","AGENTS.md"]){let p=path58.join(root,f);fs65.existsSync(p)&&found2.push(p)}let rulesDir=path58.join(root,".claude","rules");if(fs65.existsSync(rulesDir))for(let f of fs65.readdirSync(rulesDir))f.endsWith(".md")&&found2.push(path58.join(rulesDir,f));let wiki=path58.join(root,".squad","board","context","wiki");if(fs65.existsSync(wiki))for(let f of fs65.readdirSync(wiki).sort())f.endsWith(".md")&&found2.push(path58.join(wiki,f));return found2}var NOISE=new Set(["node_modules","dist","build","coverage","__pycache__","vendor","target","bin","obj","venv",SQUAD_DIR]),MAX_FILES=5e3,WEB_FILE=/\.(html?|vue|jsx|tsx|svelte|cshtml|razor)$/i,WEB_DEPS=/"(react|vue|@angular\/core|next|nuxt|svelte|express|hono|fastify|koa|@nestjs\/core)"/,WEB_PY=/^(flask|django|fastapi|starlette|bottle|tornado)\b/im,TEST_FILE=/(^|\/)(tests?|__tests__|specs?|e2e)\/|[._-](test|spec)\.[a-z]+$|(^|\/)test_[^/]+\.py$|_test\.(go|py)$|Tests?\.(cs|java|kt)$/i;async function filesOf(dir){let files=[],count3=0,add2=f=>{count3+=1,files.length<MAX_FILES&&files.push(f)};if(isRepo(dir)&&!workspaceRepos(dir).length&&(await gitRecords(dir,["ls-files","--cached","--others","--exclude-standard"],`
426
427
  `,f=>{f.startsWith(`${SQUAD_DIR}/`)||add2(f)}).catch(()=>{}),count3))return{count:count3,files};let walk3=async rel=>{let entries;try{entries=await fs66.promises.readdir(path59.join(dir,rel),{withFileTypes:!0})}catch{return}for(let e of entries){if(e.name.startsWith(".")||NOISE.has(e.name))continue;let p=rel?`${rel}/${e.name}`:e.name;e.isDirectory()?await walk3(p):e.isFile()&&add2(p)}};return await walk3(""),{count:count3,files}}function read(file3){try{return fs66.readFileSync(file3,"utf8")}catch{return""}}function kindOfCode(dir,files){let count3=new Map;for(let f of files){let lang2=LANG[path59.extname(f).toLowerCase()];lang2&&count3.set(lang2,(count3.get(lang2)??0)+1)}let ranked=[...count3.entries()].sort((a,b)=>b[1]-a[1]),language=(ranked.find(([l])=>l!=="HTML")??ranked[0])?.[0],web=files.some(f=>WEB_FILE.test(f))||WEB_DEPS.test(read(path59.join(dir,"package.json")))||WEB_PY.test(read(path59.join(dir,"requirements.txt")))||/(flask|django|fastapi)/i.test(read(path59.join(dir,"pyproject.toml")))||files.some(f=>/(^|\/)manage\.py$/.test(f));return{...language?{language}:{},web}}function boardOf(dir){try{return yaml8.load(fs66.readFileSync(path59.join(dir,SQUAD_DIR,"config.yaml"),"utf8"))?.board??null}catch{return}}function real(dir){try{return fs66.realpathSync(dir)}catch{return path59.resolve(dir)}}function repoTop(dir){let top=git(dir,["rev-parse","--show-toplevel"],{allowFail:!0});return top?real(top):null}function partOfProject(dir,project){let abs=real(dir),top=repoTop(abs);if(top&&!path59.relative(top,real(project)).startsWith(".."))return!0;let repos=boardOf(project)?.repos;return Array.isArray(repos)?repos.some(r=>{if(typeof r?.path!="string")return!1;let rel=path59.relative(real(path59.join(project,r.path)),abs);return!rel.startsWith("..")&&!path59.isAbsolute(rel)}):!1}function addAsk(dir){let abs=path59.resolve(dir);if(fs66.existsSync(path59.join(abs,SQUAD_DIR,"config.yaml")))return null;let above=findSquadRoot(path59.dirname(abs));if(above)return partOfProject(abs,above)?null:{kind:"inside-project",root:above,name:path59.basename(above),words:`This folder sits inside another squad-kit project, in ${above}. Open that project, or add this folder on its own?`};let top=repoTop(abs);return!top||top===real(abs)||!isOwnFolder(top)?null:{kind:"inside-repo",root:top,name:path59.basename(top),words:`This folder is part of a bigger project, ${path59.basename(top)}, in ${top}. Its tests and app run from the top folder, so squad-kit works from there too. Add the top folder instead.`}}var plural=(n,one2,many2=`${one2}s`)=>`${n.toLocaleString("en-US")} ${n===1?one2:many2}`;function listWords(names2){if(names2.length<=1)return names2.join("");let shown2=names2.slice(0,4),rest=names2.length-shown2.length;return rest?`${shown2.join(", ")} and ${rest} more`:`${shown2.slice(0,-1).join(", ")} and ${shown2.at(-1)}`}function describeCode(n,language,web,tests){let what=language==="HTML"?"web site":web?"web app":"project",head=`${language&&language!=="HTML"?`${language} `:""}${what}`;return`${/^[AEIOU]/.test(head)?"An":"A"} ${head} with ${plural(n,"file")}${tests?" and tests":""}`}var SAFE="Your code is not changed. squad-kit only adds a .squad folder for the project's memory.",TAKES="Takes about 3 to 5 minutes. You can keep working meanwhile.";async function gitState(dir){if(!isRepo(dir)||workspaceRepos(dir).length)return{unsaved:0};let branch=git(dir,["rev-parse","--abbrev-ref","HEAD"],{allowFail:!0}),unsaved=(await gitLater(dir,["status","--porcelain"],{allowFail:!0,env:{GIT_OPTIONAL_LOCKS:"0"}})).split(`
427
428
  `).filter(l=>l.trim()&&!l.slice(3).startsWith(`${SQUAD_DIR}/`)).length;return{...branch&&branch!=="HEAD"?{branch}:{},unsaved}}async function countFiles(dir){return(await filesOf(dir)).count}function nextToNothing(dir){if(isRepo(dir)&&!workspaceRepos(dir).length){let list6=git(dir,["ls-files","--cached","--others","--exclude-standard"],{allowFail:!0}).split(`
428
- `).filter(f=>f&&!f.startsWith(`${SQUAD_DIR}/`));if(list6.length)return list6.length<=1}let found2=0,walk3=at3=>{let entries=[];try{entries=fs66.readdirSync(at3,{withFileTypes:!0})}catch{}for(let e of entries){if(found2>1)return;e.name.startsWith(".")||NOISE.has(e.name)||(e.isDirectory()?walk3(path59.join(at3,e.name)):e.isFile()&&(found2+=1))}};return walk3(dir),found2<=1}async function firstLook(dir){let abs=path59.resolve(dir),board6=boardOf(abs),squad=path59.join(abs,SQUAD_DIR),[{count:count3,files},{branch,unsaved}]=await Promise.all([filesOf(abs),gitState(abs)]),code=kindOfCode(abs,files),tests=files.some(f=>TEST_FILE.test(f)),repo=isRepo(abs),nested=workspaceRepos(abs).map(r=>r.path),base2={files:count3,...code,tests,...branch?{branch}:{},unsaved,repos:nested},ask2=addAsk(abs);if(ask2)return{...base2,kind:repo?"repo":count3?"files":"empty",title:ask2.kind==="inside-repo"?"Part of a bigger project":"Inside another squad-kit project",found:ask2.words,next:"Nothing changes until you pick.",safe:"",takes:"",ask:ask2};let unsavedNote=unsaved?` ${plural(unsaved,"change is","changes are")} not saved in git yet: squad-kit works from what is saved.`:"";if(board6){let tickets=fs66.existsSync(path59.join(squad,"tickets"))?fs66.readdirSync(path59.join(squad,"tickets")).filter(f=>!f.startsWith(".")).length:0;return{...base2,kind:"project",title:"A squad-kit project",found:`This folder has squad-kit already, with ${plural(tickets,"ticket")}.`,next:"Adding it opens its board. Nothing is set up again.",safe:"Nothing in the folder changes.",takes:"Opens at once."}}if(board6===null||fs66.existsSync(path59.join(squad,"board","board.yaml"))){let legacy=fs66.existsSync(path59.join(squad,"board","board.yaml")),stories=fs66.existsSync(path59.join(squad,"stories"))?fs66.readdirSync(path59.join(squad,"stories")).filter(f=>!f.startsWith(".")).length:0;return{...base2,kind:"older",title:"An older squad-kit project",found:legacy?`A board from an older squad-kit is here, in ${describeCode(count3,code.language,code.web,tests).replace(/^An? /,"a ")}.`:`squad-kit 0.x files are here${stories?`, with ${plural(stories,"story","stories")}`:""}, in ${describeCode(count3,code.language,code.web,tests).replace(/^An? /,"a ")}.`,next:legacy?"Your specs, plans and decisions are moved to the new layout. Nothing is lost: a full copy is kept first. Then squad-kit reads the code and shows you what it understood.":"Your specs, plans and decisions stay where they are. squad-kit adds its own files beside them, keeps any notes of yours it would write over under a new name, and moves sign-ins out of the project to this computer. Then it reads the code and shows you what it understood.",safe:"Your code is not changed. squad-kit only works in the project's .squad folder.",takes:TAKES}}return nested.length?{...base2,kind:"repos",title:"Several code folders",found:`A folder with ${plural(nested.length,"code folder")} inside: ${listWords(nested)}.`,next:"squad-kit reads each one, finds how to test and run each, then shows you how they fit together.",safe:"Your code is not changed. squad-kit only adds a .squad folder beside it for the project's memory.",takes:TAKES}:count3?repo?{...base2,kind:"repo",title:"Existing code, new to squad-kit",found:`${describeCode(count3,code.language,code.web,tests)}.${unsavedNote}`,next:"squad-kit will read it, find how to test and run it, then show you what it understood.",safe:SAFE,takes:TAKES}:{...base2,kind:"files",title:"Code without git",found:`${describeCode(count3,code.language,code.web,tests)}, not in git yet.`,next:"squad-kit puts the files in git first, in one first commit, then reads them, finds how to test and run it, and shows you what it understood.",safe:"Your files stay as they are. Secrets like .env files and built folders stay out of git.",takes:TAKES}:{...base2,kind:"empty",title:"An empty folder",found:"Nothing here yet: a new project.",next:repo?"Start from an idea: squad-kit asks what you want to build.":"Start from an idea: squad-kit makes a git repo here, and asks what you want to build.",safe:"squad-kit adds a .squad folder for the project's memory.",takes:"Takes a few seconds."}}import fs166 from"fs";import{createServer}from"net";import{isatty}from"tty";import{serve}from"@hono/node-server";import{Hono as Hono2}from"hono";import{cors}from"hono/cors";import{secureHeaders}from"hono/secure-headers";import fs144 from"fs";import os23 from"os";import path135 from"path";import fs67 from"fs";import path60 from"path";import yaml9 from"js-yaml";function settingsFile(){return path60.join(squadHome(),"settings.yaml")}function readSettings(){try{let raw=yaml9.load(fs67.readFileSync(settingsFile(),"utf8"));return raw&&typeof raw=="object"?raw:{}}catch{return{}}}function writeSettings(patch){fs67.mkdirSync(squadHome(),{recursive:!0}),fs67.writeFileSync(settingsFile(),yaml9.dump({...readSettings(),...patch}),"utf8")}function welcomed(){return readSettings().welcomed===!0}function setWelcomed(){writeSettings({welcomed:!0})}var DEFAULT_PARALLEL=2,MAX_PARALLEL=5;function parallelLimit(boardOwn){let mine2=Number(readSettings().parallel),n=Number.isFinite(mine2)&&mine2>=1?mine2:boardOwn??DEFAULT_PARALLEL;return Math.min(MAX_PARALLEL,Math.max(1,Math.round(n)))}function setParallelLimit(n){if(!Number.isInteger(n)||n<1||n>MAX_PARALLEL)throw new Error(`Pick a number from 1 to ${MAX_PARALLEL}.`);return writeSettings({parallel:n}),n}function checkForUpdates(){return readSettings().checkForUpdates!==!1}function setCheckForUpdates(on){return writeSettings({checkForUpdates:on}),on}function paymentChoice(){let p=readSettings().payment;return p==="subscription"||p==="api-key"?p:void 0}function setPaymentChoice(p){let all=readSettings();delete all.payment,p!=="auto"&&(all.payment=p),fs67.mkdirSync(squadHome(),{recursive:!0}),fs67.writeFileSync(settingsFile(),yaml9.dump(all),"utf8")}function computerReady(){let version=gitVersion(),git2=version?gitIsNewEnough(version)?{ok:!0}:{ok:!1,step:"update-git"}:{ok:!1,step:"install-git"},ready=AI_TOOLS.find(t=>t.ready());if(!ready)return{git:git2,tool:{ok:!1,step:"install-tool"}};let signedIn=ready.name!=="claude"||detectClaudeLogin().present||!!process.env.ANTHROPIC_API_KEY?.trim();return{git:git2,tool:signedIn?{ok:!0,label:ready.label}:{ok:!1,label:ready.label,step:"sign-in"}}}function scheduleSquad(ticket,phase,seconds2,speed,index){let ms2=seconds2*1e3/Math.max(speed,.1);ticket.squad={phase,nextAt:new Date(Date.now()+ms2).toISOString(),...index!==void 0?{index}:{}}}function questionWords(n){return`${n} question${n===1?"":"s"}`}import fs139 from"fs";import path130 from"path";import{createHash as createHash12,randomUUID as randomUUID7}from"crypto";import fs122 from"fs";import path113 from"path";import fs68 from"fs";import path61 from"path";var KNOWN=[{name:"TypeScript",role:"Language",icon:"typescript",pkgs:["typescript"]},{name:"Next.js",role:"Framework",icon:"nextdotjs",pkgs:["next"]},{name:"Nuxt",role:"Framework",icon:"nuxt",pkgs:["nuxt"]},{name:"NestJS",role:"Framework",icon:"nestjs",pkgs:["@nestjs/core"]},{name:"Express",role:"Framework",icon:"express",pkgs:["express"]},{name:"Hono",role:"Framework",icon:"hono",pkgs:["hono"]},{name:"React",role:"Interface",icon:"react",pkgs:["react"],comesWith:["Next.js"]},{name:"Vue",role:"Interface",icon:"vuedotjs",pkgs:["vue"],comesWith:["Nuxt"]},{name:"Angular",role:"Interface",icon:"angular",pkgs:["@angular/core"]},{name:"Svelte",role:"Interface",icon:"svelte",pkgs:["svelte"]},{name:"PrimeReact",role:"Interface",icon:"primereact",pkgs:["primereact"]},{name:"Tailwind CSS",role:"Interface",icon:"tailwindcss",pkgs:["tailwindcss"]},{name:"MUI",role:"Interface",icon:"mui",pkgs:["@mui/material"]},{name:"Bootstrap",role:"Interface",icon:"bootstrap",pkgs:["bootstrap","react-bootstrap"]},{name:"Sass",role:"Interface",icon:"sass",pkgs:["sass"]},{name:"React Hook Form",role:"Interface",icon:"reacthookform",pkgs:["react-hook-form"]},{name:"TanStack Query",role:"Interface",icon:"reactquery",pkgs:["@tanstack/react-query"]},{name:"Redux",role:"Interface",icon:"redux",pkgs:["redux","@reduxjs/toolkit"]},{name:"i18next",role:"Interface",icon:"i18next",pkgs:["i18next","react-i18next","next-i18next"]},{name:"Prisma",role:"Data",icon:"prisma",pkgs:["prisma","@prisma/client"]},{name:"Drizzle",role:"Data",icon:"drizzle",pkgs:["drizzle-orm"]},{name:"Zod",role:"Data",icon:"zod",pkgs:["zod"]},{name:"GraphQL",role:"Data",icon:"graphql",pkgs:["graphql"]},{name:"tRPC",role:"Data",icon:"trpc",pkgs:["@trpc/server"]},{name:"PostgreSQL",role:"Data",icon:"postgresql",pkgs:["pg","postgres"]},{name:"MySQL",role:"Data",icon:"mysql",pkgs:["mysql2"]},{name:"MongoDB",role:"Data",icon:"mongodb",pkgs:["mongodb","mongoose"]},{name:"SQLite",role:"Data",icon:"sqlite",pkgs:["better-sqlite3","sqlite3"]},{name:"Redis",role:"Data",icon:"redis",pkgs:["redis","ioredis"]},{name:"Supabase",role:"Services",icon:"supabase",pkgs:["@supabase/supabase-js"]},{name:"Firebase",role:"Services",icon:"firebase",pkgs:["firebase","firebase-admin"]},{name:"Stripe",role:"Services",icon:"stripe",pkgs:["stripe"]},{name:"Twilio",role:"Services",icon:"twilio",pkgs:["twilio"]},{name:"Vitest",role:"Testing",icon:"vitest",pkgs:["vitest"]},{name:"Jest",role:"Testing",icon:"jest",pkgs:["jest"]},{name:"Playwright",role:"Testing",pkgs:["@playwright/test","playwright"]},{name:"Cypress",role:"Testing",icon:"cypress",pkgs:["cypress"]},{name:"Storybook",role:"Testing",icon:"storybook",pkgs:["storybook"]},{name:"ESLint",role:"Tooling",icon:"eslint",pkgs:["eslint"]},{name:"Prettier",role:"Tooling",icon:"prettier",pkgs:["prettier"]},{name:"Vite",role:"Tooling",icon:"vite",pkgs:["vite"]}],PRISMA_DB={sqlite:{name:"SQLite",role:"Data",icon:"sqlite",pkgs:[]},postgresql:{name:"PostgreSQL",role:"Data",icon:"postgresql",pkgs:[]},mysql:{name:"MySQL",role:"Data",icon:"mysql",pkgs:[]},mongodb:{name:"MongoDB",role:"Data",icon:"mongodb",pkgs:[]}};function detectStack(root,meta){let found2=new Map;for(let repo of reposOf(meta)){let dir=path61.join(root,repo.path),deps=readDeps(path61.join(dir,"package.json"));if(!deps)continue;let add2=(k,version)=>{found2.has(k.name)||found2.set(k.name,{name:k.name,role:k.role,...k.icon?{icon:k.icon}:{},...version?{version}:{},repo:repo.path,...k.comesWith?{comesWith:k.comesWith}:{}})};Object.keys(deps).length&&add2({name:"Node.js",role:"Language",icon:"nodedotjs",pkgs:[],comesWith:["Next.js","Nuxt","NestJS","Express","Hono"]});for(let k of KNOWN){let pkg=k.pkgs.find(p=>deps[p]);pkg&&add2(k,major(deps[pkg]))}let db=prismaProvider(dir);db&&PRISMA_DB[db]&&add2(PRISMA_DB[db])}return[...found2.values()]}function decidedIn(tech,decisions){let words2=[tech.name,tech.name.replace(/\.js$/,"")].map(w=>w.toLowerCase());return decisions.filter(d=>!d.replacedBy).filter(d=>{let text3=`${d.title} ${d.chosen}`.toLowerCase();return words2.some(w=>new RegExp(`(^|[^a-z])${w.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}([^a-z]|$)`).test(text3))}).map(d=>d.id)}function readDeps(file3){try{let p=JSON.parse(fs68.readFileSync(file3,"utf8"));return{...p.devDependencies,...p.dependencies}}catch{return null}}function prismaProvider(dir){try{let schema=fs68.readFileSync(path61.join(dir,"prisma","schema.prisma"),"utf8");return/datasource\s+\w+\s*{[^}]*provider\s*=\s*"(\w+)"/.exec(schema)?.[1]??null}catch{return null}}function major(range){return/(\d+)(?:\.\d+)?/.exec(range)?.[0]}var DOING={tests:"Running the tests",check:"Checking the code",copy:"Making a copy of the code. On a big project this takes a while.",setup:"Setting up this story's copy",start:"Starting the app",build:"Building the app",read:"Reading the code",plan:"Reading the plan",shots:"Looking at the screenshots",write:"Writing the code",lookup:"Looking something up",other:"Working\u2026"},CHECK_DOING={test:DOING.tests,lint:DOING.check,build:DOING.build,start:DOING.start},WINS=["tests","setup","start","build","check","read","other"];function simpleCommands(line3){let cut=line3.replace(/<<-?\s*['"]?\w+['"]?[\s\S]*$/,""),out20=[],words2=[],word2="",quote="",endWord=()=>{word2&&words2.push(word2),word2=""},endCommand=()=>{endWord(),words2.length&&out20.push(words2),words2=[]};for(let i=0;i<cut.length;i+=1){let c=cut[i];quote?c===quote?quote="":word2+=c:c==='"'||c==="'"?quote=c:c===";"||c===`
429
- `||c==="|"||c==="("||c===")"?endCommand():c==="&"?cut[i-1]===">"?word2+=c:endCommand():c===" "||c===" "?endWord():word2+=c}return endCommand(),out20}var WRAPPERS=new Set(["time","timeout","gtimeout","nice","nohup","exec","env","sudo","command","do","then","else"]),READERS=new Set(["cat","head","tail","less","more","grep","egrep","fgrep","rg","ag","find","ls","tree","wc","stat","file","pwd","which","diff","jq","awk","sed","cut","sort","uniq","tr","xargs","echo","printf","true","basename","dirname","realpath"]),TEST_TOOLS=new Set(["vitest","jest","mocha","ava","pytest","py.test","rspec","phpunit","tox","nox","karma","wdio"]),CHECK_TOOLS=new Set(["tsc","eslint","prettier","biome","stylelint","ruff","mypy","flake8","black","pylint","pyright","isort","rubocop","golangci-lint","shellcheck","git","gh"]),BUILD_TOOLS2=new Set(["tsup","webpack","rollup","esbuild","parcel","turbo","make","gmake","cmake","ng"]),START_TOOLS=new Set(["nodemon","uvicorn","gunicorn","hypercorn","flask","streamlit"]),INSTALL_WORDS=new Set(["install","i","ci","add","update","up","sync","restore","fetch","upgrade","remove","rm","uninstall","link","rebuild","bootstrap","dedupe"]),INSTALLERS=new Set(["pip","pip3","pipx","poetry","uv","pipenv","bundle","gem","composer","brew","apt","apt-get","conda"]),MIGRATORS=new Set(["prisma","drizzle-kit","knex","sequelize","typeorm","alembic","flyway","liquibase"]);function scriptKind(name){return/test|e2e|spec|cypress|playwright/i.test(name)?"tests":/^(dev|start|serve|preview|watch)\b/i.test(name)?"start":/lint|check|type|format|fmt|prettier/i.test(name)?"check":/build|compile|bundle|dist/i.test(name)?"build":/install|setup|prepare|migrate|seed|bootstrap|generate/i.test(name)?"setup":"other"}function toolKind(tool5,rest){let first2=rest.find(w=>!w.startsWith("-"))??"";return tool5==="playwright"||tool5==="cypress"?first2==="install"?"setup":first2==="test"||first2==="run"?"tests":first2==="open"||first2==="show-report"?"other":"tests":TEST_TOOLS.has(tool5)?"tests":CHECK_TOOLS.has(tool5)?"check":BUILD_TOOLS2.has(tool5)?first2==="test"?"tests":first2==="serve"||first2==="dev"?"start":"build":START_TOOLS.has(tool5)?"start":MIGRATORS.has(tool5)?"setup":INSTALLERS.has(tool5)?INSTALL_WORDS.has(first2)?"setup":first2==="run"||first2==="exec"?commandKind(rest.slice(rest.indexOf(first2)+1)):"other":tool5==="next"||tool5==="vite"||tool5==="nuxt"||tool5==="astro"||tool5==="remix"?first2==="build"?"build":"start":null}function packageKind(tool5,rest){let words2=rest.filter(w=>!w.startsWith("-")),first2=words2[0]??"";return first2?tool5==="npx"||tool5==="pnpx"||tool5==="bunx"?commandKind(words2):INSTALL_WORDS.has(first2)?"setup":first2==="run"||first2==="run-script"?scriptKind(words2[1]??""):first2==="exec"||first2==="dlx"||first2==="x"?commandKind(words2.slice(1)):first2==="t"?"tests":tool5==="npm"&&!["test","start","stop","restart"].includes(first2)?"other":toolKind(first2,words2.slice(1))??scriptKind(first2):tool5==="yarn"?"setup":"other"}function commandKind(all){let words2=all;for(;words2.length;){let w=words2[0];if(/^\w+=/.test(w)||/^\d+[smh]?$/.test(w)||WRAPPERS.has(w)||w.startsWith("-")&&words2!==all)words2=words2.slice(1);else break}let tool5=(words2[0]??"").split("/").pop()??"",rest=words2.slice(1);if(!tool5)return"other";if(tool5==="bash"||tool5==="sh"||tool5==="zsh"){let at3=rest.indexOf("-c");return at3>=0&&rest[at3+1]?plainKind(rest[at3+1]):"other"}if(tool5==="npm"||tool5==="pnpm"||tool5==="yarn"||tool5==="bun"||tool5==="npx"||tool5==="pnpx"||tool5==="bunx")return packageKind(tool5,rest);if(tool5==="sed")return rest.some(r=>/^-i/.test(r))?"other":"read";if(tool5==="cat"&&rest.some(r=>r.startsWith(">")))return"other";if(READERS.has(tool5))return"read";if(/^python[\d.]*$/.test(tool5)){let at3=rest.indexOf("-m");if(at3>=0&&rest[at3+1]){let mod=rest[at3+1];return mod==="unittest"?"tests":mod==="venv"?"setup":toolKind(mod,rest.slice(at3+2))??"other"}if(/(^|\/)manage\.py$/.test(rest[0]??"")){let what=rest[1]??"";return what==="test"?"tests":what==="runserver"?"start":what==="migrate"||what==="makemigrations"?"setup":"other"}return"other"}let known=toolKind(tool5,rest);if(known)return known;if(["go","cargo","dotnet","mvn","mvnw","gradle","gradlew","rails","rake","swift","php","artisan"].includes(tool5)){let first2=rest.find(w=>!w.startsWith("-"))??"";if(/^(test|spec)/.test(first2))return"tests";if(/^(run|serve|server|s|bootRun)$/.test(first2))return"start";if(/^(vet|clippy|check|fmt)$/.test(first2))return"check";if(/^(build|package|compile|publish|assemble)$/.test(first2))return"build";if(/^(mod|restore|fetch|get|migrate)$/.test(first2))return"setup"}return"other"}function plainKind(command){let kinds=simpleCommands(command).filter(w=>w[0]!=="cd"&&w[0]!=="pushd"&&w[0]!=="popd"&&w[0]!=="export").map(commandKind);return WINS.find(k=>kinds.includes(k))??"other"}function plainCommand(command){return DOING[plainKind(command)]}function plainDoing(line3){let reading=/^Reading\s+(.*)$/.exec(line3);return reading?/\.(png|jpe?g|gif|webp)$/i.test(reading[1].trim())?DOING.shots:/(^|\/)\.squad\//.test(reading[1])?DOING.plan:DOING.read:/^(Searching for|Looking for|Listing)\b/.test(line3)?DOING.read:/^(Editing|Writing)\b/.test(line3)?DOING.write:/^Looking up\b/.test(line3)?DOING.lookup:/^Running\s/.test(line3)?plainCommand(line3.replace(/^Running\s+/,"")):DOING.other}function plainToolLine(e,cwd){let line3=describeToolEvent(e,cwd);return line3?typeof e.input.command=="string"&&line3.startsWith("Running ")?plainCommand(e.input.command):plainDoing(line3):null}import fs90 from"fs";import path85 from"path";import fs76 from"fs";import net2 from"net";import os12 from"os";import path70 from"path";import fs69 from"fs";import path62 from"path";function testKey(t){return`${t.pkg??t.file}::${t.name??""}`}function testTitle(t){let said=(t.name??t.file).replace(/\s+/g," ").trim();return said.length>140?`${said.slice(0,139)}\u2026`:said}var PLAYWRIGHT_LINE=/^(?:\[(.+?)\] › )?(.+?):(\d+):(\d+) › (.+?)[\s─]*$/;function playwrightFailed(lines3){let out20=[];for(let i=0;i<lines3.length;i+=1){let head=lines3[i].match(/^ {2}(\d+) (failed|interrupted|did not run)\s*$/);if(!head)continue;if(head[2]!=="failed")return null;let listed3=[];for(let j=i+1;j<lines3.length&&/^ {4}\S/.test(lines3[j]);j+=1){let m=lines3[j].trim().match(PLAYWRIGHT_LINE);if(!m)return null;listed3.push({tool:"playwright",file:m[2],line:Number(m[3]),name:m[5],...m[1]?{project:m[1]}:{}})}if(listed3.length!==Number(head[1]))return null;out20.push(...listed3)}return lines3.some(l=>/^ {2}\d+ errors? (was|were) not a part of any test/.test(l))?null:out20}function vitestFailed(lines3){if(!lines3.some(l=>/^\s*Test Files\s+.*\(\d+\)\s*$/.test(l)))return[];if(lines3.some(l=>/Unhandled Errors?\s*⎯|unhandled errors? during the test run/i.test(l)))return null;let tests=new Map,suites=new Map;for(let line3 of lines3){let m=line3.match(/^\s*FAIL\s+(?:\|([^|]+)\|\s+)?(\S+)\s+(?:>\s+(.+?)|\[ .+ \])\s*$/);if(!m)continue;let t={tool:"vitest",file:m[2],...m[3]?{name:m[3]}:{},...m[1]?{project:m[1]}:{}};(m[3]?tests:suites).set(`${m[1]??""}|${testKey(t)}`,t)}let count3=re=>lines3.map(l=>l.match(re)).find(Boolean)?.[1],failed3=count3(/^\s*Tests\s+(\d+)\s+failed\b/),broken=count3(/Failed Suites (\d+)\s*⎯/);return Number(failed3??0)!==tests.size||Number(broken??0)!==suites.size?null:[...suites.values(),...tests.values()]}function jestFailed(lines3){if(!lines3.some(l=>/^Test Suites:\s/.test(l)))return[];let tests=new Map,suites=new Map,file3=null;for(let line3 of lines3){let head=line3.match(/^\s*(FAIL|PASS)\s+(.+?)(?:\s+\([\d.]+\s*m?s\))?\s*$/);if(head){let words2=head[2].split(/\s+/);file3=head[1]==="FAIL"&&!/ > |\[ /.test(head[2])?words2[words2.length-1]:null;continue}let m=file3?line3.match(/^ {2}● (.+?)\s*$/):null;m&&(m[1]==="Test suite failed to run"?suites.set(file3,{tool:"jest",file:file3}):tests.set(`${file3}::${m[1]}`,{tool:"jest",file:file3,name:m[1]}))}let failed3=lines3.map(l=>l.match(/^Tests:\s+(\d+) failed\b/)).find(Boolean)?.[1];return Number(failed3??0)!==tests.size?null:[...suites.values(),...tests.values()]}function pytestFailed(lines3){let last=[...lines3].reverse().find(l=>/^=*\s*\d+ \w+.*\bin [\d.]+s\b/.test(l)&&/\b\d+ (passed|failed|errors?|skipped|deselected|xfailed|xpassed)\b/.test(l));if(!last)return[];if(lines3.some(l=>/^!+ .*(Interrupted|stopping after)/.test(l)))return null;let out20=new Map,failedLines=0,errorLines=0;for(let line3 of lines3){let m=line3.match(/^(FAILED|ERROR) (.+?)(?: - .*)?$/);if(!m)continue;m[1]==="FAILED"?failedLines+=1:errorLines+=1;let[file3,...rest]=m[2].trim().split("::"),t={tool:"pytest",file:file3,...rest.length?{name:rest.join("::")}:{}};out20.set(testKey(t),t)}let n=word2=>Number(last.match(word2)?.[1]??0);return n(/(\d+) failed/)!==failedLines||n(/(\d+) errors?\b/)!==errorLines?null:[...out20.values()]}function goFailed(lines3){let out20=[],pending=[],current=null;for(let line3 of lines3){let top=line3.match(/^--- FAIL: (\S+) \(/);if(top){current={name:top[1],file:""},pending.push(current);continue}let at3=current&&!current.file?line3.match(/^\s+(\S+_test\.go):\d+:/):null;if(at3&&(current.file=at3[1]),/^FAIL\s+\S+\s+\[(build|setup) failed\]/.test(line3))return null;let pkg=line3.match(/^FAIL\t(\S+)\t/)??line3.match(/^FAIL\s+(\S+)\s+[\d.]+s\s*$/);if(pkg){if(!pending.length)return null;out20.push(...pending.map(p=>({tool:"go",file:p.file,name:p.name,pkg:pkg[1]}))),pending=[],current=null}}return pending.length?null:out20}function failedTests(output){let lines3=output.replace(/\x1b\[[0-9;]*m/g,"").replace(/\r/g,"").split(`
429
+ `).filter(f=>f&&!f.startsWith(`${SQUAD_DIR}/`));if(list6.length)return list6.length<=1}let found2=0,walk3=at3=>{let entries=[];try{entries=fs66.readdirSync(at3,{withFileTypes:!0})}catch{}for(let e of entries){if(found2>1)return;e.name.startsWith(".")||NOISE.has(e.name)||(e.isDirectory()?walk3(path59.join(at3,e.name)):e.isFile()&&(found2+=1))}};return walk3(dir),found2<=1}async function firstLook(dir){let abs=path59.resolve(dir),board6=boardOf(abs),squad=path59.join(abs,SQUAD_DIR),[{count:count3,files},{branch,unsaved}]=await Promise.all([filesOf(abs),gitState(abs)]),code=kindOfCode(abs,files),tests=files.some(f=>TEST_FILE.test(f)),repo=isRepo(abs),nested=workspaceRepos(abs).map(r=>r.path),base2={files:count3,...code,tests,...branch?{branch}:{},unsaved,repos:nested},ask2=addAsk(abs);if(ask2)return{...base2,kind:repo?"repo":count3?"files":"empty",title:ask2.kind==="inside-repo"?"Part of a bigger project":"Inside another squad-kit project",found:ask2.words,next:"Nothing changes until you pick.",safe:"",takes:"",ask:ask2};let unsavedNote=unsaved?` ${plural(unsaved,"change is","changes are")} not saved in git yet: squad-kit works from what is saved.`:"";if(board6){let tickets=fs66.existsSync(path59.join(squad,"tickets"))?fs66.readdirSync(path59.join(squad,"tickets")).filter(f=>!f.startsWith(".")).length:0;return{...base2,kind:"project",title:"A squad-kit project",found:`This folder has squad-kit already, with ${plural(tickets,"ticket")}.`,next:"Adding it opens its board. Nothing is set up again.",safe:"Nothing in the folder changes.",takes:"Opens at once."}}if(board6===null||fs66.existsSync(path59.join(squad,"board","board.yaml"))){let legacy=fs66.existsSync(path59.join(squad,"board","board.yaml")),stories=fs66.existsSync(path59.join(squad,"stories"))?fs66.readdirSync(path59.join(squad,"stories")).filter(f=>!f.startsWith(".")).length:0;return{...base2,kind:"older",title:"An older squad-kit project",found:legacy?`A board from an older squad-kit is here, in ${describeCode(count3,code.language,code.web,tests).replace(/^An? /,"a ")}.`:`squad-kit 0.x files are here${stories?`, with ${plural(stories,"story","stories")}`:""}, in ${describeCode(count3,code.language,code.web,tests).replace(/^An? /,"a ")}.`,next:legacy?"Your specs, plans and decisions are moved to the new layout. Nothing is lost: a full copy is kept first. Then squad-kit reads the code and shows you what it understood.":"Your specs, plans and decisions stay where they are. squad-kit adds its own files beside them, keeps any notes of yours it would write over under a new name, and moves sign-ins out of the project to this computer. Then it reads the code and shows you what it understood.",safe:"Your code is not changed. squad-kit only works in the project's .squad folder.",takes:TAKES}}return nested.length?{...base2,kind:"repos",title:"Several code folders",found:`A folder with ${plural(nested.length,"code folder")} inside: ${listWords(nested)}.`,next:"squad-kit reads each one, finds how to test and run each, then shows you how they fit together.",safe:"Your code is not changed. squad-kit only adds a .squad folder beside it for the project's memory.",takes:TAKES}:count3?repo?{...base2,kind:"repo",title:"Existing code, new to squad-kit",found:`${describeCode(count3,code.language,code.web,tests)}.${unsavedNote}`,next:"squad-kit will read it, find how to test and run it, then show you what it understood.",safe:SAFE,takes:TAKES}:{...base2,kind:"files",title:"Code without git",found:`${describeCode(count3,code.language,code.web,tests)}, not in git yet.`,next:"squad-kit puts the files in git first, in one first commit, then reads them, finds how to test and run it, and shows you what it understood.",safe:"Your files stay as they are. Secrets like .env files and built folders stay out of git.",takes:TAKES}:{...base2,kind:"empty",title:"An empty folder",found:"Nothing here yet: a new project.",next:repo?"Start from an idea: squad-kit asks what you want to build.":"Start from an idea: squad-kit makes a git repo here, and asks what you want to build.",safe:"squad-kit adds a .squad folder for the project's memory.",takes:"Takes a few seconds."}}import fs166 from"fs";import{createServer}from"net";import{isatty}from"tty";import{serve}from"@hono/node-server";import{Hono as Hono2}from"hono";import{cors}from"hono/cors";import{secureHeaders}from"hono/secure-headers";import fs144 from"fs";import os24 from"os";import path135 from"path";import fs67 from"fs";import path60 from"path";import yaml9 from"js-yaml";function settingsFile(){return path60.join(squadHome(),"settings.yaml")}function readSettings(){try{let raw=yaml9.load(fs67.readFileSync(settingsFile(),"utf8"));return raw&&typeof raw=="object"?raw:{}}catch{return{}}}function writeSettings(patch){fs67.mkdirSync(squadHome(),{recursive:!0}),fs67.writeFileSync(settingsFile(),yaml9.dump({...readSettings(),...patch}),"utf8")}function welcomed(){return readSettings().welcomed===!0}function setWelcomed(){writeSettings({welcomed:!0})}var DEFAULT_PARALLEL=2,MAX_PARALLEL=5;function parallelLimit(boardOwn){let mine2=Number(readSettings().parallel),n=Number.isFinite(mine2)&&mine2>=1?mine2:boardOwn??DEFAULT_PARALLEL;return Math.min(MAX_PARALLEL,Math.max(1,Math.round(n)))}function setParallelLimit(n){if(!Number.isInteger(n)||n<1||n>MAX_PARALLEL)throw new Error(`Pick a number from 1 to ${MAX_PARALLEL}.`);return writeSettings({parallel:n}),n}function checkForUpdates(){return readSettings().checkForUpdates!==!1}function setCheckForUpdates(on){return writeSettings({checkForUpdates:on}),on}function paymentChoice(){let p=readSettings().payment;return p==="subscription"||p==="api-key"?p:void 0}function setPaymentChoice(p){let all=readSettings();delete all.payment,p!=="auto"&&(all.payment=p),fs67.mkdirSync(squadHome(),{recursive:!0}),fs67.writeFileSync(settingsFile(),yaml9.dump(all),"utf8")}function computerReady(){let version=gitVersion(),git2=version?gitIsNewEnough(version)?{ok:!0}:{ok:!1,step:"update-git"}:{ok:!1,step:"install-git"},ready=AI_TOOLS.find(t=>t.ready());if(!ready)return{git:git2,tool:{ok:!1,step:"install-tool"}};let signedIn=ready.name!=="claude"||detectClaudeLogin().present||!!process.env.ANTHROPIC_API_KEY?.trim();return{git:git2,tool:signedIn?{ok:!0,label:ready.label}:{ok:!1,label:ready.label,step:"sign-in"}}}function scheduleSquad(ticket,phase,seconds2,speed,index){let ms2=seconds2*1e3/Math.max(speed,.1);ticket.squad={phase,nextAt:new Date(Date.now()+ms2).toISOString(),...index!==void 0?{index}:{}}}function questionWords(n){return`${n} question${n===1?"":"s"}`}import fs139 from"fs";import path130 from"path";import{createHash as createHash12,randomUUID as randomUUID7}from"crypto";import fs122 from"fs";import path113 from"path";import fs68 from"fs";import path61 from"path";var KNOWN=[{name:"TypeScript",role:"Language",icon:"typescript",pkgs:["typescript"]},{name:"Next.js",role:"Framework",icon:"nextdotjs",pkgs:["next"]},{name:"Nuxt",role:"Framework",icon:"nuxt",pkgs:["nuxt"]},{name:"NestJS",role:"Framework",icon:"nestjs",pkgs:["@nestjs/core"]},{name:"Express",role:"Framework",icon:"express",pkgs:["express"]},{name:"Hono",role:"Framework",icon:"hono",pkgs:["hono"]},{name:"React",role:"Interface",icon:"react",pkgs:["react"],comesWith:["Next.js"]},{name:"Vue",role:"Interface",icon:"vuedotjs",pkgs:["vue"],comesWith:["Nuxt"]},{name:"Angular",role:"Interface",icon:"angular",pkgs:["@angular/core"]},{name:"Svelte",role:"Interface",icon:"svelte",pkgs:["svelte"]},{name:"PrimeReact",role:"Interface",icon:"primereact",pkgs:["primereact"]},{name:"Tailwind CSS",role:"Interface",icon:"tailwindcss",pkgs:["tailwindcss"]},{name:"MUI",role:"Interface",icon:"mui",pkgs:["@mui/material"]},{name:"Bootstrap",role:"Interface",icon:"bootstrap",pkgs:["bootstrap","react-bootstrap"]},{name:"Sass",role:"Interface",icon:"sass",pkgs:["sass"]},{name:"React Hook Form",role:"Interface",icon:"reacthookform",pkgs:["react-hook-form"]},{name:"TanStack Query",role:"Interface",icon:"reactquery",pkgs:["@tanstack/react-query"]},{name:"Redux",role:"Interface",icon:"redux",pkgs:["redux","@reduxjs/toolkit"]},{name:"i18next",role:"Interface",icon:"i18next",pkgs:["i18next","react-i18next","next-i18next"]},{name:"Prisma",role:"Data",icon:"prisma",pkgs:["prisma","@prisma/client"]},{name:"Drizzle",role:"Data",icon:"drizzle",pkgs:["drizzle-orm"]},{name:"Zod",role:"Data",icon:"zod",pkgs:["zod"]},{name:"GraphQL",role:"Data",icon:"graphql",pkgs:["graphql"]},{name:"tRPC",role:"Data",icon:"trpc",pkgs:["@trpc/server"]},{name:"PostgreSQL",role:"Data",icon:"postgresql",pkgs:["pg","postgres"]},{name:"MySQL",role:"Data",icon:"mysql",pkgs:["mysql2"]},{name:"MongoDB",role:"Data",icon:"mongodb",pkgs:["mongodb","mongoose"]},{name:"SQLite",role:"Data",icon:"sqlite",pkgs:["better-sqlite3","sqlite3"]},{name:"Redis",role:"Data",icon:"redis",pkgs:["redis","ioredis"]},{name:"Supabase",role:"Services",icon:"supabase",pkgs:["@supabase/supabase-js"]},{name:"Firebase",role:"Services",icon:"firebase",pkgs:["firebase","firebase-admin"]},{name:"Stripe",role:"Services",icon:"stripe",pkgs:["stripe"]},{name:"Twilio",role:"Services",icon:"twilio",pkgs:["twilio"]},{name:"Vitest",role:"Testing",icon:"vitest",pkgs:["vitest"]},{name:"Jest",role:"Testing",icon:"jest",pkgs:["jest"]},{name:"Playwright",role:"Testing",pkgs:["@playwright/test","playwright"]},{name:"Cypress",role:"Testing",icon:"cypress",pkgs:["cypress"]},{name:"Storybook",role:"Testing",icon:"storybook",pkgs:["storybook"]},{name:"ESLint",role:"Tooling",icon:"eslint",pkgs:["eslint"]},{name:"Prettier",role:"Tooling",icon:"prettier",pkgs:["prettier"]},{name:"Vite",role:"Tooling",icon:"vite",pkgs:["vite"]}],PRISMA_DB={sqlite:{name:"SQLite",role:"Data",icon:"sqlite",pkgs:[]},postgresql:{name:"PostgreSQL",role:"Data",icon:"postgresql",pkgs:[]},mysql:{name:"MySQL",role:"Data",icon:"mysql",pkgs:[]},mongodb:{name:"MongoDB",role:"Data",icon:"mongodb",pkgs:[]}};function detectStack(root,meta){let found2=new Map;for(let repo of reposOf(meta)){let dir=path61.join(root,repo.path),deps=readDeps(path61.join(dir,"package.json"));if(!deps)continue;let add2=(k,version)=>{found2.has(k.name)||found2.set(k.name,{name:k.name,role:k.role,...k.icon?{icon:k.icon}:{},...version?{version}:{},repo:repo.path,...k.comesWith?{comesWith:k.comesWith}:{}})};Object.keys(deps).length&&add2({name:"Node.js",role:"Language",icon:"nodedotjs",pkgs:[],comesWith:["Next.js","Nuxt","NestJS","Express","Hono"]});for(let k of KNOWN){let pkg=k.pkgs.find(p=>deps[p]);pkg&&add2(k,major(deps[pkg]))}let db=prismaProvider(dir);db&&PRISMA_DB[db]&&add2(PRISMA_DB[db])}return[...found2.values()]}function decidedIn(tech,decisions){let words2=[tech.name,tech.name.replace(/\.js$/,"")].map(w=>w.toLowerCase());return decisions.filter(d=>!d.replacedBy).filter(d=>{let text3=`${d.title} ${d.chosen}`.toLowerCase();return words2.some(w=>new RegExp(`(^|[^a-z])${w.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}([^a-z]|$)`).test(text3))}).map(d=>d.id)}function readDeps(file3){try{let p=JSON.parse(fs68.readFileSync(file3,"utf8"));return{...p.devDependencies,...p.dependencies}}catch{return null}}function prismaProvider(dir){try{let schema=fs68.readFileSync(path61.join(dir,"prisma","schema.prisma"),"utf8");return/datasource\s+\w+\s*{[^}]*provider\s*=\s*"(\w+)"/.exec(schema)?.[1]??null}catch{return null}}function major(range){return/(\d+)(?:\.\d+)?/.exec(range)?.[0]}import fs90 from"fs";import path85 from"path";import fs76 from"fs";import net2 from"net";import os12 from"os";import path70 from"path";import fs69 from"fs";import path62 from"path";function testKey(t){return`${t.pkg??t.file}::${t.name??""}`}function testTitle(t){let said=(t.name??t.file).replace(/\s+/g," ").trim();return said.length>140?`${said.slice(0,139)}\u2026`:said}var PLAYWRIGHT_LINE=/^(?:\[(.+?)\] › )?(.+?):(\d+):(\d+) › (.+?)[\s─]*$/;function playwrightFailed(lines3){let out20=[];for(let i=0;i<lines3.length;i+=1){let head=lines3[i].match(/^ {2}(\d+) (failed|interrupted|did not run)\s*$/);if(!head)continue;if(head[2]!=="failed")return null;let listed3=[];for(let j=i+1;j<lines3.length&&/^ {4}\S/.test(lines3[j]);j+=1){let m=lines3[j].trim().match(PLAYWRIGHT_LINE);if(!m)return null;listed3.push({tool:"playwright",file:m[2],line:Number(m[3]),name:m[5],...m[1]?{project:m[1]}:{}})}if(listed3.length!==Number(head[1]))return null;out20.push(...listed3)}return lines3.some(l=>/^ {2}\d+ errors? (was|were) not a part of any test/.test(l))?null:out20}function vitestFailed(lines3){if(!lines3.some(l=>/^\s*Test Files\s+.*\(\d+\)\s*$/.test(l)))return[];if(lines3.some(l=>/Unhandled Errors?\s*⎯|unhandled errors? during the test run/i.test(l)))return null;let tests=new Map,suites=new Map;for(let line3 of lines3){let m=line3.match(/^\s*FAIL\s+(?:\|([^|]+)\|\s+)?(\S+)\s+(?:>\s+(.+?)|\[ .+ \])\s*$/);if(!m)continue;let t={tool:"vitest",file:m[2],...m[3]?{name:m[3]}:{},...m[1]?{project:m[1]}:{}};(m[3]?tests:suites).set(`${m[1]??""}|${testKey(t)}`,t)}let count3=re=>lines3.map(l=>l.match(re)).find(Boolean)?.[1],failed3=count3(/^\s*Tests\s+(\d+)\s+failed\b/),broken=count3(/Failed Suites (\d+)\s*⎯/);return Number(failed3??0)!==tests.size||Number(broken??0)!==suites.size?null:[...suites.values(),...tests.values()]}function jestFailed(lines3){if(!lines3.some(l=>/^Test Suites:\s/.test(l)))return[];let tests=new Map,suites=new Map,file3=null;for(let line3 of lines3){let head=line3.match(/^\s*(FAIL|PASS)\s+(.+?)(?:\s+\([\d.]+\s*m?s\))?\s*$/);if(head){let words2=head[2].split(/\s+/);file3=head[1]==="FAIL"&&!/ > |\[ /.test(head[2])?words2[words2.length-1]:null;continue}let m=file3?line3.match(/^ {2}● (.+?)\s*$/):null;m&&(m[1]==="Test suite failed to run"?suites.set(file3,{tool:"jest",file:file3}):tests.set(`${file3}::${m[1]}`,{tool:"jest",file:file3,name:m[1]}))}let failed3=lines3.map(l=>l.match(/^Tests:\s+(\d+) failed\b/)).find(Boolean)?.[1];return Number(failed3??0)!==tests.size?null:[...suites.values(),...tests.values()]}function pytestFailed(lines3){let last=[...lines3].reverse().find(l=>/^=*\s*\d+ \w+.*\bin [\d.]+s\b/.test(l)&&/\b\d+ (passed|failed|errors?|skipped|deselected|xfailed|xpassed)\b/.test(l));if(!last)return[];if(lines3.some(l=>/^!+ .*(Interrupted|stopping after)/.test(l)))return null;let out20=new Map,failedLines=0,errorLines=0;for(let line3 of lines3){let m=line3.match(/^(FAILED|ERROR) (.+?)(?: - .*)?$/);if(!m)continue;m[1]==="FAILED"?failedLines+=1:errorLines+=1;let[file3,...rest]=m[2].trim().split("::"),t={tool:"pytest",file:file3,...rest.length?{name:rest.join("::")}:{}};out20.set(testKey(t),t)}let n=word2=>Number(last.match(word2)?.[1]??0);return n(/(\d+) failed/)!==failedLines||n(/(\d+) errors?\b/)!==errorLines?null:[...out20.values()]}function goFailed(lines3){let out20=[],pending=[],current=null;for(let line3 of lines3){let top=line3.match(/^--- FAIL: (\S+) \(/);if(top){current={name:top[1],file:""},pending.push(current);continue}let at3=current&&!current.file?line3.match(/^\s+(\S+_test\.go):\d+:/):null;if(at3&&(current.file=at3[1]),/^FAIL\s+\S+\s+\[(build|setup) failed\]/.test(line3))return null;let pkg=line3.match(/^FAIL\t(\S+)\t/)??line3.match(/^FAIL\s+(\S+)\s+[\d.]+s\s*$/);if(pkg){if(!pending.length)return null;out20.push(...pending.map(p=>({tool:"go",file:p.file,name:p.name,pkg:pkg[1]}))),pending=[],current=null}}return pending.length?null:out20}function failedTests(output){let lines3=output.replace(/\x1b\[[0-9;]*m/g,"").replace(/\r/g,"").split(`
430
430
  `),found2=[];for(let read7 of[playwrightFailed,vitestFailed,jestFailed,pytestFailed,goFailed]){let some=read7(lines3);if(some===null)return[];found2.push(...some)}return found2}function quoted3(word2){return`'${word2.replace(/'/g,"'\\''")}'`}function exactly2(text3){return text3.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function wordsOf(text3){let out20=[],word2=null;for(let i=0;i<text3.length;i+=1){let ch=text3[i];if(/\s/.test(ch))word2!==null&&out20.push(word2),word2=null;else if(ch==="'"){let end=text3.indexOf("'",i+1);if(end<0)return null;word2=(word2??"")+text3.slice(i+1,end),i=end}else if(ch==='"'){let j=i+1,inside5="";for(;j<text3.length&&text3[j]!=='"';j+=1){if("$`".includes(text3[j]))return null;text3[j]==="\\"&&j+1<text3.length&&(j+=1),inside5+=text3[j]}if(j>=text3.length)return null;word2=(word2??"")+inside5,i=j}else if(ch==="\\"){if(i+1>=text3.length)return null;i+=1,word2=(word2??"")+text3[i]}else{if("$`|;&<>(){}#\n".includes(ch))return null;word2=(word2??"")+ch}}return word2!==null&&out20.push(word2),out20}function splitAnd(text3){let parts2=[],quote="",start=0;for(let i=0;i<text3.length;i+=1){let ch=text3[i];quote?ch===quote?quote="":ch==="\\"&&quote==='"'&&(i+=1):ch==="'"||ch==='"'?quote=ch:ch==="\\"?i+=1:ch==="&"&&text3[i+1]==="&"&&(parts2.push(text3.slice(start,i)),start=i+2,i+=1)}return parts2.push(text3.slice(start)),parts2.map(p=>p.trim())}function scriptsIn(dir){try{return JSON.parse(fs69.readFileSync(path62.join(dir,"package.json"),"utf8")).scripts??{}}catch{return null}}var TOOL_BINS=new Set(["playwright","vitest","jest","pytest","py.test"]),MAY_RUN_ANYTHING=new Set(["npm","pnpm","yarn","bun","bunx","npx","cd","pushd","sh","bash","zsh","make","turbo","nx","lerna","env","exec","eval","xargs","concurrently","npm-run-all","run-s","run-p","dotenv","env-cmd","cross-env-shell","poetry","uv","pipenv","tox","nox"]);function scriptOf(words2,scripts){let[tool5,...rest]=words2;if(!["npm","pnpm","yarn","bun"].includes(tool5))return;let i=0;for(;rest[i]?.startsWith("-");)i+=1;let name=rest[i];if(!name)return null;if(name==="exec"||name==="dlx"||name==="x")return;if(name==="run"||name==="run-script"){for(i+=1;rest[i]?.startsWith("-");)i+=1;name=rest[i]}else if(tool5==="npm"&&name==="t")name="test";else{if(tool5==="npm"&&!["test","start"].includes(name))return null;if(tool5==="bun"&&name==="test")return null}if(!name||!scripts)return null;let text3=scripts[name];if(text3===void 0)return tool5!=="npm"&&TOOL_BINS.has(name)?[rest.slice(i).map(quoted3).join(" ")]:null;let extra=rest.slice(i+1).filter((w,k)=>!(k===0&&w==="--"));return[scripts[`pre${name}`],`${text3}${extra.length?` ${extra.map(quoted3).join(" ")}`:""}`,scripts[`post${name}`]].filter(s=>!!s?.trim())}function chainOf(dir,command,scripts=scriptsIn(dir),depth=0){if(depth>5)return null;let out20=[];for(let part of splitAnd(command)){let words2=wordsOf(part);if(!words2?.length)return null;let script=scriptOf(words2,scripts);if(script===null)return null;if(script===void 0){out20.push(words2);continue}for(let text3 of script){let inner=chainOf(dir,text3,scripts,depth+1);if(!inner)return null;out20.push(...inner)}}return out20}var CARRY={playwright:{value:["-c","--config","--retries"],flag:[]},vitest:{value:["-c","--config","-r","--root","--dir","--mode","--environment"],flag:[]},jest:{value:["-c","--config","--rootDir","--selectProjects","--env"],flag:["-i","--runInBand"]},pytest:{value:["-c","--rootdir","-p","--ds","--dc","-o","--confcutdir"],flag:[]},go:{value:["-tags"],flag:["-race"]}};function toolRunOf(words0){let words2=[...words0],env2=[],takeEnv=()=>{for(;words2[0]&&/^[A-Za-z_]\w*=/.test(words2[0]);){let[name,...value2]=words2.shift().split("=");env2.push(`${name}=${quoted3(value2.join("="))}`)}};if(takeEnv(),words2[0]==="cross-env"&&(words2.shift(),takeEnv()),words2[0]==="npx"||words2[0]==="bunx")for(words2.shift();words2[0]?.startsWith("-");)words2.shift();else(words2[0]==="pnpm"||words2[0]==="yarn"||words2[0]==="npm")&&words2[1]==="exec"?(words2=words2.slice(2),words2[0]==="--"&&words2.shift()):(words2[0]==="pnpm"||words2[0]==="yarn")&&TOOL_BINS.has(words2[1]??"")&&words2.shift();let program2=path62.basename(words2[0]??""),tool5=null,args=[];if(program2==="playwright"&&words2[1]==="test"?[tool5,args]=["playwright",words2.slice(2)]:program2==="vitest"&&(!words2[1]||words2[1]==="run"||words2[1].startsWith("-"))?[tool5,args]=["vitest",words2.slice(words2[1]==="run"?2:1)]:program2==="jest"?[tool5,args]=["jest",words2.slice(1)]:program2==="pytest"||program2==="py.test"?[tool5,args]=["pytest",words2.slice(1)]:/^python(\d+(\.\d+)?)?$/.test(program2)&&words2[1]==="-m"&&words2[2]==="pytest"?[tool5,args]=["pytest",words2.slice(3)]:program2==="go"&&words2[1]==="test"&&([tool5,args]=["go",words2.slice(2)]),!tool5&&(program2==="playwright"&&words2[1]==="install"||program2==="go"&&["vet","build","fmt"].includes(words2[1]??"")))return"other";if(!tool5)return MAY_RUN_ANYTHING.has(program2)||TOOL_BINS.has(program2)||program2==="go"?null:"other";let carry=[],projects=[],{value,flag}=CARRY[tool5];for(let i=0;i<args.length;i+=1){let[name,given]=args[i].includes("=")?[args[i].slice(0,args[i].indexOf("=")),args[i].slice(args[i].indexOf("=")+1)]:[args[i],void 0],isProject=name==="--project"&&(tool5==="playwright"||tool5==="vitest");if(flag.includes(name))carry.push(name);else if(value.includes(name)||isProject){let v=given??args[i+=1];if(v===void 0)return null;isProject?projects.push(v):carry.push(tool5==="go"||name.startsWith("--")?`${name}=${quoted3(v)}`:`${name} ${quoted3(v)}`)}}return{tool:tool5,env:env2,carry,projects}}function toolRuns(dir,command){let chain=chainOf(dir,command);if(!chain?.length)return null;let runs2=[];for(let words2 of chain){let run7=toolRunOf(words2);if(run7===null)return null;runs2.push(run7)}return runs2}function installed(dir,tool5){if(tool5==="go")return fs69.existsSync(path62.join(dir,"go.mod"))?{}:null;if(tool5==="pytest"){for(let venv of[".venv","venv","env"])if(fs69.existsSync(path62.join(dir,venv,"bin","pytest"))&&fs69.existsSync(path62.join(dir,venv,"bin","python")))return{python:`${venv}/bin/python`};return null}return fs69.existsSync(path62.join(dir,"node_modules",".bin",tool5))?{}:null}function goModule(dir){try{return fs69.readFileSync(path62.join(dir,"go.mod"),"utf8").match(/^module\s+(\S+)/m)?.[1]??null}catch{return null}}function goFolder(dir,pkg){let mod=goModule(dir);return mod?pkg===mod?".":pkg.startsWith(`${mod}/`)?`./${pkg.slice(mod.length+1)}`:null:null}function commandFor(dir,run7,t){let has4=installed(dir,run7.tool);if(!has4)return null;let env2=run7.env.length?`${run7.env.join(" ")} `:"",carry=run7.carry.length?` ${run7.carry.join(" ")}`:"",projects=(t.project?[t.project]:run7.projects).map(p=>` --project=${quoted3(p)}`).join("");switch(run7.tool){case"playwright":return`${env2}npx playwright test${carry}${projects} ${quoted3(`${exactly2(t.file)}${t.line?`:${t.line}`:""}`)}`;case"vitest":return`${env2}npx vitest run${carry}${projects} ${quoted3(t.file)}${t.name?` -t ${quoted3(`${exactly2(t.name.split(" > ").join(" "))}$`)}`:""}`;case"jest":return`${env2}npx jest${carry} ${quoted3(exactly2(t.file))}${t.name?` -t ${quoted3(`^${exactly2(t.name.split(" \u203A ").join(" "))}$`)}`:""}`;case"pytest":return`${env2}${has4.python} -m pytest${carry} ${quoted3(t.name?`${t.file}::${t.name}`:t.file)}`;case"go":{let folder=t.pkg?goFolder(dir,t.pkg):null;return!folder||!t.name?null:`${env2}go test${carry} -count=1 -run ${quoted3(`^${exactly2(t.name.split("/")[0])}$`)} ${quoted3(folder)}`}}}function testAlone(dir,testCommand,t){let runs2=toolRuns(dir,testCommand),last=runs2?.[runs2.length-1];return!runs2||!last||last==="other"||last.tool!==t.tool||runs2.slice(0,-1).some(r=>r!=="other"&&r.tool===t.tool)||t.file.startsWith("-")||t.name?.startsWith("-")?null:commandFor(dir,last,t)}function playwrightFolder(dir,run7){let said=run7.carry.map(c=>c.match(/^(?:-c |--config=)'(.*)'$/)?.[1]).find(Boolean),files=said?[said]:["playwright.config.ts","playwright.config.js","playwright.config.mjs","playwright.config.cjs"];for(let f of files)try{let folder=fs69.readFileSync(path62.join(dir,f),"utf8").match(/testDir\s*:\s*['"`]([^'"`$]+)['"`]/)?.[1];return folder?path62.normalize(path62.join(path62.dirname(f),folder)):null}catch{}return null}function fileAlone(dir,testCommand,asked){let m=asked.trim().match(/^(.+?)(?::(\d+))?$/);if(!m)return null;let file3=path62.normalize(m[1]).replace(/^\.\//,"");if(!file3||file3.startsWith("-")||file3.startsWith("..")||path62.isAbsolute(file3)||!fs69.existsSync(path62.join(dir,file3)))return null;let runs2=(toolRuns(dir,testCommand)??[]).filter(r=>r!=="other"&&r.tool!=="go"),kinds=[...new Set(runs2.map(r=>r.tool))],tool5=kinds.length===1?kinds[0]:void 0;if(kinds.length===2&&kinds.includes("playwright")){let folder=playwrightFolder(dir,runs2.find(r=>r.tool==="playwright"));folder&&folder!=="."&&(tool5=file3===folder||file3.startsWith(`${folder}${path62.sep}`)?"playwright":kinds.find(k=>k!=="playwright"))}let run7=runs2.find(r=>r.tool===tool5);if(!run7)return null;let command=commandFor(dir,run7,{file:file3,...m[2]&&run7.tool==="playwright"?{line:Number(m[2])}:{}});return command?{command,tool:run7.tool}:null}function inChanged(t,changed,dir){if(t.tool==="go"){let folder=t.pkg?goFolder(dir,t.pkg):null;if(!folder)return!0;let rel=folder==="."?"":`${folder.slice(2)}/`;return changed.some(f=>f.startsWith(rel)&&!f.slice(rel.length).includes("/")&&f.endsWith("_test.go"))}let file3=t.file.replace(/^\.\//,"");return changed.some(f=>f===file3||f.endsWith(`/${file3}`))}import{spawn as spawn11}from"child_process";import fs72 from"fs";import os11 from"os";import path66 from"path";import fs71 from"fs";import path65 from"path";import fs70 from"fs";import path63 from"path";var MAX_PARTS=30,repoPaths=meta=>meta.repos?.length?meta.repos.map(r=>r.path):["."],commandsOn=(meta,repo)=>meta.repos?.length?meta.repos.find(r=>r.path===repo)?.commands:meta.commands,aloneInRepo=(parts2,p)=>!p.shared&&parts2.filter(x=>!x.shared&&x.repo===p.repo).length===1,withoutUrl=({url:_url,...rest})=>rest,idFrom=text3=>text3.trim().toLowerCase().replace(/[^\p{L}\p{N}_-]+/gu,"-").replace(/^-+|-+$/g,"").slice(0,40);function partOfRepo(meta,repo){let words2=repo.replace(/[-_.]+/g," ").trim(),name=repo==="."||!words2?meta.project:`${words2[0].toUpperCase()}${words2.slice(1)}`;return{id:idFrom(repo)||"app",name,repo,needs:[]}}function partsOf2(meta){let parts2=meta.parts?.length?meta.parts:repoPaths(meta).map(r=>partOfRepo(meta,r));return parts2.map(p=>{let url=aloneInRepo(parts2,p)?commandsOn(meta,p.repo)?.url?.trim():void 0;return url?{...p,url}:p})}function severalParts(meta){return partsOf2(meta).filter(p=>!p.shared).length>1}var wordsOf2=names2=>names2.length<2?names2.join(""):`${names2.slice(0,-1).join(", ")} and ${names2.at(-1)}`;function productLine(meta){if(!meta.parts?.length||!severalParts(meta))return;let on=partsOf2(meta).filter(p=>!p.off&&!p.noRun),running5=on.filter(p=>!p.shared).map(p=>p.name),shared=on.filter(p=>p.shared).map(p=>p.name),first2=running5.length===1?`The product has one part in use: ${running5[0]}.`:`The product has ${running5.length} parts: ${wordsOf2(running5)}.`;return shared.length?`${first2} ${running5.length===1?"It uses":"They share"} ${wordsOf2(shared)}.`:first2}function runsAsProduct(meta){return!!meta.parts?.length&&partsOf2(meta).filter(p=>!p.shared&&!p.off&&!p.noRun).length>1}function partsToRun(meta,changed){let parts2=partsOf2(meta).filter(p=>!p.off),want=new Set(changed.filter(id2=>parts2.some(p=>p.id===id2))),grow=more3=>{for(let before=-1;before!==want.size;){before=want.size;for(let p of parts2)for(let id2 of more3(p))parts2.some(x=>x.id===id2)&&want.add(id2)}};grow(p=>p.needs.some(n=>want.has(n))?[p.id]:[]),grow(p=>want.has(p.id)?p.needs:[]);let ordered=[],seen=new Set,visit=p=>{if(!seen.has(p.id)){seen.add(p.id);for(let n of p.needs){let dep=parts2.find(x=>x.id===n);dep&&want.has(dep.id)&&visit(dep)}ordered.push(p)}};return parts2.filter(p=>want.has(p.id)).forEach(visit),ordered.filter(p=>!p.shared&&!p.noRun)}function pictureInUse(meta,picture){let known=partsOf2(meta),off=known.filter(p=>p.off),parts2=picture.parts.filter(p=>!off.some(o=>o.id===p.id||o.name.toLowerCase()===p.name.toLowerCase())).map(p=>{let run7=known.find(k=>k.byHand&&k.id===p.id);return run7?{...p,name:run7.name}:p});return{parts:parts2,links:picture.links.filter(l=>parts2.some(p=>p.id===l.from)&&parts2.some(p=>p.id===l.to))}}function repoInUse(meta,repo){let parts2=(meta.parts??[]).filter(p=>p.repo===repo);return!parts2.length||parts2.some(p=>!p.off)}function repoRuns(meta,repo){let parts2=(meta.parts??[]).filter(p=>p.repo===repo);return!parts2.length||parts2.some(p=>!p.off&&!p.noRun)}function repoLabel(meta,repo){let names2=(meta.parts??[]).filter(p=>p.repo===repo&&!p.off&&!p.shared).map(p=>p.name);return names2.length?names2.join(", "):void 0}var isAddress=text3=>/^https?:\/\/[^\s/]+\S*$/i.test(text3);function folderInRepo(text3){let parts2=text3.trim().replace(/\\/g,"/").split("/").filter(x=>x&&x!==".");return text3.trim().startsWith("/")||/^[a-z]:/i.test(text3.trim())||parts2.includes("..")?null:parts2.join("/")}function reaches(parts2,from,to,seen=new Set){return from===to?!0:seen.has(from)?!1:(seen.add(from),(parts2.find(p=>p.id===from)?.needs??[]).some(n=>reaches(parts2,n,to,seen)))}function cleanParts(found2,meta,root){let repos=repoPaths(meta),parts2=[];for(let raw of(Array.isArray(found2)?found2:[]).slice(0,MAX_PARTS)){if(!raw||typeof raw!="object")continue;let p=raw,name=typeof p.name=="string"?p.name.trim():"",id2=idFrom(typeof p.id=="string"?p.id:name);if(!id2||!name||parts2.some(x=>x.id===id2))continue;let url=typeof p.url=="string"&&isAddress(p.url.trim())?p.url.trim():"";if(p.shared===!0){parts2.push({id:id2,name,shared:!0,needs:[],...url?{url}:{}});continue}let said=typeof p.repo=="string"?p.repo.trim().replace(/\/+$/,"").replace(/^\.\//,""):"",repo=repos.includes(said)?said:repos.length===1?repos[0]:"";if(!repo)continue;let folder=typeof p.folder=="string"?folderInRepo(p.folder):"",real2=folder&&fs70.existsSync(path63.join(root,repo,folder))?folder:"";parts2.push({id:id2,name,repo,needs:[],...real2?{folder:real2}:{},...url?{url}:{}})}if(!parts2.some(p=>!p.shared))return[];for(let repo of meta.repos?.length?repos:[]){if(parts2.some(p2=>p2.repo===repo))continue;let p=partOfRepo(meta,repo);parts2.push({...p,id:parts2.some(x=>x.id===p.id)?`${p.id}-repo`:p.id,noRun:!0})}for(let raw of Array.isArray(found2)?found2:[]){let p=raw&&typeof raw=="object"?raw:{},part=parts2.find(x=>x.id===idFrom(typeof p.id=="string"?p.id:typeof p.name=="string"?p.name:""));for(let need of Array.isArray(p.needs)?p.needs:[]){let id2=typeof need=="string"?idFrom(need):"";part&&parts2.some(x=>x.id===id2)&&!part.needs.includes(id2)&&!reaches(parts2,id2,part.id)&&part.needs.push(id2)}}return oneAddress(meta,parts2).map(p=>p.folder&&aloneInRepo(parts2,p)?(({folder:_folder,...rest})=>rest)(p):p)}function oneAddress(meta,parts2){return parts2.map(p=>aloneInRepo(parts2,p)&&commandsOn(meta,p.repo)?.url?.trim()?withoutUrl(p):p)}var samePart=(a,b)=>a.id===b.id||a.name.toLowerCase()===b.name.toLowerCase();function partsAfterRead(found2,kept2=[]){let mine2=kept2.filter(p=>p.byHand),parts2=[...found2.map(f=>mine2.find(m=>samePart(m,f))??f),...mine2.filter(m=>!found2.some(f=>samePart(m,f)))],needed=kept2.filter(k=>!parts2.some(p=>samePart(p,k))&&mine2.some(m=>m.needs.includes(k.id))),all=[...parts2,...needed],before=[...found2,...kept2],idNow=need=>{let was=before.find(p=>p.id===need);return all.find(p=>p.id===need)?.id??(was?all.find(p=>samePart(p,was))?.id:void 0)};return all.map(p=>({...p,needs:[...new Set(p.needs.map(idNow).filter(n=>!!n))]}))}function findPart(parts2,which){let said=which.trim().toLowerCase(),part=parts2.find(p=>p.id===said||p.id===idFrom(said)||p.name.toLowerCase()===said);if(!part)throw new Error(`There is no part "${which}". The parts: ${parts2.map(p=>p.id).join(", ")}.`);return part}function applied(bp,meta,parts2,part,change){let name=change.name?.trim()??part.name;if(!name)throw new Error("Give the part a name.");if(parts2.some(p=>p.id!==part.id&&p.name.toLowerCase()===name.toLowerCase()))throw new Error(`There is a part called ${name} already.`);let shared=change.shared??!!part.shared,url=(change.url??part.url??"").trim();if(url&&!isAddress(url))throw new Error(`"${url}" is not a web address. Write it like http://localhost:5180`);let off=change.off??!!part.off;if(off&&!part.shared&&!parts2.some(p=>p.id!==part.id&&!p.shared&&!p.off))throw new Error(`${name} is the only part in use, so the squad would have nothing to work on.`);let needs=(change.needs??part.needs).map(n=>findPart(parts2,n).id);for(let id2 of needs){let other=findPart(parts2,id2);if(id2===part.id)throw new Error(`${name} cannot need itself.`);if(reaches(parts2.map(p=>p.id===part.id?{...p,needs}:p),other.id,part.id))throw new Error(`${name} and ${other.name} need each other, so neither could start first. Take one of them out.`)}let base2={id:part.id,name,needs:[...new Set(needs)],byHand:!0,...url?{url}:{},...off?{off}:{}};if(shared)return{...base2,shared};let repo=change.repo?.trim()||part.repo||(repoPaths(meta).length===1?repoPaths(meta)[0]:"");if(!repoPaths(meta).includes(repo))throw new Error(repo?`There is no code repo at ${repo}. The repos: ${repoPaths(meta).join(", ")}.`:`Say which repo it is in: ${repoPaths(meta).join(", ")}.`);let folder=folderInRepo(change.folder??part.folder??"");if(folder===null)throw new Error("The folder must be inside the repo, like apps/web.");if(folder&&!fs70.existsSync(path63.join(bp.root,repo,folder)))throw new Error(`There is no folder ${folder} in ${repo==="."?"the project":repo}.`);return{...base2,repo,...folder?{folder}:{}}}function saveParts(bp,meta,parts2,changed){let next=meta;if(changed&&aloneInRepo(parts2,changed)){let withUrl=(c={})=>{let{url:_old,...rest}=c;return changed.url?{...rest,url:changed.url}:rest};next=meta.repos?.length?{...meta,repos:meta.repos.map(r=>r.path===changed.repo?{...r,commands:withUrl(r.commands)}:r)}:{...meta,commands:withUrl(meta.commands)}}writeMeta(bp,{...next,parts:oneAddress(next,parts2)})}function addPart(bp,change){let meta=readMeta(bp),parts2=partsOf2(meta),name=change.name?.trim()??"";if(!name)throw new Error("Give the part a name.");let base2=idFrom(name)||"part",id2=base2;for(let n=2;parts2.some(p=>p.id===id2);n+=1)id2=`${base2}-${n}`;let part=applied(bp,meta,parts2,{id:id2,name,needs:[]},change);return saveParts(bp,meta,[...parts2,part],part),part}function changePart(bp,which,change){let meta=readMeta(bp),parts2=partsOf2(meta),part=applied(bp,meta,parts2,findPart(parts2,which),change);return saveParts(bp,meta,parts2.map(p=>p.id===part.id?part:p),part),part}function removePart(bp,which){let meta=readMeta(bp),parts2=partsOf2(meta),part=findPart(parts2,which);if(!part.byHand)throw new Error(`The Architect found ${part.name}, so it would come back at the next read. Mark it Not used instead.`);let left2=parts2.filter(p=>p.id!==part.id);if(!left2.some(p=>p.repo===part.repo)&&!part.shared)throw new Error(`${part.name} is the only part in ${part.repo==="."?"the project":part.repo}. Mark it Not used instead.`);saveParts(bp,meta,left2.map(p=>({...p,needs:p.needs.filter(n=>n!==part.id)})))}import path64 from"path";function copiesOf(meta,t){if(!t.worktree)return[];let first2=repoFor(meta,t),named=(repo,dir,baseCommit)=>({repo,dir,...baseCommit?{baseCommit}:{},label:repoLabel(meta,repo.path)??repo.path});return[named(first2,t.worktree,t.baseCommit),...(t.copies??[]).map(c=>named(reposOf(meta).find(r=>r.path===c.repo)??{path:c.repo,base:first2.base,remote:"",commands:{}},c.dir,c.baseCommit))]}function workFolder(t){return t.storyDir??t.worktree}function storyCopyDir(root,id2,repo){return path64.join(worktreeRoot(root),id2.toLowerCase(),repo)}function reposOfParts(meta,parts2){let known=partsOf2(meta),repos=(parts2??[]).map(id2=>known.find(p=>p.id===id2&&!p.shared&&!p.off)?.repo).filter(r=>!!r);return[...new Set(repos)].filter(r=>reposOf(meta).some(x=>x.path===r))}function repoOrder(meta){let parts2=partsOf2(meta),quiet=parts2.filter(p=>p.noRun&&p.repo).map(p=>p.repo),running5=partsToRun(meta,parts2.map(p=>p.id)).map(p=>p.repo).filter(r=>!!r);return[...new Set([...quiet,...running5])]}function repoReliesOn(meta,later,first2){let parts2=partsOf2(meta),seen=new Set,reach2=id2=>{if(seen.has(id2))return!1;seen.add(id2);let p=parts2.find(x=>x.id===id2);return!!p&&(p.repo===first2||p.needs.some(reach2))};return parts2.filter(p=>p.repo===later).some(p=>p.needs.some(reach2))}function copyViews(meta,t){if(!t.copies?.length)return[t];let{copies,...one2}=t,views=[one2,...copies.map(c=>({...one2,repo:c.repo,worktree:c.dir,...c.baseCommit?{baseCommit:c.baseCommit}:{}}))],order=repoOrder(meta),place=v=>{let at3=order.indexOf(v.repo??"");return at3<0?order.length:at3};return views.map((v,i)=>({v,i})).sort((a,b)=>place(a.v)-place(b.v)||a.i-b.i).map(x=>x.v)}function appCopyOf(meta,t){let copies=copiesOf(meta,t);return copies.find(c=>c.repo.commands.start?.trim()&&c.repo.commands.url?.trim())??copies[0]}var read2=file3=>{try{return fs71.readFileSync(file3,"utf8")}catch{return null}},has=(dir,...p)=>fs71.existsSync(path65.join(dir,...p));function packages(dir){let text3=read2(path65.join(dir,"package.json"));if(text3===null)return null;try{let j=JSON.parse(text3);return{...j.dependencies,...j.devDependencies,...j.peerDependencies}}catch{return{}}}function xcodeProject(dir){let here=d=>{try{return fs71.readdirSync(d).some(f=>f.endsWith(".xcodeproj")||f.endsWith(".xcworkspace"))}catch{return!1}};if(here(dir))return!0;try{return fs71.readdirSync(dir,{withFileTypes:!0}).some(e=>e.isDirectory()&&!e.name.startsWith(".")&&e.name!=="node_modules"&&here(path65.join(dir,e.name)))}catch{return!1}}var GRADLE=["build.gradle","build.gradle.kts"],androidApp=file3=>/com\.android\.application/.test(read2(file3)??"");function kindsOf(dir){let kinds=[],platforms=new Set,deps=packages(dir),pubspec=read2(path65.join(dir,"pubspec.yaml")),flutter2=pubspec!==null&&/^\s+sdk:\s*["']?flutter\b/m.test(pubspec),reactNative=!!(deps&&("react-native"in deps||"expo"in deps));return flutter2&&kinds.push("flutter"),reactNative&&kinds.push("react-native"),flutter2||reactNative?(has(dir,"ios")&&platforms.add("ios"),has(dir,"android")&&platforms.add("android"),has(dir,"web")&&platforms.add("web"),reactNative&&deps&&"expo"in deps&&!has(dir,"ios")&&!has(dir,"android")&&(platforms.add("ios"),platforms.add("android"))):(deps&&(kinds.push("web"),platforms.add("web")),xcodeProject(dir)&&(kinds.push("ios"),platforms.add("ios")),GRADLE.some(g=>has(dir,"android","app",g)||androidApp(path65.join(dir,"app",g))||androidApp(path65.join(dir,g)))&&(kinds.push("android"),platforms.add("android"))),["docker-compose.yml","docker-compose.yaml","compose.yml","compose.yaml"].some(f=>has(dir,f))&&kinds.push("docker"),{kinds,platforms:[...platforms]}}function placesOf(root,meta,story2){let repos=meta?reposOf(meta):[{path:".",base:"main",remote:"",commands:{}}],copies=meta&&story2?.worktree&&fs71.existsSync(story2.worktree)?copiesOf(meta,story2):[],storyRepos=new Set(copies.length?copies.map(c=>c.repo.path):story2?.repo?[story2.repo]:[]),parts2=meta?partsOf2(meta).filter(p=>!p.shared&&!p.off):[],wanted=story2?.parts?.length?new Set(story2.parts):null,out20=[];for(let r of repos){if(storyRepos.size&&!storyRepos.has(r.path))continue;let top=copies.find(c=>c.repo.path===r.path)?.dir??path65.join(root,r.path),inRepo=parts2.filter(p=>(p.repo??".")===r.path&&(!wanted||wanted.has(p.id)));inRepo.length||out20.push({dir:top,place:r.path});for(let p of inRepo)out20.push({dir:path65.join(top,p.folder??""),place:path65.join(r.path,p.folder??""),part:p.name})}let seen=new Set;return out20.filter(p=>seen.has(p.dir)?!1:(seen.add(p.dir),!0))}function projectKinds(root,meta,story2){return placesOf(root,meta,story2).map(p=>({place:p.place,...p.part?{part:p.part}:{},...kindsOf(p.dir)})).filter(p=>p.kinds.length)}var PRECACHE_MS=20*6e4;function runsFlutter(command){return/(?:^|[\s;&|(])flutter(?:\s|$)/.test(command)}function precacheArgs(platforms,platform=process.platform){let want=[...platforms.includes("ios")&&platform==="darwin"?["--ios"]:[],...platforms.includes("web")?["--web"]:[]];return want.length?["precache",...want]:null}function engineOf(root){try{return fs72.readFileSync(path66.join(root,"bin","internal","engine.version"),"utf8").trim()}catch{return""}}var realRunner=(program2,args,o)=>new Promise(resolve2=>{let output="",child=track(spawn11(program2,args,{cwd:o.cwd,env:o.env,detached:OWN_GROUP,stdio:["ignore","pipe","pipe"]})),keep=chunk=>{output=(output+chunk.toString("utf8")).slice(-4e3),o.onOutput?.()};child.stdout.on("data",keep),child.stderr.on("data",keep);let end=()=>endGroup(child),timer=setTimeout(end,PRECACHE_MS);o.signal?.addEventListener("abort",end,{once:!0}),child.on("error",err2=>output+=`
431
431
  ${err2.message}`),whenExited(child).then(code=>{clearTimeout(timer),o.signal?.removeEventListener("abort",end),resolve2({ok:code===0,output})})}),runner=realRunner;var done=new Set,running2=new Map;async function flutterDownloads(dir,command,o={}){if(!runsFlutter(command))return"not-needed";let{kinds,platforms}=kindsOf(dir),args=kinds.includes("flutter")?precacheArgs(platforms,o.platform):null,env2=o.env??projectEnv(),flutter2=args?flutterOnPath(env2):null;if(!args||!flutter2)return"not-needed";let key=[flutter2.root,engineOf(flutter2.root),...args].join(`
432
432
  `);if(done.has(key))return"had";let run7=running2.get(key);return run7||(run7=runner(flutter2.program,args,{cwd:o.home??os11.homedir(),env:{...env2,FLUTTER_SUPPRESS_ANALYTICS:"true"},...o.signal?{signal:o.signal}:{},...o.onOutput?{onOutput:o.onOutput}:{}}).then(r=>(r.ok?done.add(key):writeLog({level:"warn",text:`squad-kit could not get Flutter's downloads (flutter ${args.join(" ")}), so the command runs without them:
@@ -438,7 +438,7 @@ ${body10}`).join(`
438
438
  `):null}return/^vite\.config\./.test(name)?block2(text3,/\btest\s*:\s*\{/):null}function sections2(text3){let out20=[];for(let chunk of text3.split(/^(?=\[)/m)){let nl=chunk.indexOf(`
439
439
  `),head=(nl<0?chunk:chunk.slice(0,nl)).trim();head.startsWith("[")&&out20.push([head,(nl<0?"":chunk.slice(nl+1)).trim()])}return out20}function block2(text3,start){let m=start.exec(text3);if(!m)return null;let depth=0;for(let i=m.index+m[0].length-1;i<text3.length;i++)if(text3[i]==="{")depth++;else if(text3[i]==="}"&&--depth===0)return text3.slice(m.index,i+1).replace(/\s+/g," ");return text3.slice(m.index).replace(/\s+/g," ")}var short=text3=>createHash5("sha1").update(text3).digest("hex").slice(0,8),plural2=(n,one2)=>`${n} ${one2}${n===1?"":"s"}`;function worthReading(file3){let name=path69.basename(file3);return isTestFile(file3)||isTestConfig(file3)||MIXED_FILES.test(name)||BUILD_FILES.test(name)||/\.rs$/.test(file3)}function scriptsOf2(command){let out20=[];for(let m of(command??"").matchAll(/\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:-\S+\s+)*([\w:.-]+)/g))["install","ci","exec","dlx","x"].includes(m[1])||out20.push(m[1]);return out20}async function gateChanged(dir,base2,command,who4){let changed=new Set((await gitLater(dir,["diff","--name-only","--relative",base2],{allowFail:!0})).split(`
440
440
  `).filter(Boolean));if(!changed.size)return[];let out20=[];for(let word2 of new Set(command.split(/\s+/).map(w=>w.replace(/^['"]|['"]$/g,"").replace(/^\.\//,"")))){if(!changed.has(word2)||!git(dir,["rev-parse","--verify","-q",`${base2}:./${word2}`],{allowFail:!0}))continue;let now3=fs75.existsSync(path69.join(dir,word2))?fs75.readFileSync(path69.join(dir,word2),"utf8"):"";out20.push({kind:"config",file:word2,text:`${word2} changed, and ${who4} runs it.`,key:`gate:${word2}:${short(now3)}`})}if(changed.has("package.json")){let scriptIn=(text3,s)=>{try{return(JSON.parse(text3).scripts??{})[s]}catch{return}},was=git(dir,["show",`${base2}:./package.json`],{allowFail:!0}),nowText=fs75.existsSync(path69.join(dir,"package.json"))?fs75.readFileSync(path69.join(dir,"package.json"),"utf8"):"";for(let s of scriptsOf2(command)){let now3=scriptIn(nowText,s);scriptIn(was,s)!==now3&&out20.push({kind:"config",file:"package.json",text:`The "${s}" script in package.json changed, and ${who4} runs it.`,key:`gate:${s}:${short(now3??"")}`})}}return out20}function findWeakening(changes,scripts=[]){let out20=[],tests=changes.filter(c=>isTestFile(c.path)||c.from&&isTestFile(c.from)||/\.rs$/.test(c.path)),before=tests.reduce((n,c)=>n+testCases(c.from??c.path,c.before),0);if(tests.reduce((n,c)=>n+testCases(c.path,c.after),0)<before)for(let c of tests){let was=testCases(c.from??c.path,c.before),now3=testCases(c.path,c.after);now3>=was||(c.after===null?out20.push({kind:"deleted",file:c.path,text:`${c.path} was deleted (${plural2(was,"test")}).`,key:`deleted:${c.path}`}):out20.push({kind:"fewer",file:c.path,text:`${c.path} has fewer tests: ${was} before, ${now3} now.`,key:`fewer:${c.path}:${now3}`}))}for(let c of changes){if(isTestFile(c.path)||/\.rs$/.test(c.path)){let was=skipMarkers(c.from??c.path,c.before),added2=[...skipMarkers(c.path,c.after)].filter(([word2,n])=>n>(was.get(word2)??0)).map(([word2])=>word2);if(added2.length){let only=added2.filter(w=>/only|^f\w+$|focus/.test(w)),off=added2.filter(w=>!only.includes(w)),parts2=[off.length?`tests switched off with ${off.join(", ")}`:"",only.length?`only some tests run because of ${only.join(", ")}`:""].filter(Boolean);out20.push({kind:"skip",file:c.path,text:`In ${c.path}: ${parts2.join("; ")}.`,key:`skip:${c.path}:${added2.join(",")}:${count2(c.after,SKIPS[lang(c.path)])}`})}}let name=path69.basename(c.path);if(isTestConfig(c.path)&&c.before!==null&&c.before.trim()!==(c.after??"").trim())out20.push({kind:"config",file:c.path,text:c.after===null?`The test settings in ${c.path} were deleted.`:`The test settings in ${c.path} changed.`,key:`config:${c.path}:${short(c.after??"")}`});else if(MIXED_FILES.test(name)){let was=testPart(c.from??c.path,c.before,scripts),now3=testPart(c.path,c.after,scripts);if(was!==null&&was!==now3&&!grewOnly(name,was,now3)){let what=name==="package.json"?`The test scripts or settings in ${c.path} changed`:`The test settings in ${c.path} changed`;out20.push({kind:"config",file:c.path,text:`${what}${scriptChange(name,was,now3)}.`,key:`config:${c.path}:${short(now3??"")}`})}}else if(BUILD_FILES.test(name)){let now3=count2(c.after,BUILD_SKIPS);now3>count2(c.before,BUILD_SKIPS)&&out20.push({kind:"config",file:c.path,text:`Tests are skipped or their failures ignored in ${c.path}.`,key:`config:${c.path}:${now3}`})}}return out20}function runsNoTests(script){return typeof script=="string"&&script.split(/&&|\|\||;/).every(part=>/^\s*(?:echo\b.*|exit(?:\s+\d+)?|true|:|node\s+-e\s+["']process\.exit\(\d*\)["'])\s*$/.test(part))}function grewOnly(name,was,now3){if(name!=="package.json"||now3===null)return!1;let a=JSON.parse(was),b=JSON.parse(now3);return Object.keys(a).every(k=>JSON.stringify(a[k])===JSON.stringify(b[k])||runsNoTests(a[k])&&typeof b[k]=="string"&&!runsNoTests(b[k]))}function scriptChange(name,was,now3){if(name!=="package.json")return"";let a=JSON.parse(was),b=now3?JSON.parse(now3):{},k=Object.keys(a).find(x=>x.startsWith("scripts.")&&JSON.stringify(a[x])!==JSON.stringify(b[x]));if(!k)return"";let script=k.slice(8);return b[k]===void 0?`: the "${script}" script was removed`:`: "${script}" was "${String(a[k])}", now "${String(b[k])}"`}var MAX_FILES2=400,MAX_BYTES=1024*1024;async function changedTestFiles(dir,base2,skip=[]){let fields=(await gitLater(dir,["diff","--name-status","-z","--find-renames",base2],{allowFail:!0})).split("\0").filter(Boolean),listed3=[];for(let i=0;i<fields.length;){let status=fields[i++][0];if(status==="R"||status==="C"){let from=fields[i++];listed3.push({status:"R",from,path:fields[i++]})}else listed3.push({status,path:fields[i++]})}for(let f of(await gitLater(dir,["ls-files","--others","--exclude-standard","-z"],{allowFail:!0})).split("\0").filter(Boolean))listed3.push({status:"A",path:f});for(let added2 of listed3.filter(l=>l.status==="A")){let gone=listed3.find(l=>l.status==="D"&&path69.basename(l.path)===path69.basename(added2.path));gone&&(listed3.splice(listed3.indexOf(gone),1),Object.assign(added2,{status:"R",from:gone.path}))}let out20=[];for(let l of listed3)if(!skip.some(s=>l.path.startsWith(s))&&!(!worthReading(l.path)&&!(l.from&&worthReading(l.from)))){if(out20.length>=MAX_FILES2)break;out20.push({path:l.path,...l.from?{from:l.from}:{},before:l.status==="A"?null:blob(dir,base2,l.from??l.path),after:l.status==="D"?null:readText2(path69.join(dir,l.path))})}return out20}function blob(dir,base2,file3){return git(dir,["show",`${base2}:${file3}`],{allowFail:!0})||null}function readText2(file3){try{let st=fs75.statSync(file3);return st.isFile()&&st.size<=MAX_BYTES?fs75.readFileSync(file3,"utf8"):null}catch{return null}}async function weakenedChecks(dir,base2,opts={}){return findWeakening(await changedTestFiles(dir,base2,opts.skip),scriptsOf2(opts.testCommand))}function blockedByGuard(ran2,guard){for(let r of ran2){if(r.ok||r.outsideGuard)continue;let hit=guardBlockedPath(r.output,guard);if(hit)return{result:r,path:hit}}return null}var CHECK_SILENT_MS=5*6e4,CHECK_MS=15*6e4,STOPPED2="You stopped the squad.";function minutes(ms2){let m=Math.max(1,Math.round(ms2/6e4));return`${m} minute${m===1?"":"s"}`}var OUTSIDE_THE_GUARD_WORDS="Xcode can't start inside squad-kit's guard, so squad-kit runs Xcode's builds and tests outside it, for the checks, the phone pictures and Start, under Xcode's own protections. Everything else the squad runs stays inside the guard.";function outsideTheGuard(cwd,command,guard){if(!guard||!ownXcodeCommand(command))return!1;try{return fs76.realpathSync(cwd)===guard.cwd}catch{return!1}}async function runCommand(cwd,command,timeoutMs=CHECK_MS,watch={}){if(outsideTheGuard(cwd,command,watch.guard)){let{guard,...rest}=watch;return{...await runCommand(cwd,command,timeoutMs,{...rest,env:{...guardEnv(guard,cwd),...watch.env}}),outsideGuard:!0}}watch.guard&&await flutterDownloads(cwd,command,{...watch.signal?{signal:watch.signal}:{},...watch.onOutput?{onOutput:watch.onOutput}:{}});let homes=ownHomes(command,watch.guard),set2=env3=>env3?{...watch,env:{...watch.env,...env3}}:watch,first2=await runOnce(cwd,command,timeoutMs,set2(homes.env(homes.before)));if(first2.ok||first2.ended||watch.signal?.aborted)return first2;let refused=homes.refused(first2.output),env2=homes.env([...homes.before,...refused]);if(!refused.length||!env2)return first2;let detail=homes.said(refused,env2);watch.onHealed?watch.onHealed(detail):writeLog({level:"info",text:detail});let again2={...await runOnce(cwd,command,timeoutMs,set2(env2)),healed:detail};return homes.learn(again2.output,refused),{...again2,seconds:first2.seconds+again2.seconds}}function ownHomes(command,guard){let cache4=guard?.cache,hidden={hidden:guard?.noRead??[]},before=cache4?healedBefore(cache4,command,hidden):[];return{before,env(places){if(!cache4||!places.length)return null;try{return healEnv(cache4,places,hidden)}catch{return null}},refused(output){return cache4&&guard?homeCachesRefused(output,guard).filter(p=>!before.includes(p)):[]},said(refused,env2){return`A tool could not write in ${refused.map(p=>inHome(p)).join(", ")}, so squad-kit ran this once more with HOME=${env2.HOME}, where ${refused.length===1?"that place is":"those places are"} the project's own: ${command}`},learn(output,refused){cache4&&guard&&!homeCachesRefused(output,guard).some(p=>refused.includes(p))&&rememberHealed(cache4,command,[...before,...refused])}}}function inHome(p,home2=os12.homedir()){return p.startsWith(`${home2}/`)?`~${p.slice(home2.length)}`:p}function runOnce(cwd,command,timeoutMs,watch){let started2=Date.now();return watch.signal?.aborted?Promise.resolve({ok:!1,output:STOPPED2,seconds:0}):new Promise(resolve2=>{let child=spawnProject(command,{cwd,env:{CI:"1",FORCE_COLOR:"0",...watch.env},...watch.guard?{guard:watch.guard}:{}});child.pid&&watch.onSpawn?.(child.pid);let out20="",ended="",end=why=>{ended||(ended=why,endGroup(child))},silent,heard=()=>{if(!watch.silentMs)return;clearTimeout(silent);let ms2=watch.silentMs;silent=setTimeout(()=>end(`The command printed nothing for ${minutes(ms2)}; it may be waiting in watch mode or for input.`),ms2)},keep=chunk=>{out20=(out20+chunk.toString("utf8")).slice(-12e3),watch.onOutput?.(),heard()};child.stdout.on("data",keep),child.stderr.on("data",keep),heard();let timer=setTimeout(()=>end(`stopped after ${Math.round(timeoutMs/6e4)} minutes`),timeoutMs),onStop=()=>end(STOPPED2);watch.signal?.addEventListener("abort",onStop,{once:!0});let done2=!1,finish2=(ok2,output,code)=>{done2||(done2=!0,clearTimeout(timer),clearTimeout(silent),watch.signal?.removeEventListener("abort",onStop),resolve2({ok:ok2,output,seconds:(Date.now()-started2)/1e3,...ended?{ended:!0}:{},...typeof code=="number"?{code}:{}}))};child.on("error",err2=>finish2(!1,err2.message)),whenExited(child,1e3,()=>endGroup(child)).then(code=>{watch.endAll&&endGroup(child),finish2(code===0&&!ended,`${out20.trim()}${ended?`
441
- (${ended})`:""}`.trim(),code)})})}async function withOneMoreRun(run7,watch,onAgain,alone){let first2=await run7();if(first2.ok||first2.ended||watch.signal?.aborted||watch.guard&&!first2.outsideGuard&&guardBlockedPath(first2.output,watch.guard))return first2;onAgain();let quick=alone?.(first2);if(quick&&!(await quick()).ok||watch.signal?.aborted)return first2;let again2=await run7();return again2.ok?{...again2,failedFirst:tail(first2.output,30)}:again2}var MAX_ALONE=10;function failedAlone(cwd,command,watch){return first2=>{let failed3=failedTests(first2.output);if(!failed3.length||failed3.length>MAX_ALONE)return null;let each=failed3.map(t=>testAlone(cwd,command,t));return each.some(c=>!c)?null:async()=>{for(let one2 of each){let r=await runCommand(cwd,one2,void 0,{silentMs:CHECK_SILENT_MS,...watch});if(!r.ok||!testCount(r.output))return{ok:!1}}return{ok:!0}}}}function failedOnceLine(name){return name==="start"?"The app did not start the first time, and started when run again.":`${name==="test"?"A test":name==="lint"?"The code check":name==="build"?"The build":`The ${name} check`} failed once and passed when run again.`}var OWN_CHECK="scripts/check";function ownCheck(cwd,commands){if(process.platform==="win32")return;try{let stat=fs76.statSync(path70.join(cwd,OWN_CHECK));if(!stat.isFile()||!(stat.mode&73))return}catch{return}let said=[commands.test,commands.lint,commands.build].filter(Boolean).join(" "),scripts={};try{scripts=JSON.parse(fs76.readFileSync(path70.join(cwd,"package.json"),"utf8")).scripts??{}}catch{}return[said,...scriptsOf2(said).map(s=>scripts[s]??"")].some(c=>c.includes(OWN_CHECK))?void 0:`./${OWN_CHECK}`}function checksToRun(cwd,commands){return[...["test","lint","build"].map(name=>({name,command:commands[name]?.trim()})),{name:"check",command:ownCheck(cwd,commands)}].filter(s=>!!s.command)}function startsToo(commands,results){return!!(commands.start?.trim()&&commands.url?.trim())&&!results.some(r=>r.name==="test"&&testCount(r.output)!==0)}async function runChecks(cwd,commands,onStart,watch={},appWaitMs,known=[]){let results=[];for(let{name,command}of checksToRun(cwd,commands)){if(watch.signal?.aborted)break;let had=known.find(k=>k.name===name&&k.command===command);if(had){results.push(had);continue}onStart?.(name,command);let r=await withOneMoreRun(()=>runCommand(cwd,command,void 0,{silentMs:CHECK_SILENT_MS,...watch}),watch,()=>onStart?.(name),name==="test"?failedAlone(cwd,command,watch):void 0);results.push({name,command,...r})}let start=commands.start?.trim(),url=commands.url?.trim();if(start&&url&&startsToo(commands,results)&&!watch.signal?.aborted){onStart?.("start");let once2=async()=>{let began=Date.now(),seconds2=()=>(Date.now()-began)/1e3;try{let app=await startApp(cwd,start,url,appWaitMs,watch.signal,watch.guard,watch.onHealed?{onHealed:watch.onHealed}:{});return app.stop(),{ok:!0,output:`The app answered at ${app.url}.`,seconds:seconds2()}}catch(err2){return{ok:!1,output:err2.message,seconds:seconds2()}}};results.push({name:"start",command:start,...await withOneMoreRun(once2,watch,()=>onStart?.("start"))})}return results}var AddressTaken=class extends Error{};async function answers(url){for(let u of[url,url.replace("://localhost","://127.0.0.1")])if(await fetch(u,{signal:AbortSignal.timeout(3e3)}).then(r=>r.status<500).catch(()=>!1))return!0;return!1}function held(port){return port?new Promise(resolve2=>{let s=net2.connect({host:"127.0.0.1",port}),done2=yes=>{s.destroy(),resolve2(yes)};s.setTimeout(800,()=>setImmediate(()=>done2(!1))),s.once("connect",()=>done2(!0)),s.once("error",()=>done2(!1))}):Promise.resolve(!1)}var stopping=new Map;async function goneFrom(port,exited){let until=Date.now()+KILL_AFTER_MS+1e3;for(;Date.now()<until&&(!exited()||await held(port));)await new Promise(r=>setTimeout(r,50))}function freePort(){return new Promise((resolve2,reject)=>{let srv=net2.createServer();srv.once("error",reject),srv.listen(0,"127.0.0.1",()=>{let port=srv.address().port;srv.close(()=>resolve2(port))})})}function portOf(url){try{let u=new URL(url);return u.port?Number(u.port):u.protocol==="https:"?443:u.protocol==="http:"?80:null}catch{return null}}var NO_WINDOW_START="The app's start command opens a window on this computer, and squad-kit starts apps with no window. This project has no web version it could start instead.";function withNoWindow(cwd,command,url){if(!opensAWindow(command))return{command,url};let found2=findCommands(cwd);if(!found2.start||opensAWindow(found2.start))throw new Error(NO_WINDOW_START);return writeLog({level:"info",text:`The start command opens a window (${command}), so squad-kit starts the web version with no window instead: ${found2.start}`}),{command:found2.start,url:found2.url??url}}async function startApp(cwd,given,at3,waitMs=3*6e4,signal2,guard,more3={}){if(signal2?.aborted)throw new Error(STOPPED2);let{command,url}=withNoWindow(cwd,given,at3);if(await stopping.get(portOf(url)??0),signal2?.aborted)throw new Error(STOPPED2);if(guard&&await flutterDownloads(cwd,command,signal2?{signal:signal2}:{}),signal2?.aborted)throw new Error(STOPPED2);let taken=await answers(url)||await held(portOf(url));if(taken&&more3.fixed)throw new AddressTaken(`${url} is used by another program (${command}).`);let port=taken?await freePort():null,target=port?url.replace(/(:\/\/[^/:]+)(:\d+)?/,`$1:${port}`):url,own2=port?appPortVariables(cwd,portOf(url)):[],env2={CI:"1",FORCE_COLOR:"0",BROWSER:"none",...port?Object.fromEntries(["PORT",...own2].map(name=>[name,String(port)])):{},...more3.env},homes=ownHomes(command,guard),launch2=extra=>{let run7={child:spawnProject(command,{cwd,env:{...env2,...extra},...guard?{guard}:{}}),out:"",exited:null},keep=chunk=>{run7.out=(run7.out+chunk.toString("utf8")).slice(-6e3)};return run7.child.stdout.on("data",keep),run7.child.stderr.on("data",keep),run7.child.on("error",err2=>{run7.out+=`
441
+ (${ended})`:""}`.trim(),code)})})}async function withOneMoreRun(run7,watch,onAgain,alone){let first2=await run7();if(first2.ok||first2.ended||watch.signal?.aborted||watch.guard&&!first2.outsideGuard&&guardBlockedPath(first2.output,watch.guard))return first2;onAgain();let quick=alone?.(first2);if(quick&&!(await quick()).ok||watch.signal?.aborted)return first2;let again2=await run7();return again2.ok?{...again2,failedFirst:tail(first2.output,30)}:again2}var MAX_ALONE=10;function failedAlone(cwd,command,watch){return first2=>{let failed3=failedTests(first2.output);if(!failed3.length||failed3.length>MAX_ALONE)return null;let each=failed3.map(t=>testAlone(cwd,command,t));return each.some(c=>!c)?null:async()=>{for(let one2 of each){let r=await runCommand(cwd,one2,void 0,{silentMs:CHECK_SILENT_MS,...watch});if(!r.ok||!testCount(r.output))return{ok:!1}}return{ok:!0}}}}function failedOnceLine(name){return name==="start"?"The app did not start the first time, and started when run again.":`${name==="test"?"A test":name==="lint"?"The code check":name==="build"?"The build":`The ${name} check`} failed once and passed when run again.`}var OWN_CHECK="scripts/check";function ownCheck(cwd,commands){if(process.platform==="win32")return;try{let stat=fs76.statSync(path70.join(cwd,OWN_CHECK));if(!stat.isFile()||!(stat.mode&73))return}catch{return}let said=[commands.test,commands.lint,commands.build].filter(Boolean).join(" "),scripts={};try{scripts=JSON.parse(fs76.readFileSync(path70.join(cwd,"package.json"),"utf8")).scripts??{}}catch{}return[said,...scriptsOf2(said).map(s=>scripts[s]??"")].some(c=>c.includes(OWN_CHECK))?void 0:`./${OWN_CHECK}`}function checksToRun(cwd,commands){return[...["test","lint","build"].map(name=>({name,command:commands[name]?.trim()})),{name:"check",command:ownCheck(cwd,commands)}].filter(s=>!!s.command)}function startsToo(commands,results){return!!(commands.start?.trim()&&commands.url?.trim())&&!results.some(r=>r.name==="test"&&testCount(r.output)!==0)}async function runChecks(cwd,commands,onStart,watch={},appWaitMs,known=[]){let results=[];for(let{name,command}of checksToRun(cwd,commands)){if(watch.signal?.aborted)break;let had=known.find(k=>k.name===name&&k.command===command);if(had){results.push(had);continue}onStart?.(name,command);let r=await withOneMoreRun(()=>runCommand(cwd,command,void 0,{silentMs:CHECK_SILENT_MS,...watch}),watch,()=>onStart?.(name),name==="test"?failedAlone(cwd,command,watch):void 0);results.push({name,command,...r})}let start=commands.start?.trim(),url=commands.url?.trim();if(start&&url&&startsToo(commands,results)&&!watch.signal?.aborted){onStart?.("start");let once2=async()=>{let began=Date.now(),seconds2=()=>(Date.now()-began)/1e3;try{let app=await startApp(cwd,start,url,appWaitMs,watch.signal,watch.guard,watch.onHealed?{onHealed:watch.onHealed}:{});return app.stop(),{ok:!0,output:`The app answered at ${app.url}.`,seconds:seconds2()}}catch(err2){return{ok:!1,output:err2.message,seconds:seconds2()}}};results.push({name:"start",command:start,...await withOneMoreRun(once2,watch,()=>onStart?.("start"))})}return results}var AddressTaken=class extends Error{};async function answers(url){for(let u of[url,url.replace("://localhost","://127.0.0.1")])if(await fetch(u,{signal:AbortSignal.timeout(3e3)}).then(r=>r.status<500).catch(()=>!1))return!0;return!1}function held(port){return port?new Promise(resolve2=>{let s=net2.connect({host:"127.0.0.1",port}),done2=yes=>{s.destroy(),resolve2(yes)};s.setTimeout(800,()=>setImmediate(()=>done2(!1))),s.once("connect",()=>done2(!0)),s.once("error",()=>done2(!1))}):Promise.resolve(!1)}var stopping=new Map;async function goneFrom(port,exited){let until=Date.now()+KILL_AFTER_MS+1e3;for(;Date.now()<until&&(!exited()||await held(port));)await new Promise(r=>setTimeout(r,50))}function freePort(){return new Promise((resolve2,reject)=>{let srv=net2.createServer();srv.once("error",reject),srv.listen(0,"127.0.0.1",()=>{let port=srv.address().port;srv.close(()=>resolve2(port))})})}function portOf(url){try{let u=new URL(url);return u.port?Number(u.port):u.protocol==="https:"?443:u.protocol==="http:"?80:null}catch{return null}}var NO_WINDOW_START="The app's start command opens a window on this computer, and squad-kit starts apps with no window. This project has no web version it could start instead.";function withNoWindow(cwd,command,url){if(!opensAWindow(command))return{command,url};let found2=webStartIn(cwd);if(!found2)throw new Error(NO_WINDOW_START);return writeLog({level:"info",text:`The start command opens a window (${command}), so squad-kit starts the web version with no window instead: ${found2.start}`}),{command:found2.start,url:found2.url??url}}function webStartIn(cwd){let found2=findCommands(cwd);return found2.start&&!opensAWindow(found2.start)?{start:found2.start,...found2.url?{url:found2.url}:{}}:null}function windowStartNote(cwd,command){if(!opensAWindow(command))return null;let found2=webStartIn(cwd);return found2?`This opens a window. squad-kit starts the app without one: ${found2.start}`:"This opens a window, and squad-kit starts apps without one. This project has no web version it could start instead."}async function startApp(cwd,given,at3,waitMs=3*6e4,signal2,guard,more3={}){if(signal2?.aborted)throw new Error(STOPPED2);let{command,url}=withNoWindow(cwd,given,at3);if(await stopping.get(portOf(url)??0),signal2?.aborted)throw new Error(STOPPED2);if(guard&&await flutterDownloads(cwd,command,signal2?{signal:signal2}:{}),signal2?.aborted)throw new Error(STOPPED2);let taken=await answers(url)||await held(portOf(url));if(taken&&more3.fixed)throw new AddressTaken(`${url} is used by another program (${command}).`);let port=taken?await freePort():null,target=port?url.replace(/(:\/\/[^/:]+)(:\d+)?/,`$1:${port}`):url,own2=port?appPortVariables(cwd,portOf(url)):[],env2={CI:"1",FORCE_COLOR:"0",BROWSER:"none",...port?Object.fromEntries(["PORT",...own2].map(name=>[name,String(port)])):{},...more3.env},homes=ownHomes(command,guard),launch2=extra=>{let run7={child:spawnProject(command,{cwd,env:{...env2,...extra},...guard?{guard}:{}}),out:"",exited:null},keep=chunk=>{run7.out=(run7.out+chunk.toString("utf8")).slice(-6e3)};return run7.child.stdout.on("data",keep),run7.child.stderr.on("data",keep),run7.child.on("error",err2=>{run7.out+=`
442
442
  ${err2.message}`,run7.exited=-1}),run7.child.on("exit",code=>run7.exited=code??0),run7},app=launch2(homes.env(homes.before)),healed=null,stop=()=>{endGroup(app.child);let at4=portOf(target);if(!at4||stopping.has(at4))return;let ended=app,gone=goneFrom(at4,()=>ended.exited!==null).finally(()=>stopping.delete(at4));stopping.set(at4,gone)};signal2?.addEventListener("abort",stop,{once:!0});let busy=port?` ${new URL(url).host} is used by another program (maybe your own "npm run dev"), so squad-kit tried port ${port}${own2.length?` (set as PORT and ${own2.join(", ")})`:""}.`:"",until=Date.now()+waitMs;try{for(let tries=0;Date.now()<until;tries+=1){if(signal2?.aborted)throw stop(),new Error(STOPPED2);if(app.exited!==null){let refused=healed?[]:homes.refused(app.out),home2=refused.length?homes.env([...homes.before,...refused]):null;if(!home2)throw new Error(`The app stopped while starting (${command}):${busy} ${tail(app.out,8)}`);let detail=homes.said(refused,home2);more3.onHealed?more3.onHealed(detail):writeLog({level:"info",text:detail}),endGroup(app.child),healed=refused,app=launch2(home2);continue}if(await answers(target)){if(signal2?.aborted)throw stop(),new Error(STOPPED2);healed&&homes.learn(app.out,healed);let up=app;return{url:target,stop,lines:(n=20)=>tail(up.out.trim(),n),ended:()=>up.exited!==null}}await new Promise(r=>setTimeout(r,Math.min(2e3,250*2**tries)))}}finally{signal2?.removeEventListener("abort",stop)}throw stop(),new Error(port?`The app did not start on a free port.${busy} Stop the other program, then press Try again. (${command}): ${tail(app.out,8)}`:`The app did not answer at ${url} within ${Math.round(waitMs/6e4)} minutes (${command}): ${tail(app.out,8)}`)}var SUMMARY_LINES=[/^\s*Tests:?\s+(?:.*\D)?(\d+)\s+passed\b/i,/^\s*(\d+)\s+pass(?:ing)?\b/i,/^\s*[#ℹ]\s*pass\s+(\d+)\s*$/,/^\s*(?:(?:Passed|Failed)!.*\b)?Passed:\s+(\d+)/,/^[=\s]*(?:\d+\s+\w+,\s+)*(\d+)\s+passed\b.*\bin\s+[\d.]+\s*s/i,/^\s*(\d+)\s+passed\s+\([\d.]+\s*(?:ms|s|m|h)\)\s*$/i,/^test result: \w+\.\s+(\d+)\s+passed;/];function plainOutput(output){return output.replace(/\x1b\[[0-9;]*m/g,"")}function testCount(output){let text3=plainOutput(output),total=null;for(let line3 of text3.split(`
443
443
  `)){let m2=SUMMARY_LINES.map(p=>line3.match(p)).find(Boolean);m2&&(total=(total??0)+Number(m2[1]))}if(total!==null)return total;let m=text3.match(/(\d+)\s+passing/i)??text3.match(/Passed:\s+(\d+)/i)??text3.match(/(\d+)\s+passed/i);return m?Number(m[1]):null}function tail(output,lines3=40){return output.split(`
444
444
  `).slice(-lines3).join(`
@@ -675,7 +675,7 @@ ${ideas.join(`
675
675
  `)),out20}function blocks(text3){let out20=[];for(let l of text3.split(`
676
676
  `)){let m=/^### (.+)$/.exec(l);m?out20.push({head:dec(m[1].trim()),lines:[]}):out20.length&&out20[out20.length-1].lines.push(l)}return out20}var items2=(text3="")=>text3.split(`
677
677
  `).filter(l=>l.startsWith("- ")).map(l=>unline(l.slice(2).trim())).filter(Boolean),prose=lines3=>unline(lines3.filter(l=>l.trim()&&!/^- (Id|Kind|Files|Links to|Ticket): /.test(l)).join(" ").trim()),fact=(lines3,key)=>lines3.find(l=>l.startsWith(`- ${key}: `))?.slice(key.length+4).trim();function parseOverviewDoc(text3){let{header,body:body10}=splitHeader(text3),s=sectionsOf(body10),o=emptyOverview();typeof header.readAt=="string"&&(o.readAt=header.readAt),typeof header.refreshedAt=="string"&&(o.refreshedAt=header.refreshedAt),header.approved&&typeof header.approved.by=="string"&&(o.approved={by:header.approved.by,at:String(header.approved.at??"")});let users=unline((s.get("Who uses it")??"").trim());users&&(o.users=users),o.can=items2(s.get("What it can do today")),o.worth=items2(s.get("Worth knowing"));for(let b of blocks(s.get("How it fits together")??"")){let id2=dec(fact(b.lines,"Id")??b.head),kind=PART_KINDS.find(k=>k===fact(b.lines,"Kind")),files=fact(b.lines,"Files")?.split(" | ").map(f=>dec(f.trim())).filter(Boolean)??[];o.parts.push({id:id2,name:b.head,what:prose(b.lines),files,...kind?{kind}:{}});for(let to of fact(b.lines,"Links to")?.split(" | ")??[]){let m=/^(.*?)(?: \((.*)\))?$/.exec(to.trim());m?.[1]&&o.links.push({from:id2,to:dec(m[1].trim()),...m[2]?{label:m[2]}:{}})}}for(let b of blocks(s.get("Good first tickets")??"")){let ticket=fact(b.lines,"Ticket");o.ideas.push({title:b.head,why:prose(b.lines),...ticket?{ticket:dec(ticket)}:{}})}return o}function readOverview(bp){try{return parseOverviewDoc(fs92.readFileSync(overviewFile(bp),"utf8"))}catch{return emptyOverview()}}function writeOverview(bp,o){let file3=overviewFile(bp);fs92.mkdirSync(path87.dirname(file3),{recursive:!0});let tmp=`${file3}.${process.pid}.tmp`;fs92.writeFileSync(tmp,overviewDoc(o)),fs92.renameSync(tmp,file3)}function hasOverview(bp){return fs92.existsSync(overviewFile(bp))}function cleanOverview(a){let texts=(v,max)=>Array.isArray(v)?v.filter(x=>typeof x=="string"&&!!x.trim()).map(x=>x.trim()).slice(0,max):[],parts2=[],seen=new Set;for(let p of Array.isArray(a.parts)?a.parts:[]){if(!p||typeof p!="object")continue;let r=p,name=typeof r.name=="string"?r.name.trim():"",id2=(typeof r.id=="string"&&r.id.trim()?r.id.trim():name).slice(0,60);if(!name||!id2||seen.has(id2))continue;seen.add(id2);let kind=PART_KINDS.find(k=>k===r.kind);if(parts2.push({id:id2,name,what:typeof r.what=="string"?r.what.trim():"",files:texts(r.files,4),...kind?{kind}:{}}),parts2.length>=10)break}let links=[];for(let l of Array.isArray(a.links)?a.links:[]){if(!l||typeof l!="object")continue;let r=l;typeof r.from!="string"||typeof r.to!="string"||r.from===r.to||!seen.has(r.from)||!seen.has(r.to)||links.some(x=>x.from===r.from&&x.to===r.to)||links.push({from:r.from,to:r.to,...typeof r.label=="string"&&r.label.trim()?{label:r.label.trim().slice(0,30)}:{}})}let ideas=[];for(let i of Array.isArray(a.ideas)?a.ideas:[]){if(!i||typeof i!="object")continue;let r=i;if(!(typeof r.title!="string"||!r.title.trim())&&(ideas.push({title:r.title.trim().slice(0,120),why:typeof r.why=="string"?r.why.trim():""}),ideas.length>=3))break}return{...typeof a.users=="string"&&a.users.trim()?{users:a.users.trim()}:{},can:texts(a.can,8),worth:texts(a.worth,6),ideas,parts:parts2,links:links.slice(0,20)}}function overviewAfterMerge(bp,at3=new Date().toISOString()){hasOverview(bp)&&writeOverview(bp,{...readOverview(bp),refreshedAt:at3})}import fs99 from"fs";import path91 from"path";import yaml14 from"js-yaml";function ownerOf(t){return t.owner?.trim()||t.createdBy}function sameName(a,b){return!!a?.trim()&&a.trim().toLowerCase()===b?.trim().toLowerCase()}function isMine(t,meta){return sameName(ownerOf(t),meta.user?.name)}import fs93 from"fs";function isMerged(t){return!!t.mergedAt||t.stage==="done"}function openOnBoard(bp,self){return listTickets(bp).filter(t=>t.id!==self&&!isMerged(t)&&!t.archived&&!t.deleting)}function unmergedDeps(bp,t){return(t.dependsOn??[]).map(id2=>readTicket(bp,id2)).filter(d=>!!(d&&!isMerged(d)&&!d.deleting))}function depsReady(bp,ids){return ids.every(id2=>{if(ticketProblem(bp,id2))return!1;let d=readTicket(bp,id2);return!d||d.deleting||isMerged(d)})}function names(deps){return deps.map(d=>typeof d=="string"?d:d.id).join(" and ")}function dependsLine(t,deps){return`${t.id} needs ${names(deps)}, which ${deps.length===1?"is":"are"} not merged yet`}function waitLine(deps){return`Waits for ${names(deps)} to merge \xB7 starts by itself`}function hasCode(d){return!!(d.worktree||d.branch)}function reaches2(bp,from,to,seen=new Set){return from===to?!0:seen.has(from)?!1:(seen.add(from),(readTicket(bp,from)?.dependsOn??[]).some(x=>reaches2(bp,x,to,seen)))}function designDepends(bp,t,named){let ids=[...new Set((named??[]).map(x=>x.trim().toUpperCase()).filter(Boolean))].filter(id2=>id2!==t.id&&readTicket(bp,id2)&&!reaches2(bp,id2,t.id));return ids.length?ids:void 0}function holdForDependency(bp,t){let deps=unmergedDeps(bp,t);if(!deps.length)return!1;let c=t.dependency,covered=!!(c&&deps.every(d=>c.on.includes(d.id)));if(covered&&c.way!=="wait")return!1;if(t.live=!1,t.squad=null,covered)return t.waiting=null,t.status=waitLine(deps),!0;let line3=dependsLine(t,deps);return t.track==="auto"?(t.dependency={on:deps.map(d=>d.id),way:"wait",by:"the squad",at:now()},t.waiting=null,t.status=waitLine(deps),addActivity(t,"squad",`${line3}. Builds by itself: it waits, and the build starts by itself once ${deps.length===1?"it merges":"they merge"}`),!0):(t.waiting={kind:"depends",summary:line3},t.status=line3,addActivity(t,"squad",`${line3}, so the build waits for you to choose how to build`),!0)}function holdBuild(bp,id2){let t=readTicket(bp,id2);if(!t||t.worktree&&fs93.existsSync(t.worktree)||!unmergedDeps(bp,t).length)return!1;let held4=!1;return updateTicket(bp,id2,x=>{held4=holdForDependency(bp,x)}),held4}function waitsForDependency(t){return t.waiting?.kind==="depends"||t.dependency?.way==="wait"&&!t.dependency.started&&!t.squad&&!t.waiting}var WAY_TEXT={branch:on=>`Chose to build on ${on}'s branch: its work so far comes into this ticket's copy`,anyway:(on,base2)=>`Chose to build anyway, from ${base2} without ${on}. It stays on the ticket's record`,wait:on=>`Chose to wait for ${on}: the build starts by itself once it is merged`};function chooseBuild(bp,id2,way){if(!WAY_TEXT[way])throw new BoardActionError("Pick how to build: on its branch, anyway, or wait.");let meta=readMeta(bp);return updateTicket(bp,id2,t=>{let failed3=t.waiting?.kind==="stuck"&&!!t.dependency&&!(t.worktree&&fs93.existsSync(t.worktree));if(!waitsForDependency(t)&&!failed3)throw new BoardActionError(`${t.id} is not waiting for a ticket it depends on.`);failed3&&(t.stuck=void 0,t.retryPhase=void 0);let base2=repoFor(meta,t).base,deps=unmergedDeps(bp,t);if(!deps.length){t.waiting=null,t.dependency=t.dependency?{...t.dependency,started:now()}:void 0,startBuild(t,"Building on the newest code",meta.speed),addActivity(t,"squad","What it needs is merged now, so it builds on the newest code");return}if(way==="branch"){if(deps.length>1)throw new BoardActionError(`${t.id} needs ${names(deps)}. A copy can start from one branch only: build anyway, or wait for them.`);let d=deps[0];if(meta.engine==="live"&&!hasCode(d))throw new BoardActionError(`${d.id} has no code yet to build on. Wait for it, or build anyway.`);if(repoFor(meta,d).path!==repoFor(meta,t).path)throw new BoardActionError(`${d.id} changes another repo, so ${t.id} cannot start from its branch. Build anyway, or wait for it.`)}let on=names(deps);if(t.dependency={on:deps.map(d=>d.id),way,by:meta.user.name,at:now()},t.waiting=null,addActivity(t,"you",WAY_TEXT[way](on,base2)),way==="wait"){t.live=!1,t.squad=null,t.status=waitLine(deps);return}startBuild(t,way==="branch"?`Building on ${on}'s branch`:`Building without ${on}`,meta.speed)})}function startBuild(t,status,speed){t.stage="implement",t.live=!0,t.status=status,scheduleSquad(t,"build",1,speed,0)}function wakeIfReady(bp,t){let c=t.dependency;if(c?.way!=="wait"||c.started||t.squad||t.waiting||t.deleting||!depsReady(bp,c.on))return!1;let meta=readMeta(bp),base2=repoFor(meta,t).base,gone=c.on.filter(id2=>!readTicket(bp,id2));return updateTicket(bp,t.id,x=>{x.dependency={...c,started:now()},startBuild(x,`${names(c.on)} merged \xB7 building now`,meta.speed),addActivity(x,"squad",gone.length?`${names(gone)} left the board, so the build starts now, on the newest code`:`${names(c.on)} ${c.on.length===1?"is":"are"} merged, so the build starts now, on the newest code`)}),!0}function setDependsOn(bp,id2,on){let ids=[...new Set(on.map(x=>x.trim().toUpperCase()).filter(Boolean))],meta=readMeta(bp);return updateTicket(bp,id2,t=>{if(isMerged(t))throw new BoardActionError(`${t.id} is merged already, so nothing holds it up.`);for(let d of ids){if(d===t.id)throw new BoardActionError("A ticket cannot depend on itself.");if(!readTicket(bp,d))throw new BoardActionError(`${d} is not on the board.`);if(reaches2(bp,d,t.id))throw new BoardActionError(`${d} depends on ${t.id} already, so ${t.id} cannot depend on it: they would wait for each other.`)}t.dependsOn=ids.length?ids:void 0,addActivity(t,"you",ids.length?`Set what it depends on: ${names(ids)}`:"Said it depends on no other ticket");let before=t.squad?.phase==="build"&&!(t.worktree&&fs93.existsSync(t.worktree));(waitsForDependency(t)||before)&&!holdForDependency(bp,t)&&!before&&(t.waiting=null,startBuild(t,"Building on the newest code",meta.speed))})}function refuseBeforeDependency(bp,t){if(t.dependency?.way!=="branch")return;let waiting2=t.dependency.on.map(x=>readTicket(bp,x)).filter(d=>!!(d&&!isMerged(d)&&!d.deleting));if(!waiting2.length)return;let base2=repoFor(readMeta(bp),t).base;throw new BoardActionError(`${t.id} is built on ${names(waiting2)}'s branch, so it carries ${waiting2.length===1?"its":"their"} work too. Accept ${names(waiting2)} first: then ${t.id} brings in the newest code and goes in after it.`)}function bringInLatest(bp,id2){let meta=readMeta(bp);return updateTicket(bp,id2,t=>{let base2=repoFor(meta,t).base;if(isMerged(t)||t.stage==="release")throw new BoardActionError(`${t.id} is accepted already: it goes into ${base2} as it is.`);let copy2=meta.engine!=="live"||!!(t.worktree&&fs93.existsSync(t.worktree));if(!["implement","test","review"].includes(t.stage)||!copy2)throw new BoardActionError(`${t.id} is not being built yet. Its copy starts from the newest code when the build starts.`);if(t.takenOver)throw new BoardActionError(`You have ${t.id}. Give it back first.`);if(t.chat?.open&&!t.chat.beside)throw new BoardActionError(`${t.id} has a chat open. Finish it first.`);if(t.waiting&&t.waiting.kind!=="proof"&&t.waiting.kind!=="stuck")throw new BoardActionError(`${t.id} waits for you: ${t.waiting.summary}. Do that first.`);if(meta.engine!=="live"){addActivity(t,"you","Brought in the newest code. The sample board has no code, so nothing changed");return}if(!t.waiting&&(t.live||t.squad)){t.baseWanted={by:meta.user.name,at:now()},addActivity(t,"you","Asked to bring in the newest code: it comes in when the running step ends, before the checks");return}t.waiting=null,t.stuck=void 0,t.retryPhase=void 0,t.stage="implement",t.live=!0,t.status="Bringing in the newest code",addActivity(t,"you","Asked to bring in the newest code"),scheduleSquad(t,"update-base",.2,1)})}import fs95 from"fs";import fs94 from"fs";import path88 from"path";var same=(a,b)=>a.role===b.role&&a.step===b.step&&a.level===b.level&&a.model===b.model;function addTo(t,u,runs2){let sum=k=>{let v=u?.[k];typeof v=="number"&&(t[k]=(t[k]??0)+v)};t.runs+=runs2,sum("inputTokens"),sum("cachedTokens"),sum("outputTokens"),sum("costUsd");let total=u&&"runs"in u?u:null,noTokens=total?total.tokensUnknown??0:u?.inputTokens===void 0&&u?.outputTokens===void 0?1:0,noCost=total?total.costUnknown??0:u?.costUsd===void 0?1:0;noTokens&&(t.tokensUnknown=(t.tokensUnknown??0)+noTokens),noCost&&(t.costUnknown=(t.costUnknown??0)+noCost)}function addRun(lines3,key,usage){let all=(lines3??[]).map(l=>({...l})),line3=all.find(l=>same(l,key));return line3||(line3={...key,runs:0},all.push(line3)),addTo(line3,usage,1),all}function totalOf(lines3){let t={runs:0};for(let l of lines3??[])addTo(t,l,l.runs);return t}var ROLE_ORDER=["analyst","architect","builder","reviewer","tester"];function byRole(lines3){let out20=[];for(let role of ROLE_ORDER){let mine2=(lines3??[]).filter(l=>l.role===role);if(!mine2.length)continue;let levels=[];for(let l of mine2){let row3=levels.find(x=>x.level===l.level&&x.model===l.model);row3||levels.push(row3={level:l.level,model:l.model,steps:[],total:{runs:0}}),row3.steps.includes(l.step)||row3.steps.push(l.step),addTo(row3.total,l,l.runs)}out20.push({role,total:totalOf(mine2),levels})}return out20}function recordUsage(bp,ticketId,key,usage){addToMonth(bp,usage);try{let meta=readMeta(bp);if(ticketId){updateTicket(bp,ticketId,t=>{t.usage=addRun(t.usage,key,usage),t.quality=meta.quality??DEFAULT_QUALITY});return}writeMeta(bp,{...meta,usage:addRun(meta.usage,key,usage)})}catch{}}function monthKey(at3=Date.now()){let d=new Date(at3);return`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}`}function monthsFile(bp){return path88.join(bp.localDir,"usage-months.json")}function readMonths(bp){try{let raw=JSON.parse(fs94.readFileSync(monthsFile(bp),"utf8"));return!raw||typeof raw!="object"?{}:Object.fromEntries(Object.entries(raw).filter(([k,v])=>/^\d{4}-\d{2}$/.test(k)&&v&&typeof v=="object"&&typeof v.runs=="number"))}catch{return{}}}function addToMonth(bp,usage,at3=Date.now()){try{let months=readMonths(bp),key=monthKey(at3),t=months[key]??{runs:0};addTo(t,usage,1),months[key]=t,fs94.mkdirSync(path88.dirname(monthsFile(bp)),{recursive:!0});let tmp=`${monthsFile(bp)}.${process.pid}.tmp`;fs94.writeFileSync(tmp,`${JSON.stringify(months,null,1)}
678
- `),fs94.renameSync(tmp,monthsFile(bp))}catch{}}function boardUsage(bp){let meta=readMeta(bp),all=listTickets(bp),tickets=all.filter(t=>t.usage?.length),project=meta.usage??[],gone=(meta.deleted??[]).filter(d=>d.usage?.length&&!all.some(t=>t.id===d.id&&t.deleting?.at===d.at)),deleted=gone.flatMap(d=>d.usage),lines3=[...tickets.flatMap(t=>t.usage),...project,...deleted];return{total:totalOf(lines3),project:totalOf(project),roles:byRole(lines3),tickets:tickets.length,deleted:{tickets:gone.length,total:totalOf(deleted)}}}var MORE_STEPS={chat:"Talking it through",draft:"Filling in a new ticket"};function stepWords(role,step2){return STEPS[role]?.find(s=>s.id===step2)?.name??MORE_STEPS[step2]??step2}function costWords(t){if(t.costUsd===void 0)return null;let amount=t.costUsd<.005?"under $0.01":`$${t.costUsd.toFixed(2)}`;return t.costUnknown?`${amount}, ${t.costUnknown} ${t.costUnknown===1?"run":"runs"} not reported`:amount}function shortNumber(n){return n>=1e6?`${(n/1e6).toFixed(n>=1e7?0:1)}M`:n>=1e3?`${(n/1e3).toFixed(n>=1e4?0:1)}k`:String(n)}function tokenWords(t){return t.inputTokens===void 0&&t.outputTokens===void 0?null:[t.inputTokens!==void 0?`read ${shortNumber(t.inputTokens)} tokens`:"",t.outputTokens!==void 0?`wrote ${shortNumber(t.outputTokens)}`:""].filter(Boolean).join(", ")}var WARN_AT=.8;function share(spent,limit){return limit&&limit>0&&spent!==void 0?spent/limit:void 0}function budgetState(bp,t,budget=readMeta(bp).budget,at3=Date.now()){let monthSpent=readMonths(bp)[monthKey(at3)]?.costUsd??0,ticketSpent=t?totalOf(t.usage).costUsd??0:void 0,ticketShare=t?share(ticketSpent,budget?.ticketUsd):void 0,monthShare=share(monthSpent,budget?.monthUsd),warn2=[],over=[];for(let[kind,s]of[["ticket",ticketShare],["month",monthShare]])s!==void 0&&(s>=1?over.push(kind):s>=WARN_AT&&warn2.push(kind));return{...budget?.ticketUsd?{ticketUsd:budget.ticketUsd}:{},...budget?.monthUsd?{monthUsd:budget.monthUsd}:{},...ticketSpent!==void 0?{ticketSpent}:{},monthSpent,...ticketShare!==void 0?{ticketShare}:{},...monthShare!==void 0?{monthShare}:{},warn:warn2,over}}function parseUsd(v){if(v==null)return null;let s=String(v).trim().replace(/^\$/,"").replace(/\s*(usd|dollars?)$/i,"");if(!s||/^(off|none|no)$/i.test(s))return null;let n=Number(s);if(!Number.isFinite(n)||n<0)throw new Error(`"${v}" is not an amount. Write it in US dollars, like 5 or 2.50, or off.`);return n===0?null:Math.round(n*100)/100}function setBudget(bp,change){let meta=readMeta(bp),next={...meta.budget};for(let k of["ticketUsd","monthUsd"]){let v=change[k];v===null?delete next[k]:v!==void 0&&(next[k]=v)}return writeMeta(bp,{...meta,budget:Object.keys(next).length?next:void 0}),next}var NO_AI=new Set(["test","express-approve","accept","publish"]),usd=n=>`$${n.toFixed(2)}`;function carriedOn(t){let s=t.stuck;return!s?.budget||t.waiting?null:s.options.some(o=>o.chosen)?s.budget:null}async function budgetStop(bp,id2,phase,payment,at3=Date.now()){if(NO_AI.has(phase))return!1;let budget=readMeta(bp).budget;if(!budget?.ticketUsd&&!budget?.monthUsd)return!1;let t=readTicket(bp,id2);if(!t)return!1;let yes=carriedOn(t);if(yes&&(t=updateTicket(bp,id2,x=>{x.budgetOk={...x.budgetOk,...yes==="ticket"?{ticket:!0}:{month:monthKey(at3)}},x.stuck&&delete x.stuck.budget})),await payment()!=="api-key")return!1;let state=budgetState(bp,t,budget,at3),over=state.over.filter(k=>k==="ticket"?!t.budgetOk?.ticket:t.budgetOk?.month!==monthKey(at3));if(!over.length)return!1;let kind=over[0],what=kind==="ticket"?`${t.id} used ${usd(state.ticketSpent??0)} of its ${usd(state.ticketUsd??0)} budget.`:`The squad used ${usd(state.monthSpent)} of this month's ${usd(state.monthUsd??0)} budget for ${readMeta(bp).project}.`,copy2=!!(t.worktree&&fs95.existsSync(t.worktree));return updateTicket(bp,id2,x=>{x.live=!1,x.squad=null,x.retryPhase=phase,x.status=kind==="ticket"?"Paused at its budget":"Paused at this month's budget",x.waiting={kind:"stuck",summary:kind==="ticket"?"Reached its budget and waits for you":"Reached this month's budget and waits for you"},x.stuck={budget:kind,question:`${what} squad-kit paused it before the next step, so no step was cut in half. Carry on, or raise the budget in Settings first.`,options:[{do:"retry",name:"Carry on",note:kind==="ticket"?"The squad finishes this ticket and does not stop it for its budget again.":"The squad goes on with this ticket this month."},...copy2?[{do:"yourself",name:"Do the rest yourself",note:"Open the ticket's copy in your editor and finish it. Then the checks, review and proof run again."}]:[],{do:"plan",name:"Send it back to the plan",note:"A smaller plan uses less. The Architect rewrites it with what is done so far."}]},addActivity(x,"squad",`Paused before the next step: ${what}`)}),!0}function accepted(t){return t.stage==="release"||t.stage==="done"||!!t.mergedAt}function workSeconds(t){if(t.history?.length)return t.history.reduce((s,h)=>s+Math.max(0,(Date.parse(h.to)-Date.parse(h.from))/1e3),0)}function median(xs){let s=[...xs].sort((a,b)=>a-b),m=Math.floor(s.length/2);return s.length%2?s[m]:(s[m-1]+s[m])/2}function partWords(r){return r>=.85?"about as much as":r>=.58?"about two thirds of":r>=.42?"about half of":r>=.29?"about a third of":r>=.21?"about a quarter of":r>=.15?"about a fifth of":r>=.07?"about a tenth of":"less than a tenth of"}function sizeOfWork(t,all,project){let mine2=totalOf(t.usage).costUsd,others=all.filter(x=>x.id!==t.id&&accepted(x)).map(x=>totalOf(x.usage).costUsd).filter(c=>typeof c=="number"&&c>0);if(mine2===void 0||others.length<2)return null;let usual=median(others),times=mine2/usual,size2=times<.6?"small":times<=1.6?"medium":"large",words2=times>=1.75?`About ${Math.round(times)} times a usual ticket in ${project}.`:times>=1.25?`About one and a half times a usual ticket in ${project}.`:times>=.85?`About the same as a usual ticket in ${project}.`:`${cap(partWords(times))} a usual ticket in ${project}.`;return{size:size2,times,words:words2,from:others.length}}function cap(s){return s.charAt(0).toUpperCase()+s.slice(1)}function and(xs){return xs.length<=1?xs[0]??"":`${xs.slice(0,-1).join(", ")} and ${xs.at(-1)}`}var runsOf=(t,step2)=>(t.usage??[]).filter(l=>l.step===step2).reduce((n,l)=>n+l.runs,0);function sizeWhy(t){let parts2=[],q5=t.questions?.length??0;q5&&parts2.push(`${q5} ${q5===1?"question":"questions"}`),t.track==="express"?parts2.push("the Express track"):t.track==="auto"?parts2.push("a design written from the breakdown"):(t.decision||t.steps?.length)&&parts2.push("a design"),t.mockup&&parts2.push("a mockup");let fixes=runsOf(t,"fix");fixes&&parts2.push(fixes===1?"one round of fixes":`${fixes} rounds of fixes`);let reviews=runsOf(t,"review");return reviews===2?parts2.push("a second review"):reviews>2&&parts2.push(`${reviews} reviews`),parts2.length?`It had ${and(parts2)}.`:""}var GROUP={questions:"Questions and spec",spec:"Questions and spec",scan:"Reading the project",blueprint:"Reading the project",design:"Design",build:"Build",fix:"Fixes",review:"Review",proof:"Screenshots",chat:"Talking it through",draft:"Filling in the ticket"};function whereItWent(lines3){let all=lines3??[],byCost=all.length>0&&all.every(l=>l.costUsd!==void 0&&!l.costUnknown),amount=l=>byCost?l.costUsd??0:(l.inputTokens??0)+(l.outputTokens??0),groups=new Map;for(let l of all){let name=GROUP[l.step]??l.step,g=groups.get(name)??{amount:0,cost:0};g.amount+=amount(l),g.cost+=l.costUsd??0,groups.set(name,g)}let sum=[...groups.values()].reduce((s,g)=>s+g.amount,0),parts2=[...groups.entries()].map(([name,g])=>({name,share:sum>0?g.amount/sum:0,...byCost?{costUsd:g.cost}:{}})).filter(p=>p.share>0).sort((a,b)=>b.share-a.share);return{by:byCost?"cost":"tokens",parts:parts2}}var STEP_WORD={questions:"questions",spec:"spec",scan:"reading the project",blueprint:"reading the project",design:"design",build:"the build",fix:"fixes",review:"review",proof:"screenshots",chat:"talking it through",draft:"filling in the ticket"};function engineerDetails(lines3){let byModel=new Map;for(let l of lines3??[]){let model=l.model||`${l.level} level`,steps=byModel.get(model)??[],w=STEP_WORD[l.step]??l.step;steps.includes(w)||steps.push(w),byModel.set(model,steps)}let models=[...byModel.entries()].map(([m,steps])=>`${m} for ${and(steps)}`).join(" \xB7 "),t=totalOf(lines3),tokens=t.inputTokens===void 0&&t.outputTokens===void 0?null:[t.inputTokens!==void 0?`${shortNumber(t.inputTokens)} tokens read${t.cachedTokens&&t.inputTokens?` (${Math.round(100*t.cachedTokens/t.inputTokens)}% from cache)`:""}`:"",t.outputTokens!==void 0?`${shortNumber(t.outputTokens)} written`:""].filter(Boolean).join(", ");return{models,tokens}}function expressTip(t,all){if(t.track==="express"||t.track==="auto")return null;let mine2=totalOf(t.usage).costUsd,x=all.filter(o=>o.id!==t.id&&o.track==="express"&&accepted(o)&&(totalOf(o.usage).costUsd??0)>0).sort((a,b)=>(b.mergedAt??b.createdAt).localeCompare(a.mergedAt??a.createdAt))[0];if(x&&mine2&&mine2>0){let r=totalOf(x.usage).costUsd/mine2;if(r<.85)return`The Express track used ${partWords(r)} this for ${x.title} (${x.id}). For a small change like that, pick Express when you make the ticket.`}return"For a small change, like a typo or one word on a button, pick the Express track when you make the ticket. It skips the questions and the plan approval, so it uses much less."}function monthTotal(bp,at3=Date.now()){let months=readMonths(bp),month=monthKey(at3),first2=Object.keys(months).sort()[0];return{month,total:months[month]??{runs:0},...first2?{since:first2}:{}}}function ticketCost(bp,t,payment,at3=Date.now()){let all=listTickets(bp),meta=readMeta(bp),seconds2=workSeconds(t);return{payment,project:meta.project,total:totalOf(t.usage),size:sizeOfWork(t,all,meta.project),why:sizeWhy(t),where:whereItWent(t.usage),month:monthTotal(bp,at3),...payment.payment==="api-key"?{budget:budgetState(bp,t,meta.budget,at3)}:{},tip:expressTip(t,all),details:engineerDetails(t.usage),...seconds2!==void 0?{seconds:seconds2}:{}}}var QUALITY_AFTER=3;function qualityStats(bp){let done2=listTickets(bp).filter(t=>accepted(t)&&t.quality);return QUALITIES.map(quality=>{let mine2=done2.filter(t=>t.quality===quality),costs=mine2.map(t=>totalOf(t.usage).costUsd).filter(c=>typeof c=="number"),times=mine2.map(workSeconds).filter(s=>typeof s=="number"),avg=xs=>xs.reduce((a,b)=>a+b,0)/xs.length;return{quality,tickets:mine2.length,...costs.length?{costUsd:avg(costs)}:{},...times.length?{seconds:avg(times)}:{}}})}function costOverview(bp,payment,at3=Date.now()){let meta=readMeta(bp),budget=meta.budget??{},warnings=[];if(payment.payment==="api-key"&&(budget.ticketUsd||budget.monthUsd)){let month=budgetState(bp,null,budget,at3);if(month.monthShare!==void 0&&month.monthShare>=WARN_AT&&warnings.push({kind:"month",spent:month.monthSpent,limit:budget.monthUsd,share:month.monthShare,over:month.monthShare>=1}),budget.ticketUsd)for(let t of listTickets(bp).filter(x=>!accepted(x)&&!x.archived&&!x.deleting)){let s=budgetState(bp,t,budget,at3);s.ticketShare!==void 0&&s.ticketShare>=WARN_AT&&!t.budgetOk?.ticket&&warnings.push({kind:"ticket",id:t.id,title:t.title,spent:s.ticketSpent??0,limit:budget.ticketUsd,share:s.ticketShare,over:s.ticketShare>=1})}}return{payment,project:meta.project,month:monthTotal(bp,at3),budget,warnings,quality:qualityStats(bp),qualityAfter:QUALITY_AFTER}}var KEEP_MS=10*6e4,found=new Map;function detect(tool5,now3){let seen=found.get(tool5.name);if(seen&&now3-seen.at<KEEP_MS)return seen.value;let value=(tool5.billing?.()??Promise.resolve(null)).catch(()=>null);return found.set(tool5.name,{at:now3,value}),value}function builderTool(bp){try{return aiTool(readAgent(bp,"builder").tool)}catch{return AI_TOOLS[0]}}async function howPaid(bp,opts={}){let tool5=opts.tool??builderTool(bp),choice=paymentChoice(),billing=tool5?await detect(tool5,opts.now??Date.now()):null;return{payment:choice??billing?.kind??"subscription",from:choice?"you":billing?"found":"guess",found:billing,tool:tool5?.label??"AI tool",name:tool5?.plan??tool5?.label??"AI"}}var STATES2=["New","Active","Resolved","Closed"],STAGE_AT={"not-started":0,"in-progress":1,resolved:2,closed:3},DemoTracker=class{name;label;abilities={sprints:!1,wiki:!1,bugs:!0,tags:!0,create:!0};identity;items=new Map;log=[];nextId;lv;prefix;baseUrl;delayMs;failing=null;constructor(opts={}){this.name=opts.name??"azure",this.label=opts.label??"Azure DevOps",this.prefix=opts.prefix??"AB#",this.lv={epic:"Epic",feature:"Feature",story:"User Story",process:"Agile",...opts.levels},this.nextId=opts.start??120,this.baseUrl=opts.baseUrl??"",this.delayMs=opts.delayMs??0,this.identity=`demo/${this.name}`}breakAfter(writes,message=`${this.label} did not answer`){this.failing={left:writes,message}}mend(){this.failing=null}remember(items3){for(let i of items3)this.items.has(i.id)||this.items.set(i.id,{id:i.id,type:i.type,title:i.title,state:"New",description:"",acceptance:"",tags:[],...i.parent?{parent:i.parent}:{},dependsOn:[],comments:[]}),/^\d+$/.test(i.id)&&(this.nextId=Math.max(this.nextId,Number(i.id)+1))}parseRef(text3){let id2=text3.trim().replace(new RegExp(`^${this.prefix.replace(/[^\w]/g,"\\$&")}`,"i"),"").replace(/^#/,"");return/^\d+$/.test(id2)&&Number(id2)>0?id2:null}refOf(id2){return`${this.prefix}${id2}`}async whoAmI(){return"Demo"}async sprints(){return[]}async list(opts={}){return[...this.items.values()].filter(w=>w.type!==this.lv.epic&&w.type!==this.lv.feature&&(opts.includeDone||w.state!=="Closed")).map(w=>this.row(w))}async get(id2){let w=this.item(id2),parents=[];for(let p=w.parent?this.items.get(w.parent):void 0;p&&parents.length<2;p=p.parent?this.items.get(p.parent):void 0)parents.push({id:p.id,type:p.type,title:p.title});return{...this.row(w),description:w.description,acceptanceCriteria:w.acceptance,reproSteps:"",parents,dependsOn:w.dependsOn.map(d=>this.item(d)).map(d=>({ref:this.refOf(d.id),title:d.title,state:d.state})),attachments:[]}}async download(){throw new Error("The demo tracker keeps no files.")}async wikiPages(){return[]}async moveTo(id2,stage){let w=this.item(id2);return STAGE_AT[stage]<=STATES2.indexOf(w.state)?null:(await this.write(`move ${id2} ${STATES2[STAGE_AT[stage]]}`),w.state=STATES2[STAGE_AT[stage]],w.state)}async comment(id2,markdown){let w=this.item(id2);await this.write(`comment ${id2}`),w.comments.push(markdown)}async addTag(id2,tag){let w=this.item(id2);w.tags.includes(tag)||(await this.write(`tag ${id2} ${tag}`),w.tags.push(tag))}async fileBug(input7){return await this.write("create Bug"),this.row(this.add({type:"Bug",title:input7.title,description:input7.steps,tags:input7.tags}))}async levels(){return{...this.lv}}async createItem(input7){input7.parent&&this.item(input7.parent);let type=this.lv[input7.level];await this.write(`create ${type} ${this.nextId}`);let w=this.add({type,title:input7.title,description:input7.markdown,acceptance:input7.acceptance??"",tags:input7.tags??[],parent:input7.parent});return this.row(w)}async findMade(input7){let type=this.lv[input7.level],hit=[...this.items.values()].reverse().find(w=>w.type===type&&w.tags.includes(input7.tag)&&w.description.includes(input7.mark));return hit?this.row(hit):null}async updateItem(id2,change){let w=this.item(id2);change.parent&&this.item(change.parent),await this.write(`update ${id2}`),change.title!==void 0&&(w.title=change.title),change.markdown!==void 0&&(w.description=change.markdown),change.acceptance!==void 0&&(w.acceptance=change.acceptance),change.parent!==void 0&&(w.parent=change.parent)}async linkDepends(id2,on){let w=this.item(id2),missing3=[...new Set(on)].filter(d=>d!==id2&&!w.dependsOn.includes(d));for(let d of missing3)this.item(d);missing3.length&&(await this.write(`link ${id2} ${missing3.join(" ")}`),w.dependsOn.push(...missing3))}item(id2){let w=this.items.get(String(id2));if(!w)throw new Error(`${this.refOf(String(id2))} is not in ${this.label}.`);return w}add(fields){let id2=String(this.nextId++),w={id:id2,state:"New",description:"",acceptance:"",tags:[],dependsOn:[],comments:[],...fields};return w.parent||delete w.parent,this.items.set(id2,w),w}row(w){return{id:w.id,ref:this.refOf(w.id),type:w.type,title:w.title,state:w.state,tags:[...w.tags],url:this.baseUrl?`${this.baseUrl}${w.id}`:"",isBug:w.type==="Bug"}}async write(what){if(this.delayMs&&await new Promise(r=>setTimeout(r,this.delayMs)),this.failing){if(this.failing.left<=0)throw new Error(this.failing.message);this.failing.left-=1}this.log.push(what)}};function demoTracker(opts={}){return new DemoTracker(opts)}var demos=new Map,DEMO_WRITE_MS=400;function sendTracker(bp){let meta=readMeta(bp);if(meta.engine==="demo"){let t=demos.get(bp.root);return t||(t=demoTracker({levels:{project:meta.project},delayMs:Math.round(DEMO_WRITE_MS/Math.max(.1,meta.speed||1))}),demos.set(bp.root,t)),t}try{let t=trackerFor(bp);return t?.abilities.create?t:null}catch{return null}}function sendTargetOf(bp){let t=sendTracker(bp);if(!t)return;let project;try{let meta=readMeta(bp);project=meta.engine==="demo"?meta.project:trackerModule(t.name)?.read(bp.root).values.project?.trim()||void 0}catch{project=void 0}return{module:t.name,label:t.label,...project?{project}:{}}}var SMALL=new Set("a an the is are was were be been being so and or of to in on at for by it its that this as with from can may will would should could also then than which who when where there here their them they these those one any all each every do does did has have had just only".split(" ")),NEGATIVE=new Set(["not","no","never","none","nothing","without"]);function stem(w){if(/^\d/.test(w))return w;let s=w;return s.length>5&&s.endsWith("ing")?s=s.slice(0,-3):s.length>4&&s.endsWith("ed")||s.length>4&&s.endsWith("es")?s=s.slice(0,-2):s.length>3&&s.endsWith("s")&&!s.endsWith("ss")&&(s=s.slice(0,-1)),s.length>3&&s.endsWith("e")&&(s=s.slice(0,-1)),s}function meaningWords(text3){let s=text3.toLowerCase().normalize("NFKD").replace(new RegExp("\\p{M}+","gu"),"").replace(/[’‘]/g,"'").replace(/\bcannot\b/g,"can not").replace(/n't\b/g," not").replace(/'s\b/g,"");return new Set((s.match(/\d{1,2}:\d{2}|[\p{L}\p{N}]+/gu)??[]).filter(w=>!SMALL.has(w)).map(w=>NEGATIVE.has(w)?w:stem(w)))}function sharedWords(a,b){let A=meaningWords(a),B=meaningWords(b);if(!A.size||!B.size)return{overlap:0,jaccard:0};let both=0;for(let w of A)B.has(w)&&(both+=1);return{overlap:both/Math.min(A.size,B.size),jaccard:both/(A.size+B.size-both)}}function saysTheSame(a,b){let A=meaningWords(a),B=meaningWords(b);if(Math.min(A.size,B.size)<3)return A.size===B.size&&[...A].every(w=>B.has(w));let{overlap,jaccard}=sharedWords(a,b);if(overlap<.6||jaccard<.45)return!1;let negative=s=>[...s].some(w=>NEGATIVE.has(w));if(negative(A)===negative(B))return!0;let rest=s=>[...s].filter(w=>!NEGATIVE.has(w)),ra=rest(A),rb=new Set(rest(B)),both=ra.filter(w=>rb.has(w)).length;return both/(ra.length+rb.size-both)<.75}import fs96 from"fs";var MAX_BASE_TRIES=3,LANDING_LOOK_MS=15e3,MAX_LANDING_WAIT_MS=3*6e4;function baseTries(t){return t.selfAccept?.baseTries??(t.selfAccept?.baseTried?1:0)}function baseMoved(bp,story2){return copyViews(readMeta(bp),story2).some(t=>oneBaseMoved(bp,t))}function oneBaseMoved(bp,t){if(!t.worktree||!t.branch||!fs96.existsSync(t.worktree))return!0;try{let repo=repoFor(readMeta(bp),t),remote=repo.remote||remotes(t.worktree)[0];return[repo.base,...remote?[`refs/remotes/${remote}/${repo.base}`]:[]].some(ref2=>{let tip=git(t.worktree,["rev-parse","--verify","-q",ref2],{allowFail:!0});return!!tip&&tip!==t.baseCommit&&!isAncestor(t.worktree,tip,t.branch)})}catch{return!0}}function bringInAgain(bp,id2){let t0=requireTicket(bp,id2),moved=baseTries(t0)===0||baseMoved(bp,t0);return updateTicket(bp,id2,t=>{if(t.waiting?.kind!=="stuck"||t.stuck?.gate!=="merge")return;let tries=baseTries(t);if(tries>=MAX_BASE_TRIES){addActivity(t,"squad",`It still clashes after the newest code came in ${MAX_BASE_TRIES} times, so it waits for you`);return}if(!moved){addActivity(t,"squad","It still clashes, and the code has not moved since it last came in, so it waits for you");return}t.selfAccept={...t.selfAccept,baseTried:now(),baseTries:tries+1},t.waiting=null,t.stuck=void 0,t.retryPhase=void 0,t.stage="implement",t.live=!0,t.status="Bringing in the newest code";let then=t.track==="auto"?"":". Then the proof comes back to you";addActivity(t,"squad",`It clashes with newer changes, so squad-kit brings in the newest code and runs the checks, the review and the proof again (try ${tries+1} of ${MAX_BASE_TRIES})${then}`),scheduleSquad(t,"update-base",.2,1)})}function landingFirst(bp,t){let meta=readMeta(bp),repo=repoFor(meta,t).path;return listTickets(bp).find(o=>o.id!==t.id&&(o.squad?.phase==="accept"||o.squad?.phase==="publish")&&!o.waiting&&!o.stopRequested&&!o.deleting&&!o.archived&&repoFor(meta,o).path===repo)?.id??null}function waitForLanding(bp,id2){let t=requireTicket(bp,id2),since=t.selfAccept?.landingSince,going=landingFirst(bp,t);if(!going||since&&Date.now()-Date.parse(since)>=MAX_LANDING_WAIT_MS)return since&&updateTicket(bp,id2,x=>delete x.selfAccept?.landingSince),!1;let base2=repoFor(readMeta(bp),t).base;return updateTicket(bp,id2,x=>{since||(x.selfAccept={...x.selfAccept,landingSince:now()},addActivity(x,"squad",`${going} is about to merge into ${base2}, so this story waits for it, then brings in the newest code`)),x.status=`Waits for ${going} to merge, then brings in the newest code`,x.squad&&(x.squad.nextAt=new Date(Date.now()+LANDING_LOOK_MS).toISOString())}),!0}import fs98 from"fs";import path90 from"path";import yaml13 from"js-yaml";function waitsAtReview(t){let kind=t.waiting?.kind;if(kind==="proof")return!0;if(kind!=="stuck")return!1;let gate=t.stuck?.gate;return gate==="checks"||gate==="review"||gate==="proof"||gate==="merge"||t.stage==="test"||t.stage==="review"}function pageOf(shot){let p=shot.page?.trim()??"";if(!p)return"";let bare=p.replace(/^[a-z]+:\/\/[^/]+/i,"");return bare.startsWith("/")?bare:`/${bare}`}function rowPictures(shot){return[...shot.after?[{after:shot.after,...shot.page?{page:shot.page}:{},...shot.note?{note:shot.note}:{}}]:[],...shot.more??[]]}function pagesOf(shot){return[...new Set([shot,...shot.more??[]].map(pageOf).filter(Boolean))]}function proofPages(t){return[...new Set((t.proof??[]).flatMap(pagesOf))]}function pageUrl(base2,page){return`${base2.replace(/\/+$/,"")}${page.startsWith("/")?page:`/${page}`}`}var ASK_SESSION={analyst:"intake",builder:"build",tester:"tester"},BUILT=new Set(["implement","test","review","release","done"]),QUEUED="Next in line";function askAgent(t,live=!0){let pick2=t.proof?.length?"tester":t.sessions?.build||BUILT.has(t.stage)?"builder":"analyst";return pick2==="analyst"||!live||t.sessions?.[ASK_SESSION[pick2]]?pick2:"analyst"}function inQueue(t){return!t.waiting&&(t.status??"").startsWith(QUEUED)}function squadWorks(t){return!!(t.live||t.squad)&&!t.waiting&&!t.takenOver&&!inQueue(t)}var WHO2={architect:"Architect",builder:"Builder",reviewer:"Reviewer"},WORKING={tickets:"is being read",specs:"is being planned",implement:"is being built",test:"is being checked",review:"is being reviewed",release:"is accepted",bugs:"is being looked into",done:"is released"};function whereItStands(t){let id2=t.id;if(t.stage==="done")return`${id2} is released${t.release?` in ${t.release}`:""}`;if(t.stage==="release")return`${id2} is accepted${t.release?` and goes out in ${t.release}`:""}`;if(t.takenOver?.free)return`${id2} is in a conversation outside squad-kit, and the squad waits until it comes back`;if(t.takenOver)return`You have ${id2}, and the squad waits until you give it back`;let deps=(t.dependsOn?.length?t.dependsOn:t.dependency?.on??[]).join(", ");switch(t.waiting?.kind){case"proof":return`${id2} waits for your check`;case"stuck":return waitsAtReview(t)?`${id2} stopped at a check and waits for your decision`:`${id2} stopped and waits for your decision`;case"ask":return`${id2} waits for your answer to the ${t.agentQuestion?WHO2[t.agentQuestion.from]:"squad"}'s question`;case"depends":return`${id2} waits for ${deps||"another story"} to be merged`;case"questions":return`${id2} waits for your answers to the questions`;case"spec":return`${id2}'s spec waits for your approval`;case"plan":return`${id2}'s design waits for your approval`;case"change":return`${id2} waits for you to decide on the design update`;case"release":return`${id2} waits for you to release it`;case"chat":return`${id2} waits while you talk it through`;case"yours":return`You have ${id2}, and the squad waits until you give it back`}return t.dependency?.way==="wait"&&!t.dependency.started&&!t.squad?`${id2} waits for ${deps||"another story"} to be merged`:inQueue(t)?`${id2} waits in the queue`:t.live||t.squad?`${id2} ${WORKING[t.stage]}`:t.stage==="tickets"||t.stage==="bugs"?`${id2} has not started yet`:`${id2} is not running right now`}var isApiProof=file3=>file3.endsWith(".api.json");var CHECK_FAILED="squad-kit could not check the screenshots this time. Press Try again to try once more.";function criterionText2(t,n){let c=t.criteria?.[n-1];if(c){let text3=`${c.then.charAt(0).toUpperCase()}${c.then.slice(1)}`;return t.criteria.some((o,i)=>i!==n-1&&o.then.trim()===c.then.trim())?`${text3} (${c.given}, ${c.when})`:text3}return(t.notes??[]).filter(x=>x.kind==="criterion")[n-1-(t.criteria?.length??0)]?.text.trim()||null}function doneWhenCount(t){return(t.criteria?.length??0)+(t.notes??[]).filter(n=>n.kind==="criterion").length}function offScreenNumbers(t){let out20=new Set;for(let n=1;n<=doneWhenCount(t);n+=1)(t.offScreen??[]).some(o=>o.criterion===criterionText2(t,n))&&out20.add(n);return out20}function verdictOf(said){return said==="met"||said==="broken"||said==="not-shown"?said:said==="not-met"?"broken":said==="unclear"?"not-shown":null}var WORSE={met:0,"not-shown":1,broken:2};function checkVerdicts(answer2,asked,given=!1){let out20=new Map,line3=v=>typeof v=="string"?v.replace(/\s+/g," ").trim():"",list6=answer2?.checks;for(let c of Array.isArray(list6)?list6:[]){let o=c??{},n=Number(o.criterion);if(!asked.includes(n))continue;let said={shows:verdictOf(o.shows)??"not-shown",why:line3(o.why),advice:line3(o.advice),noChange:o.noChange===!0,...o.canShow===!1?{canShow:!1,canShowWhy:line3(o.canShowWhy)}:{}},had=out20.get(n);(!had||WORSE[said.shows]>WORSE[had.shows]||said.shows===had.shows&&had.canShow===!1&&said.canShow!==!1)&&out20.set(n,said)}if(!given)for(let n of asked)out20.has(n)||out20.set(n,{shows:"not-shown",why:"",advice:"",noChange:!1});return out20}var ABOUT_THE_PICTURE=new RegExp([/\b(?:not|n't)\s+(?:\w+\s+)?(?:in|within|inside)\s+(?:the\s+|this\s+|that\s+|these\s+|those\s+|any\s+of\s+the\s+|either\s+|any\s+)?(?:pictures?|screenshots?|images?|shots?|frames?|views?)\b/,/\bnot\s+in\s+(?:it|them|any\s+of\s+them)\b/,/\bin\s+(?:none|neither)\s+of\s+them\b/,/\b(?:none|neither)\s+of\s+(?:the\s+|these\s+|those\s+)?(?:pictures?|screenshots?|images?|shots?)\b/,/\bno\s+(?:picture|screenshot|image|shot)\s+shows?\b/,/\bmissing\s+from\s+(?:the\s+|this\s+|these\s+|those\s+)?(?:pictures?|screenshots?|images?|shots?)\b/,/\bout\s+of\s+(?:the\s+)?(?:pictures?|screenshots?|frames?|view|shots?)\b/,/\b(?:pictures?|screenshots?|images?|shots?)\s+(?:\w+\s+)?(?:(?:does|did|do|can|could)\s*(?:not|n't)|cannot|fails?\s+to)\s+(?:\w+\s+)?(?:show|tell|prove|include|capture|cover)\b/,/\b(?:pictures?|screenshots?|images?|shots?)\s+(?:is|was|are|were)\s*(?:not|n't)\b/,/\b(?:pictures?|screenshots?|images?|shots?)\s+(?:only\s+)?shows?\b[^.]*\bbut\s+not\b/,/\b(?:cannot|can't|could\s*(?:not|n't))\s+be\s+(?:seen|judged|told|confirmed)\b/,/\b(?:cannot|can't|could\s*(?:not|n't))\s+(?:tell|judge|confirm)\b/,/\b(?:i|we)\s+(?:cannot|can't|could\s*(?:not|n't))\s+see\b/,/\bbelow\s+the\s+(?:fold|visible\s+part)\b/,/\bstill\s+loading\b/,/\bat\s+the\s+required\b/].map(r=>r.source).join("|"),"i");function drawnFromPicture(why){return!!why?.trim()&&!ABOUT_THE_PICTURE.test(why)}var SAME_PICTURE="The after screenshot is the same as the before one, so it does not show the change.",SAME_ADVICE='Open the screen, state or data where the "done when" can be seen, not the same view as before the change, and do the steps that lead to its result.';function together(tester,check,same5=!1){let c=check??{shows:"not-shown",why:"",advice:"",noChange:!1};if(c.shows==="broken"&&drawnFromPicture(c.why))return{shows:"broken",why:c.why,advice:""};if(c.shows!=="met"&&c.canShow===!1)return{shows:"off-screen",why:c.canShowWhy||c.why,advice:""};if(same5&&!c.noChange)return{shows:"not-shown",why:SAME_PICTURE,advice:SAME_ADVICE};if(c.shows!=="met")return{shows:"not-shown",why:c.why,advice:c.advice};let said=tester.why??tester.note;return tester.shows==="broken"&&drawnFromPicture(said)?{shows:"broken",why:said,advice:""}:{shows:"met",why:"",advice:""}}function fromReason(reason,check){return check?.canShow===!1?{shows:"off-screen",why:check.canShowWhy||reason,advice:""}:{shows:"not-shown",why:check?.why??"",advice:check?.advice??""}}var NO_REASON=/\b(?:did\s*(?:not|n't)\s+try|not\s+tried|too\s+long|no\s+time|skipped\s+it)\b/i;function afterLastRetake(v,reason){let said=reason?.trim();return v.shows!=="not-shown"||!said||NO_REASON.test(said)||v.why===SAME_PICTURE?v:{shows:"off-screen",why:said,advice:""}}function setMain(row3,p){row3.after=p.after,p.page?row3.page=p.page:delete row3.page,p.device?row3.device=p.device:delete row3.device,p.note?row3.note=p.note:delete row3.note}function addPictures(row3,pictures,same5=(a,b)=>a===b){for(let p of pictures){let picture={after:p.file,...p.page?{page:p.page}:{},...p.device?{device:p.device}:{},...p.note?{note:p.note}:{}};if(!row3.after||same5(row3.after,p.file)){setMain(row3,picture);continue}let others=row3.more??=[],at3=others.findIndex(m=>same5(m.after,p.file));at3>=0?others[at3]=picture:others.push(picture)}row3.shows=["broken","not-shown","met"].find(s=>pictures.some(p=>p.shows===s));let broken=pictures.filter(p=>p.shows==="broken"&&p.note),said=broken.find(p=>drawnFromPicture(p.note))??broken[0];said?row3.why=said.note:delete row3.why}function makeMain(row3,at3){let next=row3.more[at3];row3.more[at3]={after:row3.after,...row3.page?{page:row3.page}:{},...row3.device?{device:row3.device}:{},...row3.note?{note:row3.note}:{}},setMain(row3,next)}function lowerFirst(s){return/^[A-Z][a-z]/.test(s)?`${s.charAt(0).toLowerCase()}${s.slice(1)}`:s}function firstSentence(s){let one2=s.split(/(?<=[.!?])\s+/)[0].trim();return/[.!?]$/.test(one2)?one2:`${one2}.`}function numbersWords(rows2){let n=rows2.map(r=>String(r.criterion));return n.length>1?`${n.slice(0,-1).join(", ")} and ${n.at(-1)}`:n[0]}function brokenStop(rows2){if(rows2.length===1){let r=rows2[0];return{question:`The screenshots show "done when" ${r.criterion} is not working yet: ${lowerFirst(firstSentence(r.why))}`,points:[`"Done when" ${r.criterion}: ${r.text}`]}}return{question:`The screenshots show ${rows2.length} "done when" are not working yet.`,points:rows2.map(r=>`"Done when" ${r.criterion}, ${r.text}: ${r.why}`)}}function notShownStop(rows2){return{question:`The squad could not show "done when" ${numbersWords(rows2)} on a screen. Try it yourself, or ask a question.`,points:rows2.map(r=>`"Done when" ${r.criterion}, ${r.text}: ${r.why}`)}}function retakeLine(n,text3,why,advice){let said=why.trim().replace(/[.!?]+$/,"");return`The after screenshot for "done when" ${n} ("${text3}") does not show it clearly yet${said?`: ${said}`:""}. ${advice.trim()?`Show it like this: ${advice.trim()}`:"Take it again so its result shows."}`}function reasonRefusedLine(n,text3,reason,why,advice){let said=why.trim().replace(/[.!?]+$/,"");return`You said "done when" ${n} ("${text3}") cannot be shown on a screen: ${reason.trim().replace(/[.!?]+$/,"")}. squad-kit's check says it can${said?`: ${said}`:""}. ${advice.trim()?`Show it like this: ${advice.trim()}`:"Take it now."}`}function cannotShowLine(n,text3,why){return`squad-kit's check says the rest of "done when" ${n} ("${text3}") cannot be shown: ${why.trim().replace(/[.!?]+$/,"")}. If you see a way to show it, take it now. If you agree, put it in offScreen with which part and why. Your pictures stay.`}function fixLine(r){return`"Done when" ${r.criterion} (${r.text}): ${r.why}`}function brokenRows(t){let out20=[];for(let n=1;n<=doneWhenCount(t);n+=1){let text3=criterionText2(t,n),row3=(t.proof??[]).find(p=>p.criterion===text3&&p.shows==="broken");text3&&row3&&out20.push({criterion:n,text:text3,why:row3.why??row3.note??"The screenshot shows it does not work as written."})}return out20}var STR={type:"string"},PROCESS_RULE="squad-kit runs the process: branches, commits, pushes, pull requests, reviews and releases. The repo's own rule files cover the code. Where one tells you to open your own pull request, commit, push or follow its own workflow, leave that part to squad-kit and carry on.",CHECKLIST={type:"array",items:{type:"object",properties:{n:{type:"integer"},answer:{type:"string",enum:["yes","no"]},why:STR},required:["n","answer"]}},COMMANDS={type:"object",properties:{setup:STR,test:STR,lint:STR,build:STR,start:STR,url:STR}},PARTS={type:"array",maxItems:10,items:{type:"object",properties:{id:STR,name:STR,what:STR,files:{type:"array",maxItems:4,items:STR},kind:{type:"string",enum:[...PART_KINDS]}},required:["id","name","what","files"]}},LINKS={type:"array",maxItems:20,items:{type:"object",properties:{from:STR,to:STR,label:STR},required:["from","to"]}},RUNS={type:"array",maxItems:30,items:{type:"object",properties:{id:STR,name:STR,shared:{type:"boolean"},repo:STR,folder:STR,needs:{type:"array",items:STR},url:STR},required:["id","name"]}},SCAN_SCHEMA={type:"object",properties:{empty:{type:"boolean"},summary:STR,map:{type:"array",items:{type:"object",properties:{area:STR,detail:STR},required:["area","detail"]}},decisions:{type:"array",items:{type:"object",properties:{title:STR,why:STR},required:["title","why"]}},commands:COMMANDS,repos:{type:"array",items:{type:"object",properties:{path:STR,summary:STR,commands:COMMANDS},required:["path","summary","commands"]}},users:STR,can:{type:"array",maxItems:8,items:STR},worth:{type:"array",maxItems:6,items:STR},ideas:{type:"array",maxItems:3,items:{type:"object",properties:{title:STR,why:STR},required:["title","why"]}},parts:PARTS,links:LINKS,runs:RUNS},required:["empty","summary","map","decisions","commands"]},BLUEPRINT_SCHEMA={type:"object",properties:{map:{type:"array",items:{type:"object",properties:{area:STR,detail:STR},required:["area","detail"]}},stack:{type:"array",items:{type:"object",properties:{name:STR,role:{type:"string",enum:[...TECH_ROLES]},why:STR,decision:STR},required:["name","why"]}},patterns:{type:"array",maxItems:10,items:{type:"object",properties:{name:STR,what:STR,where:{type:"array",maxItems:4,items:STR},decision:STR},required:["name","what","where"]}},parts:PARTS,links:LINKS},required:["map","stack","patterns"]},FIRST_BLUEPRINT_SCHEMA={...BLUEPRINT_SCHEMA,properties:{...BLUEPRINT_SCHEMA.properties,runs:RUNS,summary:STR,users:STR}},TECH_DECISION_SCHEMA={type:"object",properties:{title:STR,area:{type:"string",enum:[...DECISION_AREAS]},options:{type:"array",minItems:2,maxItems:3,items:STR},chosen:STR,why:STR,buildsOn:{type:"array",maxItems:4,items:STR}},required:["title","area","options","chosen","why","buildsOn"]},IMPACT_SCHEMA={type:"object",properties:{tickets:{type:"array",items:{type:"object",properties:{id:STR,affected:{type:"boolean"},why:STR,change:STR},required:["id","affected","why","change"]}}},required:["tickets"]},QUESTIONS_SCHEMA={type:"object",properties:{summary:STR,questions:{type:"array",maxItems:5,items:{type:"object",properties:{from:{type:"string",enum:["analyst","architect"]},kind:{type:"string",enum:["gap","clash"]},text:STR,options:{type:"array",minItems:2,maxItems:4,items:STR},typedOption:STR},required:["from","kind","text","options"]}}},required:["summary","questions"]},PLAN_SCHEMA={type:"object",properties:{story:STR,criteria:{type:"array",minItems:1,maxItems:6,items:{type:"object",properties:{given:STR,when:STR,then:STR},required:["given","when","then"]}},decision:{type:"object",properties:{title:STR,options:{type:"array",minItems:2,maxItems:3,items:{type:"object",properties:{name:STR,note:STR,chosen:{type:"boolean"}},required:["name","note","chosen"]}},why:STR,clientCheck:STR,replaces:STR,area:{type:"string",enum:[...DECISION_AREAS]}},required:["title","options","why","area"]},follows:{type:"array",maxItems:12,items:STR},dependsOn:{type:"array",maxItems:5,items:STR},steps:{type:"array",minItems:1,maxItems:8,items:STR},tests:{type:"integer"},screens:{type:"integer"},asks:{type:"integer"},releaseNote:STR,commands:COMMANDS,repo:STR,parts:{type:"array",maxItems:12,items:STR},mockup:STR},required:["story","criteria","steps","tests","screens","releaseNote"]},CRITERIA2={type:"array",minItems:1,maxItems:6,items:{type:"object",properties:{given:STR,when:STR,then:STR},required:["given","when","then"]}},SPEC_SCHEMA={type:"object",properties:{story:STR,criteria:CRITERIA2,releaseNote:STR},required:["story","criteria","releaseNote"]},CHANGE_SCHEMA={type:"object",properties:{summary:STR,size:{type:"string",enum:["small","big"]},why:STR,story:STR,criteria:{type:"array",maxItems:8,items:{type:"object",properties:{number:{type:"integer"},action:{type:"string",enum:["change","add","remove"]},given:STR,when:STR,then:STR},required:["number","action"]}}},required:["summary","size","why","story","criteria"]},CHANGE_DESIGN_SCHEMA={type:"object",properties:{summary:STR,steps:{type:"array",minItems:1,maxItems:10,items:{type:"object",properties:{title:STR,change:{type:"string",enum:["same","changed","new"]}},required:["title","change"]}}},required:["summary","steps"]},CHAT_SCHEMA={type:"object",properties:{reply:STR,ready:{type:"boolean"},options:{type:"array",maxItems:4,items:STR},files:{type:"array",maxItems:8,items:STR}},required:["reply","ready"]},QUESTION={type:"object",properties:{text:STR,options:{type:"array",maxItems:4,items:STR},why:STR,files:{type:"array",maxItems:6,items:STR},kind:{type:"string",enum:["gap","clash"]},sources:{type:"array",maxItems:4,items:{type:"object",properties:{text:STR,ticket:STR,section:STR,page:{type:"integer"}},required:["text"]}}},required:["text","kind"]},DECIDED={type:"array",maxItems:6,items:STR},DESIGN_SCHEMA={type:"object",properties:{...Object.fromEntries(Object.entries(PLAN_SCHEMA.properties).filter(([k])=>!["story","criteria","releaseNote"].includes(k))),question:QUESTION,decided:DECIDED},required:["steps","tests","screens"]},BUILD_SCHEMA={type:"object",properties:{summary:STR,testsAdded:{type:"integer"},notes:STR,question:QUESTION,decided:DECIDED},required:["summary"]},REVIEW_SCHEMA={type:"object",properties:{approved:{type:"boolean"},summary:STR,comments:{type:"array",items:STR},alsoChanged:{type:"array",items:STR},checklist:CHECKLIST,question:QUESTION,decided:DECIDED},required:["approved","summary","comments"]},PROOF_SCHEMA={type:"object",properties:{shots:{type:"array",maxItems:40,items:{type:"object",properties:{criterion:{type:"integer"},file:STR,shows:{type:"string",enum:["met","broken","not-shown"]},note:STR},required:["criterion","file","shows","note"]}},offScreen:{type:"array",maxItems:20,items:{type:"object",properties:{criterion:{type:"integer"},why:STR},required:["criterion","why"]}},checklist:CHECKLIST},required:["shots","offScreen"]},PROOF_CHECK_SCHEMA={type:"object",properties:{checks:{type:"array",maxItems:40,items:{type:"object",properties:{criterion:{type:"integer"},shows:{type:"string",enum:["met","broken","not-shown"]},why:STR,advice:STR,noChange:{type:"boolean"},canShow:{type:"boolean"},canShowWhy:STR},required:["criterion","shows","why","noChange"]}}},required:["checks"]},DRAFT_SCHEMA={type:"object",properties:{title:STR,ask:STR,kind:{type:"string",enum:["feature","bug"]},track:{type:"string",enum:["full","express"]},priority:{type:"string",enum:["high","normal","low"]},criteria:{type:"array",maxItems:8,items:STR},rules:{type:"array",maxItems:8,items:STR},edgeCases:{type:"array",maxItems:8,items:STR},gaps:{type:"array",maxItems:5,items:STR}},required:["title","ask","kind","track","priority","criteria","rules","edgeCases","gaps"]},PLAIN="Write for a project manager who may not be a developer: plain words, short sentences, no jargon.";function writtenInArabic(t){let letters=`${t.title} ${t.ask??""}`.replace(/[^\p{L}]/gu,""),arabic=letters.replace(/[^\u0600-\u06ff\u0750-\u077f\ufb50-\ufdff\ufe70-\ufeff]/g,"");return letters.length>0&&arabic.length*2>letters.length}function theirLanguage(t){return writtenInArabic(t)?`
678
+ `),fs94.renameSync(tmp,monthsFile(bp))}catch{}}function boardUsage(bp){let meta=readMeta(bp),all=listTickets(bp),tickets=all.filter(t=>t.usage?.length),project=meta.usage??[],gone=(meta.deleted??[]).filter(d=>d.usage?.length&&!all.some(t=>t.id===d.id&&t.deleting?.at===d.at)),deleted=gone.flatMap(d=>d.usage),lines3=[...tickets.flatMap(t=>t.usage),...project,...deleted];return{total:totalOf(lines3),project:totalOf(project),roles:byRole(lines3),tickets:tickets.length,deleted:{tickets:gone.length,total:totalOf(deleted)}}}var MORE_STEPS={chat:"Talking it through",draft:"Filling in a new ticket"};function stepWords2(role,step2){return STEPS[role]?.find(s=>s.id===step2)?.name??MORE_STEPS[step2]??step2}function costWords(t){if(t.costUsd===void 0)return null;let amount=t.costUsd<.005?"under $0.01":`$${t.costUsd.toFixed(2)}`;return t.costUnknown?`${amount}, ${t.costUnknown} ${t.costUnknown===1?"run":"runs"} not reported`:amount}function shortNumber(n){return n>=1e6?`${(n/1e6).toFixed(n>=1e7?0:1)}M`:n>=1e3?`${(n/1e3).toFixed(n>=1e4?0:1)}k`:String(n)}function tokenWords(t){return t.inputTokens===void 0&&t.outputTokens===void 0?null:[t.inputTokens!==void 0?`read ${shortNumber(t.inputTokens)} tokens`:"",t.outputTokens!==void 0?`wrote ${shortNumber(t.outputTokens)}`:""].filter(Boolean).join(", ")}var WARN_AT=.8;function share(spent,limit){return limit&&limit>0&&spent!==void 0?spent/limit:void 0}function budgetState(bp,t,budget=readMeta(bp).budget,at3=Date.now()){let monthSpent=readMonths(bp)[monthKey(at3)]?.costUsd??0,ticketSpent=t?totalOf(t.usage).costUsd??0:void 0,ticketShare=t?share(ticketSpent,budget?.ticketUsd):void 0,monthShare=share(monthSpent,budget?.monthUsd),warn2=[],over=[];for(let[kind,s]of[["ticket",ticketShare],["month",monthShare]])s!==void 0&&(s>=1?over.push(kind):s>=WARN_AT&&warn2.push(kind));return{...budget?.ticketUsd?{ticketUsd:budget.ticketUsd}:{},...budget?.monthUsd?{monthUsd:budget.monthUsd}:{},...ticketSpent!==void 0?{ticketSpent}:{},monthSpent,...ticketShare!==void 0?{ticketShare}:{},...monthShare!==void 0?{monthShare}:{},warn:warn2,over}}function parseUsd(v){if(v==null)return null;let s=String(v).trim().replace(/^\$/,"").replace(/\s*(usd|dollars?)$/i,"");if(!s||/^(off|none|no)$/i.test(s))return null;let n=Number(s);if(!Number.isFinite(n)||n<0)throw new Error(`"${v}" is not an amount. Write it in US dollars, like 5 or 2.50, or off.`);return n===0?null:Math.round(n*100)/100}function setBudget(bp,change){let meta=readMeta(bp),next={...meta.budget};for(let k of["ticketUsd","monthUsd"]){let v=change[k];v===null?delete next[k]:v!==void 0&&(next[k]=v)}return writeMeta(bp,{...meta,budget:Object.keys(next).length?next:void 0}),next}var NO_AI=new Set(["test","express-approve","accept","publish"]),usd=n=>`$${n.toFixed(2)}`;function carriedOn(t){let s=t.stuck;return!s?.budget||t.waiting?null:s.options.some(o=>o.chosen)?s.budget:null}async function budgetStop(bp,id2,phase,payment,at3=Date.now()){if(NO_AI.has(phase))return!1;let budget=readMeta(bp).budget;if(!budget?.ticketUsd&&!budget?.monthUsd)return!1;let t=readTicket(bp,id2);if(!t)return!1;let yes=carriedOn(t);if(yes&&(t=updateTicket(bp,id2,x=>{x.budgetOk={...x.budgetOk,...yes==="ticket"?{ticket:!0}:{month:monthKey(at3)}},x.stuck&&delete x.stuck.budget})),await payment()!=="api-key")return!1;let state=budgetState(bp,t,budget,at3),over=state.over.filter(k=>k==="ticket"?!t.budgetOk?.ticket:t.budgetOk?.month!==monthKey(at3));if(!over.length)return!1;let kind=over[0],what=kind==="ticket"?`${t.id} used ${usd(state.ticketSpent??0)} of its ${usd(state.ticketUsd??0)} budget.`:`The squad used ${usd(state.monthSpent)} of this month's ${usd(state.monthUsd??0)} budget for ${readMeta(bp).project}.`,copy2=!!(t.worktree&&fs95.existsSync(t.worktree));return updateTicket(bp,id2,x=>{x.live=!1,x.squad=null,x.retryPhase=phase,x.status=kind==="ticket"?"Paused at its budget":"Paused at this month's budget",x.waiting={kind:"stuck",summary:kind==="ticket"?"Reached its budget and waits for you":"Reached this month's budget and waits for you"},x.stuck={budget:kind,question:`${what} squad-kit paused it before the next step, so no step was cut in half. Carry on, or raise the budget in Settings first.`,options:[{do:"retry",name:"Carry on",note:kind==="ticket"?"The squad finishes this ticket and does not stop it for its budget again.":"The squad goes on with this ticket this month."},...copy2?[{do:"yourself",name:"Do the rest yourself",note:"Open the ticket's copy in your editor and finish it. Then the checks, review and proof run again."}]:[],{do:"plan",name:"Send it back to the plan",note:"A smaller plan uses less. The Architect rewrites it with what is done so far."}]},addActivity(x,"squad",`Paused before the next step: ${what}`)}),!0}function accepted(t){return t.stage==="release"||t.stage==="done"||!!t.mergedAt}function workSeconds(t){if(t.history?.length)return t.history.reduce((s,h)=>s+Math.max(0,(Date.parse(h.to)-Date.parse(h.from))/1e3),0)}function median(xs){let s=[...xs].sort((a,b)=>a-b),m=Math.floor(s.length/2);return s.length%2?s[m]:(s[m-1]+s[m])/2}function partWords(r){return r>=.85?"about as much as":r>=.58?"about two thirds of":r>=.42?"about half of":r>=.29?"about a third of":r>=.21?"about a quarter of":r>=.15?"about a fifth of":r>=.07?"about a tenth of":"less than a tenth of"}function sizeOfWork(t,all,project){let mine2=totalOf(t.usage).costUsd,others=all.filter(x=>x.id!==t.id&&accepted(x)).map(x=>totalOf(x.usage).costUsd).filter(c=>typeof c=="number"&&c>0);if(mine2===void 0||others.length<2)return null;let usual=median(others),times=mine2/usual,size2=times<.6?"small":times<=1.6?"medium":"large",words2=times>=1.75?`About ${Math.round(times)} times a usual ticket in ${project}.`:times>=1.25?`About one and a half times a usual ticket in ${project}.`:times>=.85?`About the same as a usual ticket in ${project}.`:`${cap(partWords(times))} a usual ticket in ${project}.`;return{size:size2,times,words:words2,from:others.length}}function cap(s){return s.charAt(0).toUpperCase()+s.slice(1)}function and(xs){return xs.length<=1?xs[0]??"":`${xs.slice(0,-1).join(", ")} and ${xs.at(-1)}`}var runsOf=(t,step2)=>(t.usage??[]).filter(l=>l.step===step2).reduce((n,l)=>n+l.runs,0);function sizeWhy(t){let parts2=[],q5=t.questions?.length??0;q5&&parts2.push(`${q5} ${q5===1?"question":"questions"}`),t.track==="express"?parts2.push("the Express track"):t.track==="auto"?parts2.push("a design written from the breakdown"):(t.decision||t.steps?.length)&&parts2.push("a design"),t.mockup&&parts2.push("a mockup");let fixes=runsOf(t,"fix");fixes&&parts2.push(fixes===1?"one round of fixes":`${fixes} rounds of fixes`);let reviews=runsOf(t,"review");return reviews===2?parts2.push("a second review"):reviews>2&&parts2.push(`${reviews} reviews`),parts2.length?`It had ${and(parts2)}.`:""}var GROUP={questions:"Questions and spec",spec:"Questions and spec",scan:"Reading the project",blueprint:"Reading the project",design:"Design",build:"Build",fix:"Fixes",review:"Review",proof:"Screenshots",chat:"Talking it through",draft:"Filling in the ticket"};function whereItWent(lines3){let all=lines3??[],byCost=all.length>0&&all.every(l=>l.costUsd!==void 0&&!l.costUnknown),amount=l=>byCost?l.costUsd??0:(l.inputTokens??0)+(l.outputTokens??0),groups=new Map;for(let l of all){let name=GROUP[l.step]??l.step,g=groups.get(name)??{amount:0,cost:0};g.amount+=amount(l),g.cost+=l.costUsd??0,groups.set(name,g)}let sum=[...groups.values()].reduce((s,g)=>s+g.amount,0),parts2=[...groups.entries()].map(([name,g])=>({name,share:sum>0?g.amount/sum:0,...byCost?{costUsd:g.cost}:{}})).filter(p=>p.share>0).sort((a,b)=>b.share-a.share);return{by:byCost?"cost":"tokens",parts:parts2}}var STEP_WORD={questions:"questions",spec:"spec",scan:"reading the project",blueprint:"reading the project",design:"design",build:"the build",fix:"fixes",review:"review",proof:"screenshots",chat:"talking it through",draft:"filling in the ticket"};function engineerDetails(lines3){let byModel=new Map;for(let l of lines3??[]){let model=l.model||`${l.level} level`,steps=byModel.get(model)??[],w=STEP_WORD[l.step]??l.step;steps.includes(w)||steps.push(w),byModel.set(model,steps)}let models=[...byModel.entries()].map(([m,steps])=>`${m} for ${and(steps)}`).join(" \xB7 "),t=totalOf(lines3),tokens=t.inputTokens===void 0&&t.outputTokens===void 0?null:[t.inputTokens!==void 0?`${shortNumber(t.inputTokens)} tokens read${t.cachedTokens&&t.inputTokens?` (${Math.round(100*t.cachedTokens/t.inputTokens)}% from cache)`:""}`:"",t.outputTokens!==void 0?`${shortNumber(t.outputTokens)} written`:""].filter(Boolean).join(", ");return{models,tokens}}function expressTip(t,all){if(t.track==="express"||t.track==="auto")return null;let mine2=totalOf(t.usage).costUsd,x=all.filter(o=>o.id!==t.id&&o.track==="express"&&accepted(o)&&(totalOf(o.usage).costUsd??0)>0).sort((a,b)=>(b.mergedAt??b.createdAt).localeCompare(a.mergedAt??a.createdAt))[0];if(x&&mine2&&mine2>0){let r=totalOf(x.usage).costUsd/mine2;if(r<.85)return`The Express track used ${partWords(r)} this for ${x.title} (${x.id}). For a small change like that, pick Express when you make the ticket.`}return"For a small change, like a typo or one word on a button, pick the Express track when you make the ticket. It skips the questions and the plan approval, so it uses much less."}function monthTotal(bp,at3=Date.now()){let months=readMonths(bp),month=monthKey(at3),first2=Object.keys(months).sort()[0];return{month,total:months[month]??{runs:0},...first2?{since:first2}:{}}}function ticketCost(bp,t,payment,at3=Date.now()){let all=listTickets(bp),meta=readMeta(bp),seconds2=workSeconds(t);return{payment,project:meta.project,total:totalOf(t.usage),size:sizeOfWork(t,all,meta.project),why:sizeWhy(t),where:whereItWent(t.usage),month:monthTotal(bp,at3),...payment.payment==="api-key"?{budget:budgetState(bp,t,meta.budget,at3)}:{},tip:expressTip(t,all),details:engineerDetails(t.usage),...seconds2!==void 0?{seconds:seconds2}:{}}}var QUALITY_AFTER=3;function qualityStats(bp){let done2=listTickets(bp).filter(t=>accepted(t)&&t.quality);return QUALITIES.map(quality=>{let mine2=done2.filter(t=>t.quality===quality),costs=mine2.map(t=>totalOf(t.usage).costUsd).filter(c=>typeof c=="number"),times=mine2.map(workSeconds).filter(s=>typeof s=="number"),avg=xs=>xs.reduce((a,b)=>a+b,0)/xs.length;return{quality,tickets:mine2.length,...costs.length?{costUsd:avg(costs)}:{},...times.length?{seconds:avg(times)}:{}}})}function costOverview(bp,payment,at3=Date.now()){let meta=readMeta(bp),budget=meta.budget??{},warnings=[];if(payment.payment==="api-key"&&(budget.ticketUsd||budget.monthUsd)){let month=budgetState(bp,null,budget,at3);if(month.monthShare!==void 0&&month.monthShare>=WARN_AT&&warnings.push({kind:"month",spent:month.monthSpent,limit:budget.monthUsd,share:month.monthShare,over:month.monthShare>=1}),budget.ticketUsd)for(let t of listTickets(bp).filter(x=>!accepted(x)&&!x.archived&&!x.deleting)){let s=budgetState(bp,t,budget,at3);s.ticketShare!==void 0&&s.ticketShare>=WARN_AT&&!t.budgetOk?.ticket&&warnings.push({kind:"ticket",id:t.id,title:t.title,spent:s.ticketSpent??0,limit:budget.ticketUsd,share:s.ticketShare,over:s.ticketShare>=1})}}return{payment,project:meta.project,month:monthTotal(bp,at3),budget,warnings,quality:qualityStats(bp),qualityAfter:QUALITY_AFTER}}var KEEP_MS=10*6e4,found=new Map;function detect(tool5,now3){let seen=found.get(tool5.name);if(seen&&now3-seen.at<KEEP_MS)return seen.value;let value=(tool5.billing?.()??Promise.resolve(null)).catch(()=>null);return found.set(tool5.name,{at:now3,value}),value}function builderTool(bp){try{return aiTool(readAgent(bp,"builder").tool)}catch{return AI_TOOLS[0]}}async function howPaid(bp,opts={}){let tool5=opts.tool??builderTool(bp),choice=paymentChoice(),billing=tool5?await detect(tool5,opts.now??Date.now()):null;return{payment:choice??billing?.kind??"subscription",from:choice?"you":billing?"found":"guess",found:billing,tool:tool5?.label??"AI tool",name:tool5?.plan??tool5?.label??"AI"}}var STATES2=["New","Active","Resolved","Closed"],STAGE_AT={"not-started":0,"in-progress":1,resolved:2,closed:3},DemoTracker=class{name;label;abilities={sprints:!1,wiki:!1,bugs:!0,tags:!0,create:!0};identity;items=new Map;log=[];nextId;lv;prefix;baseUrl;delayMs;failing=null;constructor(opts={}){this.name=opts.name??"azure",this.label=opts.label??"Azure DevOps",this.prefix=opts.prefix??"AB#",this.lv={epic:"Epic",feature:"Feature",story:"User Story",process:"Agile",...opts.levels},this.nextId=opts.start??120,this.baseUrl=opts.baseUrl??"",this.delayMs=opts.delayMs??0,this.identity=`demo/${this.name}`}breakAfter(writes,message=`${this.label} did not answer`){this.failing={left:writes,message}}mend(){this.failing=null}remember(items3){for(let i of items3)this.items.has(i.id)||this.items.set(i.id,{id:i.id,type:i.type,title:i.title,state:"New",description:"",acceptance:"",tags:[],...i.parent?{parent:i.parent}:{},dependsOn:[],comments:[]}),/^\d+$/.test(i.id)&&(this.nextId=Math.max(this.nextId,Number(i.id)+1))}parseRef(text3){let id2=text3.trim().replace(new RegExp(`^${this.prefix.replace(/[^\w]/g,"\\$&")}`,"i"),"").replace(/^#/,"");return/^\d+$/.test(id2)&&Number(id2)>0?id2:null}refOf(id2){return`${this.prefix}${id2}`}async whoAmI(){return"Demo"}async sprints(){return[]}async list(opts={}){return[...this.items.values()].filter(w=>w.type!==this.lv.epic&&w.type!==this.lv.feature&&(opts.includeDone||w.state!=="Closed")).map(w=>this.row(w))}async get(id2){let w=this.item(id2),parents=[];for(let p=w.parent?this.items.get(w.parent):void 0;p&&parents.length<2;p=p.parent?this.items.get(p.parent):void 0)parents.push({id:p.id,type:p.type,title:p.title});return{...this.row(w),description:w.description,acceptanceCriteria:w.acceptance,reproSteps:"",parents,dependsOn:w.dependsOn.map(d=>this.item(d)).map(d=>({ref:this.refOf(d.id),title:d.title,state:d.state})),attachments:[]}}async download(){throw new Error("The demo tracker keeps no files.")}async wikiPages(){return[]}async moveTo(id2,stage){let w=this.item(id2);return STAGE_AT[stage]<=STATES2.indexOf(w.state)?null:(await this.write(`move ${id2} ${STATES2[STAGE_AT[stage]]}`),w.state=STATES2[STAGE_AT[stage]],w.state)}async comment(id2,markdown){let w=this.item(id2);await this.write(`comment ${id2}`),w.comments.push(markdown)}async addTag(id2,tag){let w=this.item(id2);w.tags.includes(tag)||(await this.write(`tag ${id2} ${tag}`),w.tags.push(tag))}async fileBug(input7){return await this.write("create Bug"),this.row(this.add({type:"Bug",title:input7.title,description:input7.steps,tags:input7.tags}))}async levels(){return{...this.lv}}async createItem(input7){input7.parent&&this.item(input7.parent);let type=this.lv[input7.level];await this.write(`create ${type} ${this.nextId}`);let w=this.add({type,title:input7.title,description:input7.markdown,acceptance:input7.acceptance??"",tags:input7.tags??[],parent:input7.parent});return this.row(w)}async findMade(input7){let type=this.lv[input7.level],hit=[...this.items.values()].reverse().find(w=>w.type===type&&w.tags.includes(input7.tag)&&w.description.includes(input7.mark));return hit?this.row(hit):null}async updateItem(id2,change){let w=this.item(id2);change.parent&&this.item(change.parent),await this.write(`update ${id2}`),change.title!==void 0&&(w.title=change.title),change.markdown!==void 0&&(w.description=change.markdown),change.acceptance!==void 0&&(w.acceptance=change.acceptance),change.parent!==void 0&&(w.parent=change.parent)}async linkDepends(id2,on){let w=this.item(id2),missing3=[...new Set(on)].filter(d=>d!==id2&&!w.dependsOn.includes(d));for(let d of missing3)this.item(d);missing3.length&&(await this.write(`link ${id2} ${missing3.join(" ")}`),w.dependsOn.push(...missing3))}item(id2){let w=this.items.get(String(id2));if(!w)throw new Error(`${this.refOf(String(id2))} is not in ${this.label}.`);return w}add(fields){let id2=String(this.nextId++),w={id:id2,state:"New",description:"",acceptance:"",tags:[],dependsOn:[],comments:[],...fields};return w.parent||delete w.parent,this.items.set(id2,w),w}row(w){return{id:w.id,ref:this.refOf(w.id),type:w.type,title:w.title,state:w.state,tags:[...w.tags],url:this.baseUrl?`${this.baseUrl}${w.id}`:"",isBug:w.type==="Bug"}}async write(what){if(this.delayMs&&await new Promise(r=>setTimeout(r,this.delayMs)),this.failing){if(this.failing.left<=0)throw new Error(this.failing.message);this.failing.left-=1}this.log.push(what)}};function demoTracker(opts={}){return new DemoTracker(opts)}var demos=new Map,DEMO_WRITE_MS=400;function sendTracker(bp){let meta=readMeta(bp);if(meta.engine==="demo"){let t=demos.get(bp.root);return t||(t=demoTracker({levels:{project:meta.project},delayMs:Math.round(DEMO_WRITE_MS/Math.max(.1,meta.speed||1))}),demos.set(bp.root,t)),t}try{let t=trackerFor(bp);return t?.abilities.create?t:null}catch{return null}}function sendTargetOf(bp){let t=sendTracker(bp);if(!t)return;let project;try{let meta=readMeta(bp);project=meta.engine==="demo"?meta.project:trackerModule(t.name)?.read(bp.root).values.project?.trim()||void 0}catch{project=void 0}return{module:t.name,label:t.label,...project?{project}:{}}}var SMALL=new Set("a an the is are was were be been being so and or of to in on at for by it its that this as with from can may will would should could also then than which who when where there here their them they these those one any all each every do does did has have had just only".split(" ")),NEGATIVE=new Set(["not","no","never","none","nothing","without"]);function stem(w){if(/^\d/.test(w))return w;let s=w;return s.length>5&&s.endsWith("ing")?s=s.slice(0,-3):s.length>4&&s.endsWith("ed")||s.length>4&&s.endsWith("es")?s=s.slice(0,-2):s.length>3&&s.endsWith("s")&&!s.endsWith("ss")&&(s=s.slice(0,-1)),s.length>3&&s.endsWith("e")&&(s=s.slice(0,-1)),s}function meaningWords(text3){let s=text3.toLowerCase().normalize("NFKD").replace(new RegExp("\\p{M}+","gu"),"").replace(/[’‘]/g,"'").replace(/\bcannot\b/g,"can not").replace(/n't\b/g," not").replace(/'s\b/g,"");return new Set((s.match(/\d{1,2}:\d{2}|[\p{L}\p{N}]+/gu)??[]).filter(w=>!SMALL.has(w)).map(w=>NEGATIVE.has(w)?w:stem(w)))}function sharedWords(a,b){let A=meaningWords(a),B=meaningWords(b);if(!A.size||!B.size)return{overlap:0,jaccard:0};let both=0;for(let w of A)B.has(w)&&(both+=1);return{overlap:both/Math.min(A.size,B.size),jaccard:both/(A.size+B.size-both)}}function saysTheSame(a,b){let A=meaningWords(a),B=meaningWords(b);if(Math.min(A.size,B.size)<3)return A.size===B.size&&[...A].every(w=>B.has(w));let{overlap,jaccard}=sharedWords(a,b);if(overlap<.6||jaccard<.45)return!1;let negative=s=>[...s].some(w=>NEGATIVE.has(w));if(negative(A)===negative(B))return!0;let rest=s=>[...s].filter(w=>!NEGATIVE.has(w)),ra=rest(A),rb=new Set(rest(B)),both=ra.filter(w=>rb.has(w)).length;return both/(ra.length+rb.size-both)<.75}import fs96 from"fs";var MAX_BASE_TRIES=3,LANDING_LOOK_MS=15e3,MAX_LANDING_WAIT_MS=3*6e4;function baseTries(t){return t.selfAccept?.baseTries??(t.selfAccept?.baseTried?1:0)}function baseMoved(bp,story2){return copyViews(readMeta(bp),story2).some(t=>oneBaseMoved(bp,t))}function oneBaseMoved(bp,t){if(!t.worktree||!t.branch||!fs96.existsSync(t.worktree))return!0;try{let repo=repoFor(readMeta(bp),t),remote=repo.remote||remotes(t.worktree)[0];return[repo.base,...remote?[`refs/remotes/${remote}/${repo.base}`]:[]].some(ref2=>{let tip=git(t.worktree,["rev-parse","--verify","-q",ref2],{allowFail:!0});return!!tip&&tip!==t.baseCommit&&!isAncestor(t.worktree,tip,t.branch)})}catch{return!0}}function bringInAgain(bp,id2){let t0=requireTicket(bp,id2),moved=baseTries(t0)===0||baseMoved(bp,t0);return updateTicket(bp,id2,t=>{if(t.waiting?.kind!=="stuck"||t.stuck?.gate!=="merge")return;let tries=baseTries(t);if(tries>=MAX_BASE_TRIES){addActivity(t,"squad",`It still clashes after the newest code came in ${MAX_BASE_TRIES} times, so it waits for you`);return}if(!moved){addActivity(t,"squad","It still clashes, and the code has not moved since it last came in, so it waits for you");return}t.selfAccept={...t.selfAccept,baseTried:now(),baseTries:tries+1},t.waiting=null,t.stuck=void 0,t.retryPhase=void 0,t.stage="implement",t.live=!0,t.status="Bringing in the newest code";let then=t.track==="auto"?"":". Then the proof comes back to you";addActivity(t,"squad",`It clashes with newer changes, so squad-kit brings in the newest code and runs the checks, the review and the proof again (try ${tries+1} of ${MAX_BASE_TRIES})${then}`),scheduleSquad(t,"update-base",.2,1)})}function landingFirst(bp,t){let meta=readMeta(bp),repo=repoFor(meta,t).path;return listTickets(bp).find(o=>o.id!==t.id&&(o.squad?.phase==="accept"||o.squad?.phase==="publish")&&!o.waiting&&!o.stopRequested&&!o.deleting&&!o.archived&&repoFor(meta,o).path===repo)?.id??null}function waitForLanding(bp,id2){let t=requireTicket(bp,id2),since=t.selfAccept?.landingSince,going=landingFirst(bp,t);if(!going||since&&Date.now()-Date.parse(since)>=MAX_LANDING_WAIT_MS)return since&&updateTicket(bp,id2,x=>delete x.selfAccept?.landingSince),!1;let base2=repoFor(readMeta(bp),t).base;return updateTicket(bp,id2,x=>{since||(x.selfAccept={...x.selfAccept,landingSince:now()},addActivity(x,"squad",`${going} is about to merge into ${base2}, so this story waits for it, then brings in the newest code`)),x.status=`Waits for ${going} to merge, then brings in the newest code`,x.squad&&(x.squad.nextAt=new Date(Date.now()+LANDING_LOOK_MS).toISOString())}),!0}import fs98 from"fs";import path90 from"path";import yaml13 from"js-yaml";function waitsAtReview(t){let kind=t.waiting?.kind;if(kind==="proof")return!0;if(kind!=="stuck")return!1;let gate=t.stuck?.gate;return gate==="checks"||gate==="review"||gate==="proof"||gate==="merge"||t.stage==="test"||t.stage==="review"}function pageOf(shot){let p=shot.page?.trim()??"";if(!p)return"";let bare=p.replace(/^[a-z]+:\/\/[^/]+/i,"");return bare.startsWith("/")?bare:`/${bare}`}function rowPictures(shot){return[...shot.after?[{after:shot.after,...shot.page?{page:shot.page}:{},...shot.note?{note:shot.note}:{}}]:[],...shot.more??[]]}function pagesOf(shot){return[...new Set([shot,...shot.more??[]].map(pageOf).filter(Boolean))]}function proofPages(t){return[...new Set((t.proof??[]).flatMap(pagesOf))]}function pageUrl(base2,page){return`${base2.replace(/\/+$/,"")}${page.startsWith("/")?page:`/${page}`}`}var ASK_SESSION={analyst:"intake",builder:"build",tester:"tester"},BUILT=new Set(["implement","test","review","release","done"]),QUEUED="Next in line";function askAgent(t,live=!0){let pick2=t.proof?.length?"tester":t.sessions?.build||BUILT.has(t.stage)?"builder":"analyst";return pick2==="analyst"||!live||t.sessions?.[ASK_SESSION[pick2]]?pick2:"analyst"}function inQueue(t){return!t.waiting&&(t.status??"").startsWith(QUEUED)}function squadWorks(t){return!!(t.live||t.squad)&&!t.waiting&&!t.takenOver&&!inQueue(t)}var WHO2={architect:"Architect",builder:"Builder",reviewer:"Reviewer"},WORKING={tickets:"is being read",specs:"is being planned",implement:"is being built",test:"is being checked",review:"is being reviewed",release:"is accepted",bugs:"is being looked into",done:"is released"};function whereItStands(t){let id2=t.id;if(t.stage==="done")return`${id2} is released${t.release?` in ${t.release}`:""}`;if(t.stage==="release")return`${id2} is accepted${t.release?` and goes out in ${t.release}`:""}`;if(t.takenOver?.free)return`${id2} is in a conversation outside squad-kit, and the squad waits until it comes back`;if(t.takenOver)return`You have ${id2}, and the squad waits until you give it back`;let deps=(t.dependsOn?.length?t.dependsOn:t.dependency?.on??[]).join(", ");switch(t.waiting?.kind){case"proof":return`${id2} waits for your check`;case"stuck":return waitsAtReview(t)?`${id2} stopped at a check and waits for your decision`:`${id2} stopped and waits for your decision`;case"ask":return`${id2} waits for your answer to the ${t.agentQuestion?WHO2[t.agentQuestion.from]:"squad"}'s question`;case"depends":return`${id2} waits for ${deps||"another story"} to be merged`;case"questions":return`${id2} waits for your answers to the questions`;case"spec":return`${id2}'s spec waits for your approval`;case"plan":return`${id2}'s design waits for your approval`;case"change":return`${id2} waits for you to decide on the design update`;case"release":return`${id2} waits for you to release it`;case"chat":return`${id2} waits while you talk it through`;case"yours":return`You have ${id2}, and the squad waits until you give it back`}return t.dependency?.way==="wait"&&!t.dependency.started&&!t.squad?`${id2} waits for ${deps||"another story"} to be merged`:inQueue(t)?`${id2} waits in the queue`:t.live||t.squad?`${id2} ${WORKING[t.stage]}`:t.stage==="tickets"||t.stage==="bugs"?`${id2} has not started yet`:`${id2} is not running right now`}var isApiProof=file3=>file3.endsWith(".api.json");var CHECK_FAILED="squad-kit could not check the screenshots this time. Press Try again to try once more.";function criterionText2(t,n){let c=t.criteria?.[n-1];if(c){let text3=`${c.then.charAt(0).toUpperCase()}${c.then.slice(1)}`;return t.criteria.some((o,i)=>i!==n-1&&o.then.trim()===c.then.trim())?`${text3} (${c.given}, ${c.when})`:text3}return(t.notes??[]).filter(x=>x.kind==="criterion")[n-1-(t.criteria?.length??0)]?.text.trim()||null}function doneWhenCount(t){return(t.criteria?.length??0)+(t.notes??[]).filter(n=>n.kind==="criterion").length}function offScreenNumbers(t){let out20=new Set;for(let n=1;n<=doneWhenCount(t);n+=1)(t.offScreen??[]).some(o=>o.criterion===criterionText2(t,n))&&out20.add(n);return out20}function verdictOf(said){return said==="met"||said==="broken"||said==="not-shown"?said:said==="not-met"?"broken":said==="unclear"?"not-shown":null}var WORSE={met:0,"not-shown":1,broken:2};function checkVerdicts(answer2,asked,given=!1){let out20=new Map,line3=v=>typeof v=="string"?v.replace(/\s+/g," ").trim():"",list6=answer2?.checks;for(let c of Array.isArray(list6)?list6:[]){let o=c??{},n=Number(o.criterion);if(!asked.includes(n))continue;let said={shows:verdictOf(o.shows)??"not-shown",why:line3(o.why),advice:line3(o.advice),noChange:o.noChange===!0,...o.canShow===!1?{canShow:!1,canShowWhy:line3(o.canShowWhy)}:{}},had=out20.get(n);(!had||WORSE[said.shows]>WORSE[had.shows]||said.shows===had.shows&&had.canShow===!1&&said.canShow!==!1)&&out20.set(n,said)}if(!given)for(let n of asked)out20.has(n)||out20.set(n,{shows:"not-shown",why:"",advice:"",noChange:!1});return out20}var ABOUT_THE_PICTURE=new RegExp([/\b(?:not|n't)\s+(?:\w+\s+)?(?:in|within|inside)\s+(?:the\s+|this\s+|that\s+|these\s+|those\s+|any\s+of\s+the\s+|either\s+|any\s+)?(?:pictures?|screenshots?|images?|shots?|frames?|views?)\b/,/\bnot\s+in\s+(?:it|them|any\s+of\s+them)\b/,/\bin\s+(?:none|neither)\s+of\s+them\b/,/\b(?:none|neither)\s+of\s+(?:the\s+|these\s+|those\s+)?(?:pictures?|screenshots?|images?|shots?)\b/,/\bno\s+(?:picture|screenshot|image|shot)\s+shows?\b/,/\bmissing\s+from\s+(?:the\s+|this\s+|these\s+|those\s+)?(?:pictures?|screenshots?|images?|shots?)\b/,/\bout\s+of\s+(?:the\s+)?(?:pictures?|screenshots?|frames?|view|shots?)\b/,/\b(?:pictures?|screenshots?|images?|shots?)\s+(?:\w+\s+)?(?:(?:does|did|do|can|could)\s*(?:not|n't)|cannot|fails?\s+to)\s+(?:\w+\s+)?(?:show|tell|prove|include|capture|cover)\b/,/\b(?:pictures?|screenshots?|images?|shots?)\s+(?:is|was|are|were)\s*(?:not|n't)\b/,/\b(?:pictures?|screenshots?|images?|shots?)\s+(?:only\s+)?shows?\b[^.]*\bbut\s+not\b/,/\b(?:cannot|can't|could\s*(?:not|n't))\s+be\s+(?:seen|judged|told|confirmed)\b/,/\b(?:cannot|can't|could\s*(?:not|n't))\s+(?:tell|judge|confirm)\b/,/\b(?:i|we)\s+(?:cannot|can't|could\s*(?:not|n't))\s+see\b/,/\bbelow\s+the\s+(?:fold|visible\s+part)\b/,/\bstill\s+loading\b/,/\bat\s+the\s+required\b/].map(r=>r.source).join("|"),"i");function drawnFromPicture(why){return!!why?.trim()&&!ABOUT_THE_PICTURE.test(why)}var SAME_PICTURE="The after screenshot is the same as the before one, so it does not show the change.",SAME_ADVICE='Open the screen, state or data where the "done when" can be seen, not the same view as before the change, and do the steps that lead to its result.';function together(tester,check,same5=!1){let c=check??{shows:"not-shown",why:"",advice:"",noChange:!1};if(c.shows==="broken"&&drawnFromPicture(c.why))return{shows:"broken",why:c.why,advice:""};if(c.shows!=="met"&&c.canShow===!1)return{shows:"off-screen",why:c.canShowWhy||c.why,advice:""};if(same5&&!c.noChange)return{shows:"not-shown",why:SAME_PICTURE,advice:SAME_ADVICE};if(c.shows!=="met")return{shows:"not-shown",why:c.why,advice:c.advice};let said=tester.why??tester.note;return tester.shows==="broken"&&drawnFromPicture(said)?{shows:"broken",why:said,advice:""}:{shows:"met",why:"",advice:""}}function fromReason(reason,check){return check?.canShow===!1?{shows:"off-screen",why:check.canShowWhy||reason,advice:""}:{shows:"not-shown",why:check?.why??"",advice:check?.advice??""}}var NO_REASON=/\b(?:did\s*(?:not|n't)\s+try|not\s+tried|too\s+long|no\s+time|skipped\s+it)\b/i;function afterLastRetake(v,reason){let said=reason?.trim();return v.shows!=="not-shown"||!said||NO_REASON.test(said)||v.why===SAME_PICTURE?v:{shows:"off-screen",why:said,advice:""}}function setMain(row3,p){row3.after=p.after,p.page?row3.page=p.page:delete row3.page,p.device?row3.device=p.device:delete row3.device,p.note?row3.note=p.note:delete row3.note}function addPictures(row3,pictures,same5=(a,b)=>a===b){for(let p of pictures){let picture={after:p.file,...p.page?{page:p.page}:{},...p.device?{device:p.device}:{},...p.note?{note:p.note}:{}};if(!row3.after||same5(row3.after,p.file)){setMain(row3,picture);continue}let others=row3.more??=[],at3=others.findIndex(m=>same5(m.after,p.file));at3>=0?others[at3]=picture:others.push(picture)}row3.shows=["broken","not-shown","met"].find(s=>pictures.some(p=>p.shows===s));let broken=pictures.filter(p=>p.shows==="broken"&&p.note),said=broken.find(p=>drawnFromPicture(p.note))??broken[0];said?row3.why=said.note:delete row3.why}function makeMain(row3,at3){let next=row3.more[at3];row3.more[at3]={after:row3.after,...row3.page?{page:row3.page}:{},...row3.device?{device:row3.device}:{},...row3.note?{note:row3.note}:{}},setMain(row3,next)}function lowerFirst(s){return/^[A-Z][a-z]/.test(s)?`${s.charAt(0).toLowerCase()}${s.slice(1)}`:s}function firstSentence(s){let one2=s.split(/(?<=[.!?])\s+/)[0].trim();return/[.!?]$/.test(one2)?one2:`${one2}.`}function numbersWords(rows2){let n=rows2.map(r=>String(r.criterion));return n.length>1?`${n.slice(0,-1).join(", ")} and ${n.at(-1)}`:n[0]}function brokenStop(rows2){if(rows2.length===1){let r=rows2[0];return{question:`The screenshots show "done when" ${r.criterion} is not working yet: ${lowerFirst(firstSentence(r.why))}`,points:[`"Done when" ${r.criterion}: ${r.text}`]}}return{question:`The screenshots show ${rows2.length} "done when" are not working yet.`,points:rows2.map(r=>`"Done when" ${r.criterion}, ${r.text}: ${r.why}`)}}function notShownStop(rows2){return{question:`The squad could not show "done when" ${numbersWords(rows2)} on a screen. Try it yourself, or ask a question.`,points:rows2.map(r=>`"Done when" ${r.criterion}, ${r.text}: ${r.why}`)}}function retakeLine(n,text3,why,advice){let said=why.trim().replace(/[.!?]+$/,"");return`The after screenshot for "done when" ${n} ("${text3}") does not show it clearly yet${said?`: ${said}`:""}. ${advice.trim()?`Show it like this: ${advice.trim()}`:"Take it again so its result shows."}`}function reasonRefusedLine(n,text3,reason,why,advice){let said=why.trim().replace(/[.!?]+$/,"");return`You said "done when" ${n} ("${text3}") cannot be shown on a screen: ${reason.trim().replace(/[.!?]+$/,"")}. squad-kit's check says it can${said?`: ${said}`:""}. ${advice.trim()?`Show it like this: ${advice.trim()}`:"Take it now."}`}function cannotShowLine(n,text3,why){return`squad-kit's check says the rest of "done when" ${n} ("${text3}") cannot be shown: ${why.trim().replace(/[.!?]+$/,"")}. If you see a way to show it, take it now. If you agree, put it in offScreen with which part and why. Your pictures stay.`}function fixLine(r){return`"Done when" ${r.criterion} (${r.text}): ${r.why}`}function brokenRows(t){let out20=[];for(let n=1;n<=doneWhenCount(t);n+=1){let text3=criterionText2(t,n),row3=(t.proof??[]).find(p=>p.criterion===text3&&p.shows==="broken");text3&&row3&&out20.push({criterion:n,text:text3,why:row3.why??row3.note??"The screenshot shows it does not work as written."})}return out20}var STR={type:"string"},PROCESS_RULE="squad-kit runs the process: branches, commits, pushes, pull requests, reviews and releases. The repo's own rule files cover the code. Where one tells you to open your own pull request, commit, push or follow its own workflow, leave that part to squad-kit and carry on.",CHECKLIST={type:"array",items:{type:"object",properties:{n:{type:"integer"},answer:{type:"string",enum:["yes","no"]},why:STR},required:["n","answer"]}},COMMANDS={type:"object",properties:{setup:STR,test:STR,lint:STR,build:STR,start:STR,url:STR}},PARTS={type:"array",maxItems:10,items:{type:"object",properties:{id:STR,name:STR,what:STR,files:{type:"array",maxItems:4,items:STR},kind:{type:"string",enum:[...PART_KINDS]}},required:["id","name","what","files"]}},LINKS={type:"array",maxItems:20,items:{type:"object",properties:{from:STR,to:STR,label:STR},required:["from","to"]}},RUNS={type:"array",maxItems:30,items:{type:"object",properties:{id:STR,name:STR,shared:{type:"boolean"},repo:STR,folder:STR,needs:{type:"array",items:STR},url:STR},required:["id","name"]}},SCAN_SCHEMA={type:"object",properties:{empty:{type:"boolean"},summary:STR,map:{type:"array",items:{type:"object",properties:{area:STR,detail:STR},required:["area","detail"]}},decisions:{type:"array",items:{type:"object",properties:{title:STR,why:STR},required:["title","why"]}},commands:COMMANDS,repos:{type:"array",items:{type:"object",properties:{path:STR,summary:STR,commands:COMMANDS},required:["path","summary","commands"]}},users:STR,can:{type:"array",maxItems:8,items:STR},worth:{type:"array",maxItems:6,items:STR},ideas:{type:"array",maxItems:3,items:{type:"object",properties:{title:STR,why:STR},required:["title","why"]}},parts:PARTS,links:LINKS,runs:RUNS},required:["empty","summary","map","decisions","commands"]},BLUEPRINT_SCHEMA={type:"object",properties:{map:{type:"array",items:{type:"object",properties:{area:STR,detail:STR},required:["area","detail"]}},stack:{type:"array",items:{type:"object",properties:{name:STR,role:{type:"string",enum:[...TECH_ROLES]},why:STR,decision:STR},required:["name","why"]}},patterns:{type:"array",maxItems:10,items:{type:"object",properties:{name:STR,what:STR,where:{type:"array",maxItems:4,items:STR},decision:STR},required:["name","what","where"]}},parts:PARTS,links:LINKS},required:["map","stack","patterns"]},FIRST_BLUEPRINT_SCHEMA={...BLUEPRINT_SCHEMA,properties:{...BLUEPRINT_SCHEMA.properties,runs:RUNS,summary:STR,users:STR}},TECH_DECISION_SCHEMA={type:"object",properties:{title:STR,area:{type:"string",enum:[...DECISION_AREAS]},options:{type:"array",minItems:2,maxItems:3,items:STR},chosen:STR,why:STR,buildsOn:{type:"array",maxItems:4,items:STR}},required:["title","area","options","chosen","why","buildsOn"]},IMPACT_SCHEMA={type:"object",properties:{tickets:{type:"array",items:{type:"object",properties:{id:STR,affected:{type:"boolean"},why:STR,change:STR},required:["id","affected","why","change"]}}},required:["tickets"]},QUESTIONS_SCHEMA={type:"object",properties:{summary:STR,questions:{type:"array",maxItems:5,items:{type:"object",properties:{from:{type:"string",enum:["analyst","architect"]},kind:{type:"string",enum:["gap","clash"]},text:STR,options:{type:"array",minItems:2,maxItems:4,items:STR},typedOption:STR},required:["from","kind","text","options"]}}},required:["summary","questions"]},PLAN_SCHEMA={type:"object",properties:{story:STR,criteria:{type:"array",minItems:1,maxItems:6,items:{type:"object",properties:{given:STR,when:STR,then:STR},required:["given","when","then"]}},decision:{type:"object",properties:{title:STR,options:{type:"array",minItems:2,maxItems:3,items:{type:"object",properties:{name:STR,note:STR,chosen:{type:"boolean"}},required:["name","note","chosen"]}},why:STR,clientCheck:STR,replaces:STR,area:{type:"string",enum:[...DECISION_AREAS]}},required:["title","options","why","area"]},follows:{type:"array",maxItems:12,items:STR},dependsOn:{type:"array",maxItems:5,items:STR},steps:{type:"array",minItems:1,maxItems:8,items:STR},tests:{type:"integer"},screens:{type:"integer"},asks:{type:"integer"},releaseNote:STR,commands:COMMANDS,repo:STR,parts:{type:"array",maxItems:12,items:STR},mockup:STR},required:["story","criteria","steps","tests","screens","releaseNote"]},CRITERIA2={type:"array",minItems:1,maxItems:6,items:{type:"object",properties:{given:STR,when:STR,then:STR},required:["given","when","then"]}},SPEC_SCHEMA={type:"object",properties:{story:STR,criteria:CRITERIA2,releaseNote:STR},required:["story","criteria","releaseNote"]},CHANGE_SCHEMA={type:"object",properties:{summary:STR,size:{type:"string",enum:["small","big"]},why:STR,story:STR,criteria:{type:"array",maxItems:8,items:{type:"object",properties:{number:{type:"integer"},action:{type:"string",enum:["change","add","remove"]},given:STR,when:STR,then:STR},required:["number","action"]}}},required:["summary","size","why","story","criteria"]},CHANGE_DESIGN_SCHEMA={type:"object",properties:{summary:STR,steps:{type:"array",minItems:1,maxItems:10,items:{type:"object",properties:{title:STR,change:{type:"string",enum:["same","changed","new"]}},required:["title","change"]}}},required:["summary","steps"]},CHAT_SCHEMA={type:"object",properties:{reply:STR,ready:{type:"boolean"},options:{type:"array",maxItems:4,items:STR},files:{type:"array",maxItems:8,items:STR}},required:["reply","ready"]},QUESTION={type:"object",properties:{text:STR,options:{type:"array",maxItems:4,items:STR},why:STR,files:{type:"array",maxItems:6,items:STR},kind:{type:"string",enum:["gap","clash"]},sources:{type:"array",maxItems:4,items:{type:"object",properties:{text:STR,ticket:STR,section:STR,page:{type:"integer"}},required:["text"]}}},required:["text","kind"]},DECIDED={type:"array",maxItems:6,items:STR},DESIGN_SCHEMA={type:"object",properties:{...Object.fromEntries(Object.entries(PLAN_SCHEMA.properties).filter(([k])=>!["story","criteria","releaseNote"].includes(k))),question:QUESTION,decided:DECIDED},required:["steps","tests","screens"]},BUILD_SCHEMA={type:"object",properties:{summary:STR,testsAdded:{type:"integer"},notes:STR,question:QUESTION,decided:DECIDED},required:["summary"]},REVIEW_SCHEMA={type:"object",properties:{approved:{type:"boolean"},summary:STR,comments:{type:"array",items:STR},alsoChanged:{type:"array",items:STR},checklist:CHECKLIST,question:QUESTION,decided:DECIDED},required:["approved","summary","comments"]},PROOF_SCHEMA={type:"object",properties:{shots:{type:"array",maxItems:40,items:{type:"object",properties:{criterion:{type:"integer"},file:STR,shows:{type:"string",enum:["met","broken","not-shown"]},note:STR},required:["criterion","file","shows","note"]}},offScreen:{type:"array",maxItems:20,items:{type:"object",properties:{criterion:{type:"integer"},why:STR},required:["criterion","why"]}},checklist:CHECKLIST},required:["shots","offScreen"]},PROOF_CHECK_SCHEMA={type:"object",properties:{checks:{type:"array",maxItems:40,items:{type:"object",properties:{criterion:{type:"integer"},shows:{type:"string",enum:["met","broken","not-shown"]},why:STR,advice:STR,noChange:{type:"boolean"},canShow:{type:"boolean"},canShowWhy:STR},required:["criterion","shows","why","noChange"]}}},required:["checks"]},DRAFT_SCHEMA={type:"object",properties:{title:STR,ask:STR,kind:{type:"string",enum:["feature","bug"]},track:{type:"string",enum:["full","express"]},priority:{type:"string",enum:["high","normal","low"]},criteria:{type:"array",maxItems:8,items:STR},rules:{type:"array",maxItems:8,items:STR},edgeCases:{type:"array",maxItems:8,items:STR},gaps:{type:"array",maxItems:5,items:STR}},required:["title","ask","kind","track","priority","criteria","rules","edgeCases","gaps"]},PLAIN="Write for a project manager who may not be a developer: plain words, short sentences, no jargon.";function writtenInArabic(t){let letters=`${t.title} ${t.ask??""}`.replace(/[^\p{L}]/gu,""),arabic=letters.replace(/[^\u0600-\u06ff\u0750-\u077f\ufb50-\ufdff\ufe70-\ufeff]/g,"");return letters.length>0&&arabic.length*2>letters.length}function theirLanguage(t){return writtenInArabic(t)?`
679
679
  The person wrote this ticket in Arabic, so write in Arabic everything a person reads: questions and their options, summaries, the story, every "done when", the release note, the decision with its options and why, the steps, your notes, your findings and your answers to checklists. Keep code, file names, page addresses, commands and commit messages in English.`:""}var EXAMPLES_FIT='Each example in a "done when" must follow every other rule of the story: a date on an open day, a time inside opening hours, an amount under the limit, and so on.',LOOK=`How to look at the code: use your tools for reading a file, finding files by name and searching inside files
680
680
  (like Read, Glob or Find, and Grep). In the shell only simple read commands work (like cat, ls, grep, head, tail, wc,
681
681
  git log, git diff, git show and git status); anything else is refused, so do not try to run tests, builds, installs
@@ -1445,8 +1445,8 @@ ${event.text}`),note(bp,ticketId,`Posted the note on ${ref2}`);break}case"releas
1445
1445
  Reported on the squad-kit board as ${t.id}.`,severity:t.severity??"annoying",tags:t.foundIn?[t.foundIn]:[],related});return updateTicket(bp,t.id,x=>{x.source={type:tracker2.name,ref:bug.ref,id:bug.id,url:bug.url,itemType:bug.type||"Bug"},addActivity(x,"release",`Filed as Bug ${bug.ref} on ${tracker2.label}${related.length?`, linked to ${related.map(n=>tracker2.refOf(n)).join(", ")}`:""}`)}),!0}catch(err2){return refusedForGood(err2)?(saidToEngineers(bp,t.id,`${tracker2.label} refused a bug: ${err2.message}`),note(bp,t.id,`${tracker2.label} refused this bug, so squad-kit went on without it. It stays on this board`),!0):(again2||note(bp,t.id,`Could not file the bug on ${tracker2.label}: ${err2.message}. It is kept and filed again later`),!1)}}function trackerRetryDelay(tries){return Math.min(30*6e4,3e4*2**Math.max(0,tries-1))}function trackerDue(t){return!!t.trackerQueue?.length&&(!t.trackerRetry||Date.parse(t.trackerRetry.nextAt)<=Date.now())}async function flushTracker(bp,ticketId,tracker2=trackerFor(bp)){for(;;){let t=readTicket(bp,ticketId),event=t?.trackerQueue?.[0];if(!t||!event||!trackerDue(t))return;let sent=await tellTracker(bp,ticketId,event,tracker2,!!t.trackerRetry);try{updateTicket(bp,ticketId,x=>{if(!sent){let tries=(x.trackerRetry?.tries??0)+1;x.trackerRetry={tries,nextAt:new Date(Date.now()+trackerRetryDelay(tries)).toISOString()};return}JSON.stringify(x.trackerQueue?.[0])===JSON.stringify(event)&&x.trackerQueue.shift(),x.trackerQueue?.length||delete x.trackerQueue,delete x.trackerRetry})}catch{return}if(!sent)return}}function wikiDir(bp){return bp.wikiDir}async function syncWiki(bp,tracker2=trackerFor(bp),force=!1){if(!tracker2?.abilities.wiki)return 0;let dir=wikiDir(bp),stamp=path92.join(dir,".synced");if(!force&&fs100.existsSync(stamp)&&Date.now()-fs100.statSync(stamp).mtimeMs<10*6e4)return fs100.readdirSync(dir).filter(f=>f.endsWith(".md")).length;try{let pages=await tracker2.wikiPages();fs100.rmSync(dir,{recursive:!0,force:!0}),fs100.mkdirSync(dir,{recursive:!0});let taken=new Set;for(let p of pages){let name=p.path.replace(/^\//,"").replace(/[^\p{L}\p{M}\p{N}_\- ]+/gu,"-").trim().slice(-80),file3=freeName(`${/[\p{L}\p{N}]/u.test(name)?name:"Home"}.md`,taken);fs100.writeFileSync(path92.join(dir,file3),`<!-- ${tracker2.label} wiki page ${p.path} -->
1446
1446
  ${p.content.trim()}
1447
1447
  `,"utf8")}return fs100.writeFileSync(stamp,new Date().toISOString(),"utf8"),pages.length}catch{return 0}}function squadInside(bp,repoDir3){let rel=path92.relative(repoDir3,bp.squadDir);return rel&&!rel.startsWith("..")&&!path92.isAbsolute(rel)?rel:void 0}function repoTarget(bp,t){let meta=readMeta(bp),repo=repoFor(meta,t),dir=path92.join(projectRoot(bp),repo.path),remote=repo.remote||remotes(dir)[0];if(!remote)return null;let url=git(dir,["config","--get",`remote.${remote}.url`],{allowFail:!0});return url?{dir,base:repo.base,remote,url}:null}function opensPullRequest(bp,t){let meta=readMeta(bp);if(meta.engine!=="live"||!t.branch||meta.delivery==="push"||t.copies?.length)return!1;let target=repoTarget(bp,t);return!!(target&&repoHostFor(bp,target.url))}async function publishTicket(bp,ticketId){let t=requireTicket(bp,ticketId),target=repoTarget(bp,t),host3=target&&repoHostFor(bp,target.url);if(!target||!host3||!t.branch)throw new Error("This ticket has no code host with a saved sign-in to open a pull request with.");let env2=host3.gitEnv();await netGitLater(target.dir,["ls-remote","--heads",target.remote,target.base],{allowFail:!0,env:env2})||(await netGitLater(target.dir,["push",target.remote,`${target.base}:refs/heads/${target.base}`],{env:env2}),note(bp,ticketId,`Sent the first code to ${host3.label}, which was empty`));try{await netGitLater(t.worktree??target.dir,["push",target.remote,`${t.branch}:refs/heads/${t.branch}`],{env:env2})}catch(err2){throw/rejected|fast-forward|fetch first|before pushing again/i.test(err2.message)?new StepError(`${target.remote} has newer work on this ticket from someone else, and squad-kit does not write over it. Bring in their work, then press Try again.`,err2.message):err2}let tracker2=trackerFor(bp),item=tracker2?itemId(t,tracker2):null,pr=await host3.findPullRequest(t.branch);pr||(pr=await host3.openPullRequest({source:t.branch,target:target.base,title:t.title,description:prDescription(t,item,[]),items:item?[item]:[]}));let shots=[],rows2=t.proof??[],pictures=[...rows2.map(p=>({label:p.criterion,after:p.after})),...rows2.flatMap(p=>(p.more??[]).map(m=>({label:p.criterion,after:m.after})))].slice(0,6),sent=new Map;for(let shot of host3.abilities.attachImages?pictures:[]){let file3=path92.join(bp.proofDir,shot.after);if(!shot.after||!fs100.existsSync(file3))continue;sent.has(file3)||sent.set(file3,await host3.attachImage(pr.id,file3).catch(()=>null));let url=sent.get(file3);url&&shots.push({label:shot.label,url})}shots.length&&await host3.updateDescription(pr.id,prDescription(t,item,shots));let opened=pr;updateTicket(bp,ticketId,x=>{x.pr={id:opened.id,url:opened.url,repo:host3.repo,host:host3.host},delete x.clashedPr,delete x.prClosed,x.status=`Merging pull request ${opened.id}`,addActivity(x,"release",`Opened pull request ${opened.id} on ${host3.label}`)});let merged2=!1,movedLocal=!1,why="",clash=!1;try{await host3.merge(opened.id,`Merge ${t.title} (${t.id})`),merged2=!0;let head=await fetchedTip(target.dir,target.remote,target.base,{env:env2}),local=await baseTurn(target.dir,()=>moveBase(target.dir,target.base,head,squadInside(bp,target.dir)));local.note&&saidToEngineers(bp,ticketId,local.note),movedLocal=local.moved}catch(err2){why=err2.message,clash=await host3.mergeState(opened.id).catch(()=>null)==="clash"}if(updateTicket(bp,ticketId,x=>{if(x.live=!1,x.squad=null,x.progress=void 0,merged2){x.mergedAt=new Date().toISOString(),clearStaleStop(x);let ls=movedLocal?localSetupFor(bp,x):void 0;ls&&(x.localSetup=ls),x.status=`Merged into ${target.base} \xB7 goes out in ${x.release??"the next release"}`,addActivity(x,"release",`Merged pull request ${opened.id} into ${target.base}`)}else clash||(x.status=`PR ${opened.id} is waiting in ${host3.label} \xB7 ${x.release??"next release"}`,addActivity(x,"release",`Pull request ${opened.id} is open but not merged yet: ${why}`))}),merged2&&mapAfterMerge(bp),tracker2&&item&&!clash)try{let state=await tracker2.moveTo(item,"resolved");await tracker2.comment(item,merged2?`Merged into **${target.base}** by pull request [${opened.id}](${opened.url}), with the proof. It goes out in the next release.`:`Pull request [${opened.id}](${opened.url}) is open with the proof, waiting to be merged in ${host3.label}.`),state&&note(bp,ticketId,`Moved ${tracker2.refOf(item)} to ${state} on ${tracker2.label}`)}catch(err2){note(bp,ticketId,`Could not update ${tracker2.refOf(item)} on ${tracker2.label}: ${err2.message}`)}return{clash}}async function recheckMerge(bp,id2){let t=readTicket(bp,id2),wait=mergeWait(t);if(!t||!wait)return"waiting";let base2=repoFor(readMeta(bp),t).base,line3,moved=!1;if(wait==="pr"){let target=repoTarget(bp,t),host3=target&&repoHostFor(bp,target.url);if(!target||!host3||!t.pr)return"waiting";let state=await host3.mergeState(t.pr.id).catch(()=>null);if(state==="clash"||state==="closed")return state;if(state!=="merged"&&!(state==="mergeable"&&await host3.merge(t.pr.id,`Merge ${t.title} (${t.id})`).then(()=>!0,()=>!1)))return"waiting";moved=await followBase(bp,id2,target,host3),line3=`Pull request ${t.pr.id} was approved and merged on ${host3.label}`}else{let dirs=copyViews(readMeta(bp),t).map(view=>path92.join(projectRoot(bp),repoFor(readMeta(bp),view).path)),went=await baseTurn(dirs,()=>{if(!mergeWait(readTicket(bp,id2)))return!1;for(let view of copyViews(readMeta(bp),t))if(!mergeLocally(bp,view)?.moved)return!1;return!0}).catch(err2=>err2 instanceof MergeClash?"clash":!1);if(went==="clash")return"clash";if(!went)return"waiting";moved=!0,line3=`Merged into ${base2}: your own copy of ${base2} was free again`}return updateTicket(bp,id2,x=>{if(x.mergedAt)return;x.mergedAt=new Date().toISOString(),clearStaleStop(x);let ls=moved?localSetupFor(bp,x):void 0;ls&&(x.localSetup=ls),x.status=`Merged into ${base2} \xB7 goes out in ${x.release??"the next release"}`,addActivity(x,"release",line3)}),mapAfterMerge(bp),"merged"}async function followBase(bp,id2,target,host3){try{let head=await fetchedTip(target.dir,target.remote,target.base,{env:host3.gitEnv()}),local=await baseTurn(target.dir,()=>moveBase(target.dir,target.base,head,squadInside(bp,target.dir)));return local.note&&saidToEngineers(bp,id2,local.note),local.moved}catch{return!1}}async function trackerMerged(bp,ticketId,pr,base2,tracker2=trackerFor(bp)){let t=readTicket(bp,ticketId),item=t&&tracker2?itemId(t,tracker2):null;if(!(!tracker2||!item||readMeta(bp).engine!=="live"))try{let state=await tracker2.moveTo(item,"resolved");await tracker2.comment(item,`Merged into **${base2}** by pull request [${pr.id}](${pr.url}), with the proof. It goes out in the next release.`),state&&note(bp,ticketId,`Moved ${tracker2.refOf(item)} to ${state} on ${tracker2.label}`)}catch(err2){note(bp,ticketId,`Could not update ${tracker2.refOf(item)} on ${tracker2.label}: ${err2.message}`)}}function mapAfterMerge(bp){tidyStoryDbsBeside(bp,Date.now(),!0),overviewAfterMerge(bp),firstCodeMerged(bp);let meta=readMeta(bp);meta.blueprint?.status==="done"&&!filesMoved(projectRoot(bp),meta,meta.blueprint,bp.squadDir)||codeChanged(bp)}function localSetupFor(bp,t){let repo=repoFor(readMeta(bp),t),command=repo.commands.setup?.trim(),dir=path92.join(projectRoot(bp),repo.path);if(command){try{if(currentBranch(dir)!==repo.base)return}catch{return}return{status:"pending",dir,command}}}function mergeLocally(bp,t){if(!t.branch)return null;let meta=readMeta(bp),repo=repoFor(meta,t),dir=path92.join(projectRoot(bp),repo.path),plain=meta.delivery==="push",r=mergeIntoBase(dir,repo.base,t.branch,plain?`${t.id}: ${t.title}`:`Merge ${t.title} (${t.id})`,squadInside(bp,dir),plain);return{commit:r.commit,base:repo.base,moved:r.moved,...r.note?{note:r.note}:{}}}function prDescription(t,item,shots){let lines3=[];if(t.story&&lines3.push(t.story,""),item&&lines3.push(`Work item: #${item}`,""),t.criteria?.length){lines3.push("### Done when");for(let c of t.criteria)lines3.push(`- [x] **Given** ${c.given}, **when** ${c.when}, **then** ${c.then}`);lines3.push("")}let g=t.gates;if(g){lines3.push("### Checks"),lines3.push(`- Tests, lint and build: ${g.checks==="pass"?"pass":g.checks==="skipped"?"failed, continued anyway":g.checks==="unchecked"?"not checked, no tests ran":g.checks}${g.checksText?` (${g.checksText})`:""}`);for(let o of g.oldFailures??[])lines3.push(` - Fails on main too, without this change: ${o.title} (${o.file})${o.story?`; ${o.story} fixes it`:""}`);lines3.push(`- Code review: ${g.review==="pass"?"approved":g.review==="skipped"?"notes still open, continued anyway":g.review}${t.reviewRound?` after ${t.reviewRound} round${t.reviewRound>1?"s":""}`:""}`),lines3.push(`- Proof: ${g.proof==="pass"?`checked by ${t.createdBy}`:g.proof==="skipped"?"no screenshots, continued anyway":g.proof}`),t.noNewTests&&lines3.push(`- ${NO_NEW_TESTS_LABEL}: the project's own checks ran, and this story added no tests`),lines3.push("")}if(t.overrides?.length){lines3.push("### Continued anyway");for(let o of t.overrides)lines3.push(`- **${o.added?`Without your team's check "${o.added}"`:o.weakened?.length?"Weakened tests":o.gate==="hooks"?"Saved without the project's own check":o.gate==="checks"?"Failed checks":o.gate==="proof"?"No screenshots":"Open review notes"}**, by ${o.by}: ${o.reason}`);lines3.push("")}if(shots.length){lines3.push("### Screenshots");for(let s of shots)lines3.push(`**${s.label}**`,"",`![${s.label.replace(/[[\]]/g,"")}](${s.url})`,"")}return lines3.push(`Ticket ${t.id} on the squad-kit board.`),lines3.join(`
1448
- `)}import{execFileSync as execFileSync10}from"child_process";import{performance}from"perf_hooks";function parseClock(s){let[days,rest]=s.includes("-")?s.split("-"):["0",s],parts2=(rest??"").split(":").map(p=>parseFloat(p)),sec=0;for(let p of parts2)sec=sec*60+(Number.isFinite(p)?p:0);return Number(days)*86400+sec}function processTable(){if(process.platform==="win32")return[];try{return execFileSync10("ps",["-A","-o","pid=,ppid=,etime=,time=,command="],{encoding:"utf8",maxBuffer:8388608}).split(`
1449
- `).map(l=>l.trim().match(/^(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(.*)$/)).filter(m=>!!m).map(m=>({pid:Number(m[1]),ppid:Number(m[2]),elapsedSec:parseClock(m[3]),cpuSec:parseClock(m[4]),command:m[5]}))}catch{return[]}}function descendants(rows2,root){let out20=[],queue2=[root];for(;queue2.length;){let parent=queue2.shift();for(let r of rows2)r.ppid===parent&&(out20.push(r),queue2.push(r.pid))}return out20}var SHELL=/^(\S*\/)?(zsh|bash|sh|dash|fish)(\s|$)/,SHELL_OR_PIPE=/^(\S*\/)?(zsh|bash|sh|dash|fish)(\s|$)|^(\S*\/)?(tail|head|tee|cat|sleep)(\s|$)/;function waitingOn(rows2,root){let byPid=new Map(rows2.map(r=>[r.pid,r])),fromShell=r=>{for(let up=byPid.get(r.ppid);up;up=up.pid===root?void 0:byPid.get(up.ppid))if(SHELL.test(up.command))return!0;return!1},kids=descendants(rows2,root).filter(r=>!SHELL_OR_PIPE.test(r.command)&&fromShell(r));return kids.length?kids.reduce((a,b)=>b.elapsedSec<=a.elapsedSec?b:a):null}function short2(command){let c=command.replace(/^\S*\/(node|python3?)\s+/,"").replace(/^\S*\/([\w.-]+)/,"$1");return c.length>70?`${c.slice(0,69)}\u2026`:c}function mins(sec){let m=Math.max(1,Math.round(sec/60));return`${m} minute${m===1?"":"s"}`}var QUIET_MS=3*6e4,IDLE_MS=2*6e4,LONG_MS=12*6e4,SLEEP_MS=6e4,NO_LOOK_MS=10*6e4,Watchdog=class{constructor(quietMs=QUIET_MS,idleMs=IDLE_MS,longMs=LONG_MS,table=processTable){this.quietMs=quietMs;this.idleMs=idleMs;this.longMs=longMs;this.table=table}quietMs;idleMs;longMs;table;beats=new Map;cpu=new Map;wall=Date.now();awake=performance.now();resumed(now3=Date.now(),awake=performance.now()){let wallGap=now3-this.wall,ranGap=awake-this.awake;this.wall=now3,this.awake=awake;let slept=Math.max(wallGap-ranGap,wallGap>=NO_LOOK_MS?wallGap:0);if(slept<SLEEP_MS)return 0;for(let b of this.beats.values())b.at=Math.min(now3,b.at+slept);for(let c of this.cpu.values())c.at=Math.min(now3,c.at+slept),c.idleSince=Math.min(now3,c.idleSince+slept);return slept}beat(key,pid){let prev=this.beats.get(key);this.beats.set(key,{at:Date.now(),pid:pid??prev?.pid})}track(key,pid){this.beats.set(key,{at:this.beats.get(key)?.at??Date.now(),pid})}forget(key){this.beats.delete(key)}check(key,now3=Date.now(),rows2){let b=this.beats.get(key);if(!b)return null;let quietFor=now3-b.at;if(quietFor<this.quietMs)return null;let since=new Date(b.at).toISOString(),table=rows2??this.table(),proc=b.pid?waitingOn(table,b.pid):null,root=b.pid?table.find(r=>r.pid===b.pid):void 0,watched=proc??root;if(!watched)return quietFor>=this.longMs?{since,kind:"long",detail:`No progress for ${mins(quietFor/1e3)}.`}:null;let seen=this.cpu.get(watched.pid);(!seen||watched.cpuSec-seen.cpuSec>.5)&&this.cpu.set(watched.pid,{cpuSec:watched.cpuSec,at:now3,idleSince:now3});let idleFor=now3-(this.cpu.get(watched.pid)?.idleSince??now3),what=proc?"A command":"The AI tool",command=proc?short2(proc.command):"";return idleFor>=this.idleMs?{since,kind:"stuck",detail:proc?`${what} has run for ${mins(proc.elapsedSec)} and has done nothing for ${mins(idleFor/1e3)}. It may be waiting on the network or on a question.`:`${what} has not answered for ${mins(quietFor/1e3)}. It may be waiting on the model or the network.`,...proc?{command,pid:proc.pid}:{}}:quietFor>=this.longMs?{since,kind:"long",detail:`${what} has been working for ${mins(quietFor/1e3)} with no other progress.`,...proc?{command,pid:proc.pid}:{}}:null}};function endProcessTree(pid,rows2=processTable()){let all=[...descendants(rows2,pid).reverse(),{pid}],ended=0;for(let p of all)try{process.kill(p.pid,"SIGTERM"),ended+=1}catch{}return ended}import fs101 from"fs";import os14 from"os";import path93 from"path";import{execFile as execFile4}from"child_process";import{promisify}from"util";var run3=promisify(execFile4),AUDIO_FILE=/\.(ogg|opus|oga|m4a|mp3|wav|aac|amr|flac|weba)$/i,VIDEO_FILE=/\.(mp4|mov|m4v|webm)$/i,MAX_NOTE_BYTES=100*1024*1024;function isMediaNote(file3){return AUDIO_FILE.test(file3)||VIDEO_FILE.test(file3)}function videoNotesWork(platform=process.platform){return platform==="darwin"}var NoteTrouble=class extends Error{};function noteTroubleWords(err2){return err2 instanceof NoteTrouble||err2 instanceof ModuleError&&err2.kind!=="other"?err2.message:"something went wrong with it, so nobody can hear it for now"}function transcriptOf(file3){return`${file3}.transcript.txt`}async function prepareMedia(bp,files,opts={}){let notes=files.filter(f=>isMediaNote(f)&&!fs101.existsSync(transcriptOf(f))),result={added:[],notes:[],missingKey:!1};if(!notes.length)return result;let speech=speechFor(bp,{...opts.key?{key:opts.key}:{},...opts.fetch?{fetch:opts.fetch}:{}});if(!speech)return result.missingKey=!0,result.notes.push(`${notes.length===1?"1 voice or video note is":`${notes.length} voice or video notes are`} attached, but nobody can listen to ${notes.length===1?"it":"them"}. Add a ${speechModule().label} key in Settings`),result;for(let note2 of notes){let name=path93.basename(note2),tmp=fs101.mkdtempSync(path93.join(os14.tmpdir(),"squad-media-"));try{if(fs101.statSync(note2).size>MAX_NOTE_BYTES)throw new NoteTrouble("it is larger than 100 MB");let audio=note2;if(VIDEO_FILE.test(note2)){audio=path93.join(tmp,"sound.m4a"),await(opts.extractAudio??avconvert)(note2,audio);try{let frames=await(opts.extractFrames??swiftFrames)(note2,`${note2}.screen`);result.added.push(...frames)}catch{}}let text3=await speech.transcribe(audio);fs101.writeFileSync(transcriptOf(note2),`Transcript of ${name} (written out by ${speech.label} speech to text, may have small mistakes):
1448
+ `)}import{execFileSync as execFileSync10}from"child_process";import os14 from"os";import{performance}from"perf_hooks";var BUSY_LOAD=1.5;function computerBusy(load=os14.loadavg()[0]??0,cores=os14.cpus().length){return cores>0&&load>=cores*BUSY_LOAD}function parseClock(s){let[days,rest]=s.includes("-")?s.split("-"):["0",s],parts2=(rest??"").split(":").map(p=>parseFloat(p)),sec=0;for(let p of parts2)sec=sec*60+(Number.isFinite(p)?p:0);return Number(days)*86400+sec}function processTable(){if(process.platform==="win32")return[];try{return execFileSync10("ps",["-A","-o","pid=,ppid=,etime=,time=,command="],{encoding:"utf8",maxBuffer:8388608}).split(`
1449
+ `).map(l=>l.trim().match(/^(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(.*)$/)).filter(m=>!!m).map(m=>({pid:Number(m[1]),ppid:Number(m[2]),elapsedSec:parseClock(m[3]),cpuSec:parseClock(m[4]),command:m[5]}))}catch{return[]}}function descendants(rows2,root){let out20=[],queue2=[root];for(;queue2.length;){let parent=queue2.shift();for(let r of rows2)r.ppid===parent&&(out20.push(r),queue2.push(r.pid))}return out20}var SHELL=/^(\S*\/)?(zsh|bash|sh|dash|fish)(\s|$)/,SHELL_OR_PIPE=/^(\S*\/)?(zsh|bash|sh|dash|fish)(\s|$)|^(\S*\/)?(tail|head|tee|cat|sleep)(\s|$)/;function waitingOn(rows2,root){let byPid=new Map(rows2.map(r=>[r.pid,r])),fromShell=r=>{for(let up=byPid.get(r.ppid);up;up=up.pid===root?void 0:byPid.get(up.ppid))if(SHELL.test(up.command))return!0;return!1},kids=descendants(rows2,root).filter(r=>!SHELL_OR_PIPE.test(r.command)&&fromShell(r));return kids.length?kids.reduce((a,b)=>b.elapsedSec<=a.elapsedSec?b:a):null}function short2(command){let c=command.replace(/^\S*\/(node|python3?)\s+/,"").replace(/^\S*\/([\w.-]+)/,"$1");return c.length>70?`${c.slice(0,69)}\u2026`:c}function mins(sec){let m=Math.max(1,Math.round(sec/60));return`${m} minute${m===1?"":"s"}`}var QUIET_MS=3*6e4,IDLE_MS=2*6e4,LONG_MS=12*6e4,SLEEP_MS=6e4,NO_LOOK_MS=10*6e4,Watchdog=class{constructor(quietMs=QUIET_MS,idleMs=IDLE_MS,longMs=LONG_MS,table=processTable){this.quietMs=quietMs;this.idleMs=idleMs;this.longMs=longMs;this.table=table}quietMs;idleMs;longMs;table;beats=new Map;cpu=new Map;wall=Date.now();awake=performance.now();resumed(now3=Date.now(),awake=performance.now()){let wallGap=now3-this.wall,ranGap=awake-this.awake;this.wall=now3,this.awake=awake;let slept=Math.max(wallGap-ranGap,wallGap>=NO_LOOK_MS?wallGap:0);if(slept<SLEEP_MS)return 0;for(let b of this.beats.values())b.at=Math.min(now3,b.at+slept);for(let c of this.cpu.values())c.at=Math.min(now3,c.at+slept),c.idleSince=Math.min(now3,c.idleSince+slept);return slept}beat(key,pid){let prev=this.beats.get(key);this.beats.set(key,{at:Date.now(),pid:pid??prev?.pid})}track(key,pid){this.beats.set(key,{at:this.beats.get(key)?.at??Date.now(),pid})}forget(key){this.beats.delete(key)}check(key,now3=Date.now(),rows2){let b=this.beats.get(key);if(!b)return null;let quietFor=now3-b.at;if(quietFor<this.quietMs)return null;let since=new Date(b.at).toISOString(),table=rows2??this.table(),proc=b.pid?waitingOn(table,b.pid):null,root=b.pid?table.find(r=>r.pid===b.pid):void 0,watched=proc??root;if(!watched)return quietFor>=this.longMs?{since,kind:"long",detail:`No progress for ${mins(quietFor/1e3)}.`}:null;let seen=this.cpu.get(watched.pid);(!seen||watched.cpuSec-seen.cpuSec>.5)&&this.cpu.set(watched.pid,{cpuSec:watched.cpuSec,at:now3,idleSince:now3});let idleFor=now3-(this.cpu.get(watched.pid)?.idleSince??now3),what=proc?"A command":"The AI tool",command=proc?short2(proc.command):"";return idleFor>=this.idleMs?{since,kind:"stuck",detail:proc?`${what} has run for ${mins(proc.elapsedSec)} and has done nothing for ${mins(idleFor/1e3)}. It may be waiting on the network or on a question.`:`${what} has not answered for ${mins(quietFor/1e3)}. It may be waiting on the model or the network.`,...proc?{command,pid:proc.pid}:{}}:quietFor>=this.longMs?{since,kind:"long",detail:`${what} has been working for ${mins(quietFor/1e3)} with no other progress.`,...proc?{command,pid:proc.pid}:{}}:null}};function endProcessTree(pid,rows2=processTable()){let all=[...descendants(rows2,pid).reverse(),{pid}],ended=0;for(let p of all)try{process.kill(p.pid,"SIGTERM"),ended+=1}catch{}return ended}import fs101 from"fs";import os15 from"os";import path93 from"path";import{execFile as execFile4}from"child_process";import{promisify}from"util";var run3=promisify(execFile4),AUDIO_FILE=/\.(ogg|opus|oga|m4a|mp3|wav|aac|amr|flac|weba)$/i,VIDEO_FILE=/\.(mp4|mov|m4v|webm)$/i,MAX_NOTE_BYTES=100*1024*1024;function isMediaNote(file3){return AUDIO_FILE.test(file3)||VIDEO_FILE.test(file3)}function videoNotesWork(platform=process.platform){return platform==="darwin"}var NoteTrouble=class extends Error{};function noteTroubleWords(err2){return err2 instanceof NoteTrouble||err2 instanceof ModuleError&&err2.kind!=="other"?err2.message:"something went wrong with it, so nobody can hear it for now"}function transcriptOf(file3){return`${file3}.transcript.txt`}async function prepareMedia(bp,files,opts={}){let notes=files.filter(f=>isMediaNote(f)&&!fs101.existsSync(transcriptOf(f))),result={added:[],notes:[],missingKey:!1};if(!notes.length)return result;let speech=speechFor(bp,{...opts.key?{key:opts.key}:{},...opts.fetch?{fetch:opts.fetch}:{}});if(!speech)return result.missingKey=!0,result.notes.push(`${notes.length===1?"1 voice or video note is":`${notes.length} voice or video notes are`} attached, but nobody can listen to ${notes.length===1?"it":"them"}. Add a ${speechModule().label} key in Settings`),result;for(let note2 of notes){let name=path93.basename(note2),tmp=fs101.mkdtempSync(path93.join(os15.tmpdir(),"squad-media-"));try{if(fs101.statSync(note2).size>MAX_NOTE_BYTES)throw new NoteTrouble("it is larger than 100 MB");let audio=note2;if(VIDEO_FILE.test(note2)){audio=path93.join(tmp,"sound.m4a"),await(opts.extractAudio??avconvert)(note2,audio);try{let frames=await(opts.extractFrames??swiftFrames)(note2,`${note2}.screen`);result.added.push(...frames)}catch{}}let text3=await speech.transcribe(audio);fs101.writeFileSync(transcriptOf(note2),`Transcript of ${name} (written out by ${speech.label} speech to text, may have small mistakes):
1450
1450
 
1451
1451
  ${text3}
1452
1452
  `),result.added.push(transcriptOf(note2)),result.notes.push(`Wrote out ${name}`)}catch(err2){writeLog({level:"warn",text:`Could not write out ${name}: ${err2.message}`}),result.notes.push(`Could not write out ${name}: ${noteTroubleWords(err2)}`)}finally{fs101.rmSync(tmp,{recursive:!0,force:!0})}}return result}async function avconvert(video,out20){if(!videoNotesWork())throw new NoteTrouble("taking the sound out of a video needs a Mac for now");await run3("avconvert",["-s",video,"-p","PresetAppleM4A","-o",out20,"--replace"],{timeout:5*6e4})}var FRAMES_SWIFT=`import AVFoundation
@@ -1467,26 +1467,26 @@ for i in 0..<count {
1467
1467
  try! data.write(to: URL(fileURLWithPath: name))
1468
1468
  print(name)
1469
1469
  }
1470
- `;async function swiftFrames(video,outPrefix){if(!videoNotesWork())return[];let dir=fs101.mkdtempSync(path93.join(os14.tmpdir(),"squad-frames-"));try{let script=path93.join(dir,"frames.swift");fs101.writeFileSync(script,FRAMES_SWIFT);let{stdout:out20}=await run3("swift",[script,video,outPrefix],{encoding:"utf8",timeout:3*6e4});return out20.split(`
1471
- `).map(l=>l.trim()).filter(l=>l&&fs101.existsSync(l))}finally{fs101.rmSync(dir,{recursive:!0,force:!0})}}import{execFileSync as execFileSync11}from"child_process";import fs103 from"fs";import path95 from"path";import yaml16 from"js-yaml";import fs102 from"fs";import os15 from"os";import path94 from"path";import yaml15 from"js-yaml";var DELETED_NOTE="deleted.md";function branchOf(t){return t.branch??`squad/${t.id.toLowerCase()}`}function merged(t){return!!t.mergedAt||t.stage==="done"||t.stage==="release"}function noteText(t,by2,at3){return`${yaml15.dump({deleted:{id:t.id,by:by2,at:at3}},{lineWidth:200}).replace(/^/,`---
1470
+ `;async function swiftFrames(video,outPrefix){if(!videoNotesWork())return[];let dir=fs101.mkdtempSync(path93.join(os15.tmpdir(),"squad-frames-"));try{let script=path93.join(dir,"frames.swift");fs101.writeFileSync(script,FRAMES_SWIFT);let{stdout:out20}=await run3("swift",[script,video,outPrefix],{encoding:"utf8",timeout:3*6e4});return out20.split(`
1471
+ `).map(l=>l.trim()).filter(l=>l&&fs101.existsSync(l))}finally{fs101.rmSync(dir,{recursive:!0,force:!0})}}import{execFileSync as execFileSync11}from"child_process";import fs103 from"fs";import path95 from"path";import yaml16 from"js-yaml";import fs102 from"fs";import os16 from"os";import path94 from"path";import yaml15 from"js-yaml";var DELETED_NOTE="deleted.md";function branchOf(t){return t.branch??`squad/${t.id.toLowerCase()}`}function merged(t){return!!t.mergedAt||t.stage==="done"||t.stage==="release"}function noteText(t,by2,at3){return`${yaml15.dump({deleted:{id:t.id,by:by2,at:at3}},{lineWidth:200}).replace(/^/,`---
1472
1472
  `)}---
1473
1473
 
1474
1474
  ${by2} deleted ${t.id} "${t.title}" on their computer. squad-kit left this note so the team's boards show it as deleted. The code on this branch is kept as it was.
1475
- `}function readNote2(repo,ref2,file3){let text3=git(repo,["cat-file","blob",`${ref2}:${file3}`],{allowFail:!0}),head=/^---\n([\s\S]*?)\n---/.exec(text3)?.[1];if(!head)return null;try{let d=yaml15.load(head)?.deleted;return typeof d?.by=="string"&&d.by.trim()?{by:d.by.trim(),at:typeof d.at=="string"?d.at:now()}:null}catch{return null}}function deleteNoteCommit(repo,tip,squad,t,by2,at3){let folder=`${squad}/tickets/${t.id}`;if(!git(repo,["ls-tree","--name-only",tip,"--",`${folder}/`],{allowFail:!0})||readNote2(repo,tip,`${folder}/${DELETED_NOTE}`))return null;let tmp=fs102.mkdtempSync(path94.join(os15.tmpdir(),"squad-note-"));try{let env2={GIT_INDEX_FILE:path94.join(tmp,"index")},file3=path94.join(tmp,DELETED_NOTE);fs102.writeFileSync(file3,noteText(t,by2,at3),"utf8"),git(repo,["read-tree",tip],{env:env2}),git(repo,["rm","--cached","-r","-q","--ignore-unmatch","--",folder],{env:env2});let blob3=git(repo,["hash-object","-w",file3]);git(repo,["update-index","--add","--cacheinfo",`100644,${blob3},${folder}/${DELETED_NOTE}`],{env:env2});let tree=git(repo,["write-tree"],{env:env2});return git(repo,[...commitArgsFor(repo),"commit-tree",tree,"-p",tip,"-m",`${t.id}: deleted by ${by2}`],{env:env2})}finally{fs102.rmSync(tmp,{recursive:!0,force:!0})}}function remoteBranches(repo){let out20=new Map;for(let ref2 of git(repo,["for-each-ref","--format=%(refname)","refs/remotes/*/squad/*"],{allowFail:!0}).split(`
1475
+ `}function readNote2(repo,ref2,file3){let text3=git(repo,["cat-file","blob",`${ref2}:${file3}`],{allowFail:!0}),head=/^---\n([\s\S]*?)\n---/.exec(text3)?.[1];if(!head)return null;try{let d=yaml15.load(head)?.deleted;return typeof d?.by=="string"&&d.by.trim()?{by:d.by.trim(),at:typeof d.at=="string"?d.at:now()}:null}catch{return null}}function deleteNoteCommit(repo,tip,squad,t,by2,at3){let folder=`${squad}/tickets/${t.id}`;if(!git(repo,["ls-tree","--name-only",tip,"--",`${folder}/`],{allowFail:!0})||readNote2(repo,tip,`${folder}/${DELETED_NOTE}`))return null;let tmp=fs102.mkdtempSync(path94.join(os16.tmpdir(),"squad-note-"));try{let env2={GIT_INDEX_FILE:path94.join(tmp,"index")},file3=path94.join(tmp,DELETED_NOTE);fs102.writeFileSync(file3,noteText(t,by2,at3),"utf8"),git(repo,["read-tree",tip],{env:env2}),git(repo,["rm","--cached","-r","-q","--ignore-unmatch","--",folder],{env:env2});let blob3=git(repo,["hash-object","-w",file3]);git(repo,["update-index","--add","--cacheinfo",`100644,${blob3},${folder}/${DELETED_NOTE}`],{env:env2});let tree=git(repo,["write-tree"],{env:env2});return git(repo,[...commitArgsFor(repo),"commit-tree",tree,"-p",tip,"-m",`${t.id}: deleted by ${by2}`],{env:env2})}finally{fs102.rmSync(tmp,{recursive:!0,force:!0})}}function remoteBranches(repo){let out20=new Map;for(let ref2 of git(repo,["for-each-ref","--format=%(refname)","refs/remotes/*/squad/*"],{allowFail:!0}).split(`
1476
1476
  `).filter(Boolean)){let branch=ref2.replace(/^refs\/remotes\/[^/]+\//,"");out20.set(branch,[...out20.get(branch)??[],ref2])}return out20}function hasFile(repo,ref2,file3){return!!git(repo,["rev-parse","--verify","-q",`${ref2}:${file3}`],{allowFail:!0})}function spotOwnerDeletes(bp,opts){let repo=docsRepo(bp);if(!repo||!docsInGit(bp))return[];let meta=readMeta(bp),squad=squadInRepo(bp,repo),base2=meta.git?.base||"main",bases=[`refs/heads/${base2}`,...git(repo,["for-each-ref","--format=%(refname)",`refs/remotes/*/${base2}`],{allowFail:!0}).split(`
1477
1477
  `).filter(Boolean)],pushed=remoteBranches(repo),marked=[];for(let t of listTickets(bp)){if(isMine(t,meta)||t.archived||t.deleting||merged(t))continue;let branch=branchOf(t),folder=`${squad}/tickets/${t.id}`,refs=pushed.get(branch)??[],note2=refs.map(r=>readNote2(repo,r,`${folder}/${DELETED_NOTE}`)).find(Boolean)??null,found2=null;if(note2)found2={by:note2.by,at:note2.at,how:"note",branch};else if(refs.some(r=>hasFile(repo,r,`${folder}/ticket.md`))||bases.some(b=>hasFile(repo,b,`${folder}/ticket.md`))){let seen=refs[0]?.replace(/^refs\/remotes\//,"")??base2;(t.seenOn!==seen||t.ownerDeleted?.how==="gone")&&updateTicket(bp,t.id,x=>{x.seenOn=seen,x.ownerDeleted?.how==="gone"&&(delete x.ownerDeleted,x.status=`With ${ownerOf(x)}`,addActivity(x,"squad",`${branch} is back on the remote, so ${ownerOf(x)} still has it`))});continue}else opts.fetched&&t.seenOn&&!refs.length&&(found2={by:ownerOf(t),at:now(),how:"gone",branch});if(!found2||t.ownerDeleted&&t.ownerDeleted.how===found2.how&&t.ownerDeleted.by===found2.by)continue;let mark=found2;updateTicket(bp,t.id,x=>{x.ownerDeleted=mark,x.status=`Deleted by ${mark.by}`,x.waiting=null,x.live=!1,x.squad=null,addActivity(x,"squad",mark.how==="note"?`${mark.by} deleted it on their computer. Remove it from your board when you like: that cleans this computer only`:`${branch} is gone from the remote and ${x.id} never reached ${base2}, so ${mark.by} deleted it. Remove it from your board when you like: that cleans this computer only`)}),marked.push(t.id)}return marked}function localCopy(dir,t,base2,copyDir2){let out20={kept:[]};if(!isRepo(dir))return out20;let branch=branchOf(t),hasCopy=fs102.existsSync(copyDir2),dirty=hasCopy&&!!git(copyDir2,["status","--porcelain"],{allowFail:!0});hasCopy&&!dirty&&(out20.copy=copyDir2),dirty&&out20.kept.push(`Its copy of the code at ${copyDir2}, because it has changes that are not committed`);let tip=git(dir,["rev-parse","--verify","-q",`refs/heads/${branch}`],{allowFail:!0});return tip&&((!!git(dir,["for-each-ref","--count=1","--contains",tip,"--format=%(refname)","refs/remotes/"],{allowFail:!0})||isAncestor(dir,tip,base2))&&!dirty?out20.branch=branch:dirty||out20.kept.push(`The branch ${branch} on this computer, because it has commits that are on no remote. If you no longer need them: git branch -D ${branch}`)),out20}var DOC_FILES2=["ticket.md","spec.md","questions.md","design.md","notes.md","proof.md"];function gitEnv2(bp,t){let target=repoTarget(bp,t);return{GIT_TERMINAL_PROMPT:"0",...target?repoHostFor(bp,target.url)?.gitEnv()??{}:{}}}async function fetchBase(bp,t){let target=repoTarget(bp,t);if(target)return await netGitLater(target.dir,["fetch","--quiet",target.remote,target.base],{env:gitEnv2(bp,t),timeoutMs:6e4}).catch(()=>{}),target.remote}async function pushHandOver(bp,t){if(!docsRepo(bp))throw new Error("This project's documents sit outside its code repos, so they cannot travel with a branch yet. Share the .squad folder another way.");if(!docsInGit(bp))throw new Error('"Put project documents in git" is off for this project, so the documents cannot travel with the branch. Turn it on in Settings first.');let target=repoTarget(bp,t);if(!target)throw new Error("This project has no shared copy of the code to send it to. An engineer can add one.");let env2=gitEnv2(bp,t),had=!!(t.worktree&&fs103.existsSync(t.worktree)),copy2=had?{dir:t.worktree,branch:t.branch,baseCommit:t.baseCommit}:await addWorktree(target.dir,t.id,target.base),ticket={...t,worktree:copy2.dir,branch:copy2.branch,baseCommit:t.baseCommit??copy2.baseCommit};try{if(had){let allow=readMeta(bp).guard,saved=await commitAll(copy2.dir,`${t.title} (${t.id}): work so far`,conventionalWords("chore",`work so far on ${t.title}`,t.id),guardSupport().on?{guard:ticketGuard(bp,{cwd:copy2.dir,id:t.id,...allow?{allow}:{}})}:{});saved.hookSaid!==void 0&&(writeLog({level:"warn",project:readMeta(bp).project,ticket:t.id,text:`Saved the work so far for the hand over without the project's own check, which said no: ${saved.hookSaid}`}),ticket.hookOwed=!0)}let remoteBase=`refs/remotes/${target.remote}/${target.base}`,baseRef=git(target.dir,["rev-parse","--verify","-q",remoteBase],{allowFail:!0})?remoteBase:target.base,carried=stageProjectFiles(bp,ticket,baseRef),paths=[...stageTicketDocs(bp,ticket),...carried];return paths.length&&git(copy2.dir,["diff","--cached","--name-only","--",...paths],{allowFail:!0})&&await gitLater(copy2.dir,[...commitArgsFor(copy2.dir),"commit","-m",`${t.id}: handed over to ${t.owner}`,"--",...paths]),await netGitLater(copy2.dir,["push",target.remote,`${copy2.branch}:refs/heads/${copy2.branch}`],{env:env2}).catch(err2=>{throw/rejected|non-fast-forward|fetch first/i.test(err2.message)?new Error(`${copy2.branch} on ${target.remote} has work this computer does not have. Nothing was handed over.`):err2}),{branch:copy2.branch,remote:target.remote,commit:git(copy2.dir,["rev-parse","--short","HEAD"]),carried,...ticket.baseCommit?{baseCommit:ticket.baseCommit}:{},...ticket.hookOwed?{hookOwed:!0}:{}}}finally{had||await removeWorktree(target.dir,copy2.dir)}}function pushedTicketRefs(repo){return git(repo,["for-each-ref","--sort=-committerdate","--format=%(refname)","refs/remotes/*/squad/*"],{allowFail:!0}).split(`
1478
1478
  `).filter(Boolean)}function filesOn(repo,ref2,dir){return git(repo,["ls-tree","-r","--name-only",ref2,"--",`${dir}/`],{allowFail:!0}).split(`
1479
1479
  `).filter(Boolean).map(f=>f.slice(dir.length+1))}function blob2(repo,ref2,file3){return execFileSync11("git",gitArgs(["cat-file","blob",`${ref2}:${file3}`]),{cwd:repo,maxBuffer:MOST_OUTPUT,stdio:["ignore","pipe","ignore"],env:gitEnv()})}function blobTo(repo,ref2,file3,to){fs103.mkdirSync(path95.dirname(to),{recursive:!0});let fd=fs103.openSync(to,"w");try{execFileSync11("git",gitArgs(["cat-file","blob",`${ref2}:${file3}`]),{cwd:repo,stdio:["ignore",fd,"ignore"],env:gitEnv()})}finally{fs103.closeSync(fd)}}function ticketOn(repo,ref2,squad){let name=ref2.replace(/^refs\/remotes\/[^/]+\/squad\//,""),id2=git(repo,["ls-tree","--name-only",ref2,"--",`${squad}/tickets/`],{allowFail:!0}).split(`
1480
1480
  `).map(f=>f.slice(`${squad}/tickets/`.length)).find(f=>f.toLowerCase()===name);if(!id2)return null;let files=filesOn(repo,ref2,`${squad}/tickets/${id2}`),docs=Object.fromEntries(files.filter(f=>DOC_FILES2.includes(f)).map(f=>[f,blob2(repo,ref2,`${squad}/tickets/${id2}/${f}`).toString("utf8")]));return docs["ticket.md"]?{id:id2,files,docs}:null}function differs(bp,id2,docs){let dir=ticketDir(bp,id2),here=Object.fromEntries(DOC_FILES2.filter(f=>fs103.existsSync(path95.join(dir,f))).map(f=>[f,fs103.readFileSync(path95.join(dir,f),"utf8")]));return JSON.stringify(ticketDocs(readTicketDocs(here)))!==JSON.stringify(ticketDocs(readTicketDocs(docs)))}function bringIn(bp,repo,ref2,squad,id2,files){let dir=ticketDir(bp,id2);fs103.rmSync(dir,{recursive:!0,force:!0});for(let f of files)blobTo(repo,ref2,`${squad}/tickets/${id2}/${f}`,path95.join(dir,f));let known=new Set(listDecisions(bp).map(d=>d.id));for(let f of filesOn(repo,ref2,`${squad}/decisions`))!f.endsWith(".md")||known.has(f.slice(0,-3))||blobTo(repo,ref2,`${squad}/decisions/${f}`,path95.join(bp.decisionsDir,f))}var SAME_NAME="A teammate's different ticket has this same name, on";function sameTrackerItem(a,b){return!!a.source?.id&&a.source.type===b.source?.type&&a.source.id===b.source?.id}function pickUpPushed(bp){let repo=docsRepo(bp);if(!repo||!boardExists(bp)||!docsInGit(bp))return[];let meta=readMeta(bp),squad=squadInRepo(bp,repo),out20=[],seen=new Set,base2=meta.git?.base||"main",bases=[`refs/heads/${base2}`,...git(repo,["for-each-ref","--format=%(refname)",`refs/remotes/*/${base2}`],{allowFail:!0}).split(`
1481
1481
  `).filter(Boolean)];for(let ref2 of pushedTicketRefs(repo)){if(bases.some(b=>isAncestor(repo,ref2,b)))continue;let on=ticketOn(repo,ref2,squad);if(!on||seen.has(on.id))continue;seen.add(on.id);let theirs=readTicketDocs(on.docs);if(!theirs.title||!theirs.stage||ticketProblem(bp,on.id))continue;let local=readTicket(bp,on.id),deleted=local?void 0:meta.deleted?.filter(d=>d.id===on.id).at(-1);if(deleted&&!deleted.fromBoard&&!(theirs.handedOver?.at&&theirs.handedOver.at>deleted.at))continue;if(local&&theirs.createdAt&&local.createdAt&&theirs.createdAt!==local.createdAt&&!sameTrackerItem(local,theirs)){local.activity.some(a=>a.text.startsWith(SAME_NAME))||(writeLog({level:"warn",project:readMeta(bp).project,ticket:on.id,text:`Did not bring in ${ref2}: another ticket of the same name`}),updateTicket(bp,on.id,x=>addActivity(x,"squad",`${SAME_NAME} the shared copy of the code. It was not brought in, so this ticket stays as it is. Tell your teammate to make theirs a new ticket here.`)));continue}let newer2=theirs.handedOver?.at&&theirs.handedOver.at>(local?.handedOver?.at??""),kind=local?newer2?"handed":!isMine(local,meta)&&differs(bp,on.id,on.docs)?"follow":null:"new";if(!kind)continue;bringIn(bp,repo,ref2,squad,on.id,on.files);let branch=ref2.replace(/^refs\/remotes\/[^/]+\//,""),t=received(bp,repo,on.id,ref2,kind);out20.push({id:on.id,branch,kind,mine:isMine(t,meta),...t.handedOver?{from:t.handedOver.from}:{}})}for(let t of listTickets(bp))if(!(seen.has(t.id)||isMine(t,meta)))for(let ref2 of bases.filter(b=>b.startsWith("refs/remotes/"))){let files=filesOn(repo,ref2,`${squad}/tickets/${t.id}`);if(!files.includes("ticket.md"))continue;let docs=Object.fromEntries(files.filter(f=>DOC_FILES2.includes(f)).map(f=>[f,blob2(repo,ref2,`${squad}/tickets/${t.id}/${f}`).toString("utf8")]));differs(bp,t.id,docs)&&(bringIn(bp,repo,ref2,squad,t.id,files),received(bp,repo,t.id,ref2,"follow"),out20.push({id:t.id,branch:ref2.replace(/^refs\/remotes\/[^/]+\//,""),kind:"follow",mine:!1}));break}return out20}async function fetchPushed(bp){let repo=docsRepo(bp);if(!repo||!boardExists(bp)||!docsInGit(bp))return[];let remote=readMeta(bp).git?.remote||remotes(repo)[0],fetched=!1;if(remote){let url=git(repo,["config","--get",`remote.${remote}.url`],{allowFail:!0}),env2={GIT_TERMINAL_PROMPT:"0",...url?repoHostFor(bp,url)?.gitEnv()??{}:{}};fetched=await netGitLater(repo,["fetch","--quiet","--prune",remote],{env:env2,timeoutMs:6e4}).then(()=>!0,()=>!1)}fetched&&teamAdditions(bp,repo,remote);let picked=[];try{picked=pickUpPushed(bp)}catch{}try{spotOwnerDeletes(bp,{fetched})}catch{}return picked}function teamAdditions(bp,repo,remote){try{let base2=reposOf(readMeta(bp))[0].base;mergeHarness(bp,blob2(repo,`${remote}/${base2}`,`${squadInRepo(bp,repo)}/harness.yaml`).toString("utf8"))}catch{}try{let base2=reposOf(readMeta(bp))[0].base;mergeSettings(bp,blob2(repo,`${remote}/${base2}`,`${squadInRepo(bp,repo)}/config.yaml`).toString("utf8"),{fetched:!0})}catch{}}function catchUp(repo,t,ref2){let branch=t.branch??`squad/${t.id.toLowerCase()}`,here=git(repo,["rev-parse","--verify","-q",`refs/heads/${branch}`],{allowFail:!0}),tip=git(repo,["rev-parse",ref2],{allowFail:!0});if(!here||!tip||here===tip)return null;if(!isAncestor(repo,here,tip))return`${branch} on this computer has commits that are not on the pushed branch, so it was kept as it is.`;try{return t.worktree&&fs103.existsSync(t.worktree)?git(t.worktree,["merge","--ff-only",tip]):git(repo,["update-ref",`refs/heads/${branch}`,tip,here]),null}catch(err2){return`Could not bring the pushed work into this computer's copy: ${err2.message}`}}var RUN_STATE=["sessions","stuck","agentQuestion","chat","takenOver","retryPhase","reviewNotes","replanFor","interrupted","quiet","quietSnoozedUntil","stopRequested","retryOnStop","clock","notesSeen","planReview","changeRequest","tries","fails","reviewRound"];function takeBack(bp,id2,reason){let meta=readMeta(bp),why=reason.trim();if(!why)throw new Error("Write why you take it. It stays on the ticket.");return updateTicket(bp,id2,t=>{if(isMine(t,meta))throw new Error(`${t.id} is yours already.`);if(t.stage==="done")throw new Error(`${t.id} is released. Add a new ticket instead.`);let from=ownerOf(t);t.owner=meta.user.name,t.handedOver={from,to:meta.user.name,at:now()},delete t.ownerDeleted;for(let k of RUN_STATE)delete t[k];t.live=!1,t.squad=null,t.waiting=null,t.progress=void 0,addActivity(t,"you",`Took it from ${from}. Why: ${why}`);let next=carryOn(t,!1);t.status=next.status,t.waiting=next.waiting??null,next.phase&&(t.live=!0,scheduleSquad(t,next.phase,.5,1))})}function received(bp,repo,id2,ref2,kind){let meta=readMeta(bp),branch=ref2.replace(/^refs\/remotes\/[^/]+\//,"");return updateTicket(bp,id2,t=>{for(let k of RUN_STATE)delete t[k];t.seenOn=ref2.replace(/^refs\/remotes\//,""),delete t.ownerDeleted,t.live=!1,t.squad=null,t.waiting=null,t.progress=void 0;let owner=ownerOf(t);if(!isMine(t,meta)){t.status=`With ${owner}`,addActivity(t,"squad",kind==="follow"?`Updated from ${branch}, as ${owner} pushed it`:kind==="handed"&&t.handedOver?`${t.handedOver.from} handed it over to ${owner} on ${branch}. Their squad works on it`:`Came from ${branch}. ${owner} owns it: their squad works on it`);return}let from=t.handedOver&&sameName(t.handedOver.to,meta.user.name)?t.handedOver.from:void 0;addActivity(t,"squad",from?`${from} handed it over to you. Picked it up from ${branch}: the documents and the code came with it. Chats, live progress and screenshots stayed on ${from}'s computer`:`Picked it up from ${branch}, pushed from another computer. Chats, live progress and screenshots stayed there`);let note2=catchUp(repo,t,ref2);note2&&addActivity(t,"squad",note2),t.proof?.length&&["implement","test","review"].includes(t.stage)&&(t.proof=void 0,t.gates&&(t.gates={...t.gates,proof:"pending"}));let next=carryOn(t,!1);t.status=next.status,t.waiting=next.waiting??null,next.phase&&(t.live=!0,scheduleSquad(t,next.phase,.5,1))})}function carryOn(t,copy2){let open3=(t.questions??[]).filter(q5=>!q5.answer),spec=t.track!=="express"&&!t.specApproved;switch(t.stage){case"done":return{status:t.release?`Released in ${t.release}`:"Released"};case"release":return{status:`Ready for ${t.release??"the next release"}`};case"bugs":return{phase:"triage",status:"Triage is sorting the bug"};case"implement":case"test":case"review":return copy2?t.stage==="implement"?{phase:"build",status:"The Builder starts again from the spec, the design and the code"}:t.stage==="test"?{phase:"test",status:"Running the project checks"}:t.gates?.review==="pass"?{phase:"proof",status:"The Tester takes the screenshots again"}:{phase:"review",status:"Reviewer is reading the changes"}:{phase:"pick-up",status:"Making a copy of the code from the pushed branch"};default:return!t.questions&&t.stage==="tickets"?{phase:"analyst-read",status:"Analyst is reading the ticket"}:open3.length?{waiting:{kind:"questions",summary:`${questionWords(t.questions.length)} from the Analyst and Architect`},status:`${open3.length} questions for you`}:!t.story&&!t.steps?{phase:"plan-write",status:spec?"Analyst is writing the spec":"Architect is choosing the design"}:spec?{waiting:{kind:"spec",summary:"The spec is ready: is this what you want?"},status:"Spec ready for you"}:t.steps?t.track==="express"?{phase:"express-approve",status:"Express track: approving the plan"}:t.track==="auto"?{phase:"express-approve",status:"Builds by itself: approving the plan"}:t.specApproved?{waiting:{kind:"plan",summary:"The design is ready: is this how to build it?"},status:"Design ready for you"}:{waiting:{kind:"plan",summary:"The plan is ready for your approval"},status:"Plan ready for you"}:{phase:"plan-write",status:"Architect is choosing the design"}}}async function workSoFar(dir,baseCommit){return(await changedFiles(dir,baseCommit)).filter(f=>!f.startsWith(".squad/"))}function projectFromPushed(squadDir){let root=path95.dirname(path95.resolve(squadDir));if(!isRepo(root))return null;let squad=path95.relative(root,path95.resolve(squadDir)).split(path95.sep).join("/");for(let ref2 of pushedTicketRefs(root)){let config2;try{config2=yaml16.load(blob2(root,ref2,`${squad}/config.yaml`).toString("utf8"))}catch{continue}if(!config2?.board?.id)continue;for(let sub of["config.yaml","rules","knowledge","decisions"]){let files=sub==="config.yaml"?[""]:filesOn(root,ref2,`${squad}/${sub}`);for(let f of files){let rel=f?`${sub}/${f}`:sub,to=path95.join(squadDir,rel);fs103.existsSync(to)&&sub!=="config.yaml"||(fs103.mkdirSync(path95.dirname(to),{recursive:!0}),sub==="config.yaml"?writeConfigFile(to,blob2(root,ref2,`${squad}/${rel}`)):fs103.writeFileSync(to,blob2(root,ref2,`${squad}/${rel}`)))}}let added2=path95.join(squadDir,"harness.yaml");if(!fs103.existsSync(added2))try{fs103.writeFileSync(added2,blob2(root,ref2,`${squad}/harness.yaml`))}catch{}return ref2.replace(/^refs\/remotes\//,"")}return null}import path97 from"path";import path96 from"path";function lines(root,args){return git(root,args,{allowFail:!0}).split(`
1482
- `).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=path96.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=path96.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:failed3}=deleteHere(bp,plan);for(let b of plan.remote){let root=path96.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){failed3.push(`${b.remote}/${b.branch}: ${err2.message}`)}}return{deleted,failed:failed3,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=[],failed3=[];for(let b of plan.here){let root=path96.join(bp.root,b.repo);try{git(root,["update-ref","-d",`refs/heads/${b.branch}`,b.sha]),deleted.push(b.branch)}catch(err2){failed3.push(`${b.branch}: ${err2.message}`)}}return{deleted,failed:failed3}}function hostEnv(bp,root,remote){let url=git(root,["config","--get",`remote.${remote}.url`],{allowFail:!0});return url?repoHostFor(bp,url)?.gitEnv()??{}:{}}import fs104 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(path97.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 out20=r.out.filter(x=>x!==key),next={...r,tickets:[...new Set([...r.tickets,key])],out:out20};return out20.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 out20=(release.out??[]).filter(id2=>!release.tickets.includes(id2)&&readTicket(bp,id2));if(out20.length){joinNextRelease(bp,out20);for(let id2 of out20)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 done2=cleanBranchesHere(bp,{only:release.tickets});done2.deleted.length&&lines3.push({ok:!0,text:"Deleted what was left over from released tickets on this computer"});for(let f of done2.failed)lines3.push({ok:!1,text:`Could not delete ${f}`});done2.remoteLeft.length&&lines3.push({ok:!0,text:`What is left of them in the shared copy of the code stays (${done2.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 done2={...release,status:"released",date:release.date??j.at,tickets:going,log:log3,...unpushed.length?{unpushed}:{}};unpushed.length||delete done2.unpushed,writeRelease(bp,done2);for(let id2 of going){let t=readTicket(bp,id2);for(let v of t?.worktree?copyViews(meta,t):[])await removeWorktree(path97.join(bp.root,repoFor(meta,v).path),v.worktree);t?.storyDir&&!copyDirsIn(t.storyDir).length&&fs104.rmSync(t.storyDir,{recursive:!0,force:!0}),dropTicketCache(bp,id2)}return rest.length&&joinNextRelease(bp,rest),closeReleased(bp,done2),afterRelease(bp,done2),readRelease(bp,done2.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 fs121 from"fs";import path112 from"path";import{randomBytes as randomBytes6}from"crypto";import fs105 from"fs";import os16 from"os";import path98 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 out20={restored:[],putBack:[]},{dir}=lane;if(!fs105.existsSync(dir))return out20;let relinked=putGitLinkBack(lane),{head,branch}=placeOf(dir);return relinked?out20.moved="the copy's link to its git folder was changed":branch!==lane.branch?out20.moved=branch?`the copy is on ${branch}, not ${lane.branch}`:`the copy left ${lane.branch}`:head!==lane.head&&(out20.moved=`${branch||"HEAD"} moved from ${head8(lane.head)} to ${head8(head)}`),lane.tree&&!out20.moved&&(out20.putBack=await putTreeBack(dir,lane.tree,skip)),out20.restored=[...putKeptBack(dir,lane.kept,await readKept(dir,lane.names)),...putSettingsBack(dir,lane.hookSettings)],out20}function gitLinkOf(dir){try{let file3=path98.join(dir,".git");return fs105.lstatSync(file3).isFile()?fs105.readFileSync(file3):void 0}catch{return}}function putGitLinkBack(lane){if(!lane.gitLink||gitLinkOf(lane.dir)?.equals(lane.gitLink))return!1;let file3=path98.join(lane.dir,".git");return fs105.rmSync(file3,{force:!0,recursive:!0}),fs105.writeFileSync(file3,lane.gitLink),!0}function readPackage(dir){try{let json2=JSON.parse(fs105.readFileSync(path98.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),kept2=Object.fromEntries(HOOK_PACKAGE_KEYS.filter(k=>pkg?.[k]!==void 0).map(k=>[k,pkg[k]]));return Object.keys(kept2).length?JSON.stringify(kept2):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):{},now3=after?JSON.parse(after):{},changed=HOOK_PACKAGE_KEYS.filter(k=>JSON.stringify(was[k])!==JSON.stringify(now3[k]));for(let k of changed)was[k]===void 0?delete pkg[k]:pkg[k]=was[k];let file3=path98.join(dir,"package.json"),indent=/^\{\r?\n([ \t]+)/.exec(fs105.readFileSync(file3,"utf8"))?.[1]??" ";return fs105.writeFileSync(file3,`${JSON.stringify(pkg,null,indent)}
1482
+ `).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=path96.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=path96.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:failed3}=deleteHere(bp,plan);for(let b of plan.remote){let root=path96.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){failed3.push(`${b.remote}/${b.branch}: ${err2.message}`)}}return{deleted,failed:failed3,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=[],failed3=[];for(let b of plan.here){let root=path96.join(bp.root,b.repo);try{git(root,["update-ref","-d",`refs/heads/${b.branch}`,b.sha]),deleted.push(b.branch)}catch(err2){failed3.push(`${b.branch}: ${err2.message}`)}}return{deleted,failed:failed3}}function hostEnv(bp,root,remote){let url=git(root,["config","--get",`remote.${remote}.url`],{allowFail:!0});return url?repoHostFor(bp,url)?.gitEnv()??{}:{}}import fs104 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(path97.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 out20=r.out.filter(x=>x!==key),next={...r,tickets:[...new Set([...r.tickets,key])],out:out20};return out20.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 out20=(release.out??[]).filter(id2=>!release.tickets.includes(id2)&&readTicket(bp,id2));if(out20.length){joinNextRelease(bp,out20);for(let id2 of out20)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 done2=cleanBranchesHere(bp,{only:release.tickets});done2.deleted.length&&lines3.push({ok:!0,text:"Deleted what was left over from released tickets on this computer"});for(let f of done2.failed)lines3.push({ok:!1,text:`Could not delete ${f}`});done2.remoteLeft.length&&lines3.push({ok:!0,text:`What is left of them in the shared copy of the code stays (${done2.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 done2={...release,status:"released",date:release.date??j.at,tickets:going,log:log3,...unpushed.length?{unpushed}:{}};unpushed.length||delete done2.unpushed,writeRelease(bp,done2);for(let id2 of going){let t=readTicket(bp,id2);for(let v of t?.worktree?copyViews(meta,t):[])await removeWorktree(path97.join(bp.root,repoFor(meta,v).path),v.worktree);t?.storyDir&&!copyDirsIn(t.storyDir).length&&fs104.rmSync(t.storyDir,{recursive:!0,force:!0}),dropTicketCache(bp,id2)}return rest.length&&joinNextRelease(bp,rest),closeReleased(bp,done2),afterRelease(bp,done2),readRelease(bp,done2.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 fs121 from"fs";import path112 from"path";import{randomBytes as randomBytes6}from"crypto";import fs105 from"fs";import os17 from"os";import path98 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 out20={restored:[],putBack:[]},{dir}=lane;if(!fs105.existsSync(dir))return out20;let relinked=putGitLinkBack(lane),{head,branch}=placeOf(dir);return relinked?out20.moved="the copy's link to its git folder was changed":branch!==lane.branch?out20.moved=branch?`the copy is on ${branch}, not ${lane.branch}`:`the copy left ${lane.branch}`:head!==lane.head&&(out20.moved=`${branch||"HEAD"} moved from ${head8(lane.head)} to ${head8(head)}`),lane.tree&&!out20.moved&&(out20.putBack=await putTreeBack(dir,lane.tree,skip)),out20.restored=[...putKeptBack(dir,lane.kept,await readKept(dir,lane.names)),...putSettingsBack(dir,lane.hookSettings)],out20}function gitLinkOf(dir){try{let file3=path98.join(dir,".git");return fs105.lstatSync(file3).isFile()?fs105.readFileSync(file3):void 0}catch{return}}function putGitLinkBack(lane){if(!lane.gitLink||gitLinkOf(lane.dir)?.equals(lane.gitLink))return!1;let file3=path98.join(lane.dir,".git");return fs105.rmSync(file3,{force:!0,recursive:!0}),fs105.writeFileSync(file3,lane.gitLink),!0}function readPackage(dir){try{let json2=JSON.parse(fs105.readFileSync(path98.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),kept2=Object.fromEntries(HOOK_PACKAGE_KEYS.filter(k=>pkg?.[k]!==void 0).map(k=>[k,pkg[k]]));return Object.keys(kept2).length?JSON.stringify(kept2):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):{},now3=after?JSON.parse(after):{},changed=HOOK_PACKAGE_KEYS.filter(k=>JSON.stringify(was[k])!==JSON.stringify(now3[k]));for(let k of changed)was[k]===void 0?delete pkg[k]:pkg[k]=was[k];let file3=path98.join(dir,"package.json"),indent=/^\{\r?\n([ \t]+)/.exec(fs105.readFileSync(file3,"utf8"))?.[1]??" ";return fs105.writeFileSync(file3,`${JSON.stringify(pkg,null,indent)}
1483
1483
  `),changed.map(k=>`package.json (${k})`)}function placeOf(dir){let[head="",name=""]=git(dir,["rev-parse","HEAD","--abbrev-ref","HEAD"],{allowFail:!0}).split(`
1484
- `);return{head,branch:name==="HEAD"?"":name}}function head8(sha2){return sha2?sha2.slice(0,8):"nothing"}function listed(out20){return out20.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 kept2=new Map;if(!names2.length)return kept2;for(let rel of await protectedPaths(dir,names2)){let file3=path98.join(dir,rel);try{let st=fs105.lstatSync(file3);st.isSymbolicLink()?kept2.set(rel,{link:fs105.readlinkSync(file3)}):st.isFile()&&kept2.set(rel,{data:fs105.readFileSync(file3),mode:st.mode&511})}catch{}}return kept2}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=path98.join(dir,rel);makeParents(dir,rel),fs105.rmSync(file3,{force:!0,recursive:!0}),"link"in was?fs105.symlinkSync(was.link,file3):(fs105.writeFileSync(file3,was.data),fs105.chmodSync(file3,was.mode))}return[...changed,...added2].sort()}function makeParents(dir,rel){let at3=dir;for(let part of path98.dirname(rel).split(path98.sep).filter(p=>p&&p!==".")){at3=path98.join(at3,part);try{fs105.lstatSync(at3).isDirectory()||fs105.rmSync(at3,{force:!0})}catch{}fs105.mkdirSync(at3,{recursive:!0})}}function removeFile(dir,rel){fs105.rmSync(path98.join(dir,rel),{force:!0,recursive:!0});for(let d=path98.dirname(rel);d&&d!==".";d=path98.dirname(d))try{fs105.rmdirSync(path98.join(dir,d))}catch{break}}async function treeOf(dir){let tmp=path98.join(os16.tmpdir(),`squad-index-${randomUUID5()}`);try{let index=path98.resolve(dir,git(dir,["rev-parse","--git-path","index"]));if(fs105.existsSync(index)){fs105.copyFileSync(index,tmp);let st=fs105.statSync(index);fs105.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{fs105.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,path98.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=path98.relative(folder,file3);return r===""||!r.startsWith("..")&&!path98.isAbsolute(r)}import fs120 from"fs";import path111 from"path";import fs106 from"fs";import path99 from"path";var DOCKER_FILE=/^(?:docker-)?compose(?:[.-][\w.-]+)?\.ya?ml$|^Dockerfile(?:[.-][\w.-]+)?$|\.dockerfile$/i,MAX_DOCKER_FILES=30,storyFilesDir=bp=>path99.join(bp.localDir,"story-files"),storyFile2=(bp,f)=>path99.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(path99.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=path99.join(bp.root,r,clean4);if(text3.endsWith("/")||fs106.existsSync(onDisk)&&fs106.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(path99.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 out20=[];for(let repo of repoPaths(meta)){let files=git(path99.join(bp.root,repo),["ls-files","-z"],{allowFail:!0}).split("\0").filter(Boolean);for(let file3 of files)DOCKER_FILE.test(path99.basename(file3))&&!listed2(meta,"locked").some(x=>same3(x,{repo,path:file3}))&&out20.push({repo,path:file3})}return out20.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"&&fs106.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=path99.join(bp.root,f.repo,f.path);if(!fs106.existsSync(from))throw new Error(`There is no ${f.path} in your folder.`);let to=storyFile2(bp,f);return fs106.mkdirSync(path99.dirname(to),{recursive:!0,mode:448}),fs106.copyFileSync(from,to),fs106.chmodSync(to,384),f}function projectFilesView(bp){let meta=readMeta(bp);return{local:listed2(meta,"local").map(f=>({...f,ready:fs106.existsSync(storyFile2(bp,f)),inFolder:fs106.existsSync(path99.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 createHash10}from"crypto";import fs119 from"fs";import net3 from"net";import path110 from"path";var PHONE_COMMANDS=/(?:^|[\s;&|(])(?:flutter|xcodebuild)\s|\b(?:expo|react-native)\s/m;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()}:{}})),phone=new Set(reposOf(meta).filter(r=>PHONE_COMMANDS.test(Object.values(r.commands).join(`
1485
- `))).map(r=>r.path)),known=all.filter(a=>a.url||a.start||phone.has(a.repo)),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=[],now3=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||now3-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 fs107 from"fs";import os17 from"os";import path100 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=os17.homedir(),system="/var/run/docker.sock"){let named=/^unix:\/\/(\/.+)$/.exec(env2.DOCKER_HOST?.trim()??"")?.[1];for(let place of[named,path100.join(home2,".docker","run","docker.sock"),system])if(place)try{return fs107.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=>fs107.existsSync(path100.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(path100.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}
1486
- ${repo}`).digest("hex").slice(0,10)}`,config2=path100.join(squadHome(),"docker-config");fs107.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,run7){let socket=run7.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,run7.env.DOCKER_CONFIG],write:[...guard.write,run7.env.DOCKER_CONFIG]}}import fs108 from"fs";import path101 from"path";var lockFile2=bp=>path101.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 hold2=JSON.parse(fs108.readFileSync(lockFile2(bp),"utf8"));if(alive(hold2.pid))return hold2;fs108.rmSync(lockFile2(bp),{force:!0})}catch{}return null}function takeProduct(bp,holder,kind){let now3=productHold(bp);if(now3?.holder===holder)return null;if(now3)return now3;try{return fs108.mkdirSync(bp.localDir,{recursive:!0}),fs108.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&&fs108.rmSync(lockFile2(bp),{force:!0})}var WAIT_LIMIT_MS=45*6e4,TRY_YIELDS_AFTER_MS=15*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{fs108.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))}}import fs115 from"fs";import{spawn as spawn14}from"child_process";import fs112 from"fs";import path104 from"path";import{execFile as execFile6}from"child_process";import{createHash as createHash9}from"crypto";import fs111 from"fs";import path103 from"path";import{once}from"events";var MOBILE=["flutter","react-native","ios","android"],CROSS=["flutter","react-native"],ON_PHONE=["pictures","start-app","try-it"],CHECK_AGAIN="Come back here and press Check again.",OPEN_AGAIN="Close squad-kit and open it again, so it sees the new tool. Then press Check again.",CATALOG4=[{id:"git",name:"Git",gives:"keeps every version of your work safe",needs:[{kinds:"any",priority:"required",for:["build","checks"]}],min:"2.38",installer:"you",why:"it installs for the whole computer, which needs your computer's password",steps:{all:["Open Git's official download page.","Get Git for your computer and install it.",CHECK_AGAIN],windows:["Open Git's official download page.","Get Git for your computer and install it.",OPEN_AGAIN]},link:"https://git-scm.com/downloads",asToday:!0},{id:"ai-tool",name:"An AI tool",gives:"the squad itself: the Analyst, Architect, Builder, Reviewer and Tester",needs:[{kinds:"any",priority:"required",for:["build"]}],installer:"you",why:"it works on your own AI account, which only you can sign in to",steps:{all:["Open a terminal.","Run: npm install -g @anthropic-ai/claude-code","Run: claude, and sign in once.",CHECK_AGAIN]},link:"https://docs.anthropic.com/en/docs/claude-code/setup",asToday:!0},{id:"flutter",name:"Flutter",gives:"builds and tests the Flutter app",needs:[{kinds:["flutter"],priority:"required",for:["build","checks",...ON_PHONE]}],installer:"you",why:"it becomes part of your computer's own setup, and Flutter's installer walks you through it",steps:{mac:["Open Flutter's official install page.","Pick macOS, then iOS, and follow the steps there.","In a terminal, run: flutter doctor, to see that it works.",CHECK_AGAIN],windows:["Open Flutter's official install page.","Pick Windows, then Android, and follow the steps there.","In a terminal, run: flutter doctor, to see that it works.",OPEN_AGAIN],linux:["Open Flutter's official install page.","Pick Linux, then Android, and follow the steps there.","In a terminal, run: flutter doctor, to see that it works.",CHECK_AGAIN]},link:"https://docs.flutter.dev/get-started/install",update:{reaches:"every Flutter project",size:"about 1 GB",seconds:300},updateSteps:{all:["Open a terminal.","Run: flutter upgrade",CHECK_AGAIN],homebrew:["Open a terminal.","Run: brew upgrade --cask flutter",CHECK_AGAIN],fvm:["Open a terminal.","Run: fvm install stable","Run: fvm global stable",CHECK_AGAIN]}},{id:"xcode",name:"Xcode",gives:"the real iPhone, for pictures and Try it yourself",needs:[{kinds:CROSS,platform:"ios",priority:"required",for:ON_PHONE},{kinds:["ios"],priority:"required",for:["build","checks",...ON_PHONE]}],systems:["mac"],notHere:"needs a Mac",installer:"you",why:"it comes from the App Store and needs your Apple ID",steps:{mac:["Open the App Store and get Xcode. It is free, and big: it can take an hour.","Open Xcode once and let it finish setting up.",CHECK_AGAIN]},link:"https://apps.apple.com/app/xcode/id497799835"},{id:"simulator",name:"iPhone simulator",gives:"an iPhone on this Mac, for pictures, Start and Try it yourself",needs:[{kinds:CROSS,platform:"ios",priority:"required",for:ON_PHONE},{kinds:["ios"],priority:"required",for:["checks",...ON_PHONE]}],systems:["mac"],notHere:"needs a Mac",installer:"you",why:"it comes with Xcode, from Apple",steps:{mac:["Open Xcode.","Go to Xcode, then Settings, then Components.","Get the newest iOS.",CHECK_AGAIN]},link:"https://developer.apple.com/documentation/xcode/downloading-and-installing-additional-xcode-components"},{id:"cocoapods",name:"CocoaPods",gives:"brings in the app's iPhone libraries when it builds for the iPhone",needs:[{kinds:CROSS,platform:"ios",priority:"required",for:ON_PHONE}],systems:["mac"],notHere:"needs a Mac",installer:"you",why:"it may need your computer's password",steps:{mac:["Open a terminal.","Run: sudo gem install cocoapods. It asks for your computer's password.",CHECK_AGAIN]},link:"https://guides.cocoapods.org/using/getting-started.html"},{id:"maestro",name:"Maestro",gives:"taps through the phone app by itself, for the pictures",needs:[{kinds:MOBILE,priority:"required",for:["pictures"]}],installer:"squad-kit",link:"https://maestro.dev",getSeconds:80,with:["java"]},{id:"java",name:"Java",gives:"runs Maestro",needs:[{kinds:MOBILE,priority:"required",for:["pictures"]}],min:"17",installer:"squad-kit",link:"https://adoptium.net",getSeconds:30},{id:"android-studio",name:"Android Studio",gives:"an Android phone on this computer",needs:[{kinds:CROSS,platform:"android",priority:"recommended",for:ON_PHONE},{kinds:["android"],priority:"recommended",for:ON_PHONE}],installer:"you",why:"it is a big app with its own setup",steps:{all:["Open the official download page and get Android Studio.","Open it once and follow its setup.",CHECK_AGAIN]},link:"https://developer.android.com/studio",note:"Android pictures and Start come in the next squad-kit update."},{id:"docker",name:"Docker",gives:"runs the services the app starts with, like its database",needs:[{kinds:["docker"],priority:"recommended",for:["start-app"]}],installer:"you",why:"it installs for the whole computer, which needs your computer's password",steps:{all:["Open the official page and get Docker Desktop.","Open it once and let it start.",CHECK_AGAIN]},link:"https://docs.docker.com/get-started/get-docker/"}];function toolEntry(id2){return CATALOG4.find(t=>t.id===id2)}var KIND_WORDS={web:"web or Node.js code",flutter:"a Flutter app","react-native":"a React Native app",ios:"an iPhone app",android:"an Android app",docker:"Docker files"},PLATFORM_WORDS={ios:"iPhone",android:"Android",web:"web"},ACTION_WORDS={build:"Building",checks:"Checks",pictures:"Phone pictures","start-app":"Start","try-it":"Try it yourself"},ACTION_DOING={build:"build this story",checks:"run this story's checks",pictures:"take the phone pictures","start-app":"start the app on a phone","try-it":"open Try it yourself on a phone"},OWN_SOURCES={maestro:{version:"2.12.1",url:"https://github.com/mobile-dev-inc/maestro/releases/download/cli-2.12.1/maestro.zip",checksums:"https://github.com/mobile-dev-inc/maestro/releases/download/cli-2.12.1/checksums_sha256.txt",file:"maestro.zip",sha256:"54dbb0716425626604973cbb374c585ca30272375d9ce15177a1152765ddd9d7",size:349832427,env:{MAESTRO_CLI_NO_ANALYTICS:"1"}},java:{feature:21,api:"https://api.adoptium.net/v3/assets/latest"}};import{execFile as execFile5}from"child_process";import fs110 from"fs";import os18 from"os";import path102 from"path";function compareVersions(a,b){let nums=v=>(/^\d+(?:\.\d+)*/.exec(v.trim())?.[0]??"0").split(".").map(Number),x=nums(a),y=nums(b);for(let i=0;i<Math.max(x.length,y.length);i+=1){let d=(x[i]??0)-(y[i]??0);if(d)return d}return 0}var atLeast2=(found2,min)=>compareVersions(found2,min)>=0,PAIR_RULES=[{tool:"flutter",platform:"ios",applies:v=>!!(v.flutter&&v.xcode&&compareVersions(v.xcode,"27")>=0&&compareVersions(v.flutter,"3.44.8")<0),needs:"3.44.8",why:"Xcode 27 needs it for iPhone builds",for:["pictures","start-app","try-it"]}];function tooOld(id2,min,versions2,platforms){let v=versions2[id2];if(!v)return null;if(min&&!atLeast2(v,min))return{needs:min};let rule=PAIR_RULES.find(r=>r.tool===id2&&(!r.platform||platforms.has(r.platform))&&r.applies(versions2));return rule?{needs:rule.needs,why:rule.why,for:rule.for}:null}import{spawn as spawn13}from"child_process";import fs109 from"fs";var START="__SQUAD_KIT_PATH_START__",END="__SQUAD_KIT_PATH_END__",PRINT_PATH=`printf '%s%s%s' '${START}' "$PATH" '${END}'`;function pathBetweenMarks(output){let from=output.lastIndexOf(START);if(from<0)return null;let to=output.indexOf(END,from+START.length);return to<0?null:output.slice(from+START.length,to).trim()||null}function mergePath(current,more3,sep=":"){let seen=new Set,out20=[];for(let d of[...(current??"").split(sep),...(more3??"").split(sep)])!d||seen.has(d)||(seen.add(d),out20.push(d));return out20.join(sep)}function loginShell(system,env2=process.env,exists3=fs109.existsSync){let own2=env2.SHELL?.trim();return own2&&exists3(own2)?own2:system==="mac"?"/bin/zsh":"/bin/bash"}function readLoginPath(system,opts={}){if(system==="windows")return Promise.resolve(null);let env2=opts.env??process.env,shell2=opts.shell??loginShell(system,env2);return new Promise(resolve2=>{let out20="",done2=!1,end=value=>{done2||(done2=!0,resolve2(value))},child;try{child=spawn13(shell2,["-ilc",PRINT_PATH],{env:env2,stdio:["ignore","pipe","ignore"],windowsHide:!0})}catch{resolve2(null);return}let timer=setTimeout(()=>{child.exitCode===null&&child.signalCode===null&&child.kill("SIGKILL"),child.stdout?.destroy(),end(null)},opts.limitMs??5e3);timer.unref(),child.stdout?.setEncoding("utf8"),child.stdout?.on("data",s=>{out20=(out20+s).slice(-256*1024);let found2=pathBetweenMarks(out20);found2&&end(found2)}),child.on("error",()=>{clearTimeout(timer),end(null)}),child.on("close",()=>{clearTimeout(timer),end(pathBetweenMarks(out20))})})}function systemOf(platform=process.platform){return platform==="darwin"?"mac":platform==="win32"?"windows":"linux"}function realHost(){return{system:systemOf(),home:os18.homedir(),env:process.env,run:(cmd15,args,opts={})=>new Promise(resolve2=>{execFile5(cmd15,args,{timeout:opts.timeoutMs??15e3,encoding:"utf8",windowsHide:!0,maxBuffer:4*1024*1024,env:{...process.env,...opts.env}},(err2,out20,errText)=>resolve2({ok:!err2,out:String(out20??""),err:String(errText??"")})).stdin?.end()}),exists:p=>fs110.existsSync(p),read:p=>{try{return fs110.readFileSync(p,"utf8")}catch{return null}},real:p=>{try{return fs110.realpathSync(p)}catch{return p}},list:dir=>{try{return fs110.readdirSync(dir)}catch{return[]}},writable:p=>{try{return fs110.accessSync(p,fs110.constants.W_OK),!0}catch{return!1}},loginPath:()=>readLoginPath(systemOf())}}function onPath3(h,name){let dirs=(h.env.PATH??h.env.Path??"").split(h.system==="windows"?";":":").filter(Boolean),names2=h.system==="windows"?[`${name}.exe`,`${name}.bat`,`${name}.cmd`,name]:[name];for(let d of dirs)for(let n of names2)if(h.exists(path102.join(d,n)))return path102.join(d,n);return null}function flutterVersionOf(text3){try{let j=JSON.parse(text3),v=j.frameworkVersion??j.flutterVersion;if(v)return v}catch{}return/Flutter\s+(\d+\.\d+\.\d+(?:[-+.\w]*)?)/.exec(text3)?.[1]??/^\s*(\d+\.\d+\.\d+)\s*$/.exec(text3)?.[1]??null}function xcodeVersionOf(text3){let v=/Xcode\s+(\d+(?:\.\d+)*)/.exec(text3)?.[1];if(!v)return null;let build=/Build version\s+(\S+)/.exec(text3)?.[1];return{version:v,...build?{build}:{}}}function javaVersionOf(text3){let v=/version\s+"([^"]+)"/.exec(text3)?.[1];if(!v)return null;let old=/^1\.(\d+)(.*)$/.exec(v);return(old?`${old[1]}${old[2]}`:v).replace(/_/g,".")}function podVersionOf(text3){return/^(\d+\.\d+(?:\.\d+)?)\s*$/m.exec(text3)?.[1]??null}function iosRuntimesOf(text3){try{let versions2=(JSON.parse(text3).runtimes??[]).filter(r=>(r.platform?r.platform==="iOS":/^iOS\b/.test(r.name??""))&&r.isAvailable!==!1&&r.version).map(r=>r.version);return[...new Set(versions2)].sort((a,b)=>compareVersions(b,a))}catch{return[]}}function dockerVersionOf(text3){return/Docker version\s+(\d+\.\d+(?:\.\d+)?)/.exec(text3)?.[1]??null}function maestroVersionOf(text3){let lines3=text3.trim().split(`
1487
- `).reverse();for(let l of lines3){let m=/^\s*(\d+\.\d+\.\d+)\s*$/.exec(l);if(m)return m[1]}return null}function toolsHome(home2=squadHome()){return path102.join(home2,"tools")}function ownTool(id2,home2=squadHome()){let base2=path102.join(toolsHome(home2),id2),best=null,names2=[];try{names2=fs110.readdirSync(base2)}catch{return null}for(let v of names2)try{let mark=JSON.parse(fs110.readFileSync(path102.join(base2,v,"ready.json"),"utf8")),binary=path102.join(base2,v,mark.binary);if(!fs110.existsSync(binary))continue;(!best||compareVersions(mark.version,best.version)>0)&&(best={version:mark.version,dir:path102.join(base2,v),binary,...mark.seconds?{seconds:mark.seconds}:{}})}catch{}return best}var missing2=note2=>({state:"missing",...note2?{note:note2}:{}});function flutterPlaces(h){let home2=h.home;return h.system==="windows"?["C:\\src\\flutter","C:\\flutter",path102.join(home2,"flutter"),path102.join(home2,"dev","flutter")]:[path102.join(home2,"flutter"),path102.join(home2,"development","flutter"),path102.join(home2,"Applications","flutter"),path102.join(home2,"sdk","flutter"),path102.join(home2,"fvm","default"),"/opt/flutter","/usr/local/flutter"]}function flutterFileVersion(h,sdk){let json2=h.read(path102.join(sdk,"bin","cache","flutter.version.json")),fromJson=json2?flutterVersionOf(json2):null;if(fromJson)return fromJson;let plain=h.read(path102.join(sdk,"version"));return plain?flutterVersionOf(plain):null}async function flutter(h){let bin=onPath3(h,"flutter");if(bin){let sdk=path102.dirname(path102.dirname(h.real(bin))),version=flutterFileVersion(h,sdk);if(!version){let r=await h.run(bin,["--version","--machine"],{timeoutMs:3e4,env:{FLUTTER_SUPPRESS_ANALYTICS:"true",CI:"true"}});version=flutterVersionOf(r.out)??flutterVersionOf(r.err)}return{state:"found",where:sdk,...version?{version}:{}}}for(let sdk of flutterPlaces(h)){if(!h.exists(path102.join(sdk,"bin",h.system==="windows"?"flutter.bat":"flutter")))continue;let version=flutterFileVersion(h,sdk);return{state:"missing",where:sdk,...version?{version}:{},note:`Flutter${version?` ${version}`:""} is in ${sdk}, but not on the PATH squad-kit runs with, so the squad can't use it.`}}return missing2()}async function xcode(h){let sel=await h.run("xcode-select",["-p"],{timeoutMs:5e3}),dev=sel.out.trim();if(!sel.ok||!dev)return missing2();if(!/\.app\//.test(`${dev}/`))return missing2("Only Apple's command line tools are here, not Xcode itself.");let r=await h.run("xcodebuild",["-version"],{timeoutMs:15e3}),v=xcodeVersionOf(r.out);return r.ok&&v?{state:"found",version:v.version,where:dev.replace(/\/Contents\/Developer\/?$/,"")}:/licen[cs]e/i.test(r.err+r.out)?missing2("Xcode is here, but it has to be opened once to finish setting up."):missing2()}async function simulator(h,xcodeFound){if(!xcodeFound)return missing2("It comes with Xcode.");let r=await h.run("xcrun",["simctl","list","runtimes","-j"],{timeoutMs:2e4}),versions2=r.ok?iosRuntimesOf(r.out):[];return versions2.length?{state:"found",version:versions2[0],...versions2.length>1?{note:`iOS ${versions2.join(", iOS ")}`}:{}}:missing2("Xcode is here, without an iPhone simulator yet.")}async function cocoapods(h){let bin=onPath3(h,"pod");if(!bin)return missing2();let r=await h.run(bin,["--version"],{timeoutMs:2e4,env:{LANG:"en_US.UTF-8"}}),version=podVersionOf(r.out);return{state:"found",where:bin,...version?{version}:{}}}async function java(h,home2){let own2=ownTool("java",home2);if(own2)return{state:"found",version:own2.version,where:own2.dir,own:!0};let fromHome=h.env.JAVA_HOME?path102.join(h.env.JAVA_HOME,"bin",h.system==="windows"?"java.exe":"java"):null,bin=fromHome&&h.exists(fromHome)?fromHome:onPath3(h,"java");if(!bin||h.system==="mac"&&h.real(bin)==="/usr/bin/java"&&![...h.list("/Library/Java/JavaVirtualMachines"),...h.list(path102.join(h.home,"Library","Java","JavaVirtualMachines"))].some(m=>m.endsWith(".jdk")))return missing2();let r=await h.run(bin,["-version"],{timeoutMs:15e3}),version=javaVersionOf(r.err)??javaVersionOf(r.out);return version?{state:"found",version,where:bin}:missing2()}async function maestro(h,home2){let own2=ownTool("maestro",home2);if(own2)return{state:"found",version:own2.version,where:own2.dir,own:!0};let bin=onPath3(h,"maestro");if(!bin)return missing2();let r=await h.run(bin,["--version"],{timeoutMs:3e4,env:{...OWN_SOURCES.maestro.env}}),version=maestroVersionOf(r.out);return{state:"found",where:bin,...version?{version}:{}}}function androidStudio(h){let env2=h.env,apps2=h.system==="mac"?["/Applications/Android Studio.app",path102.join(h.home,"Applications","Android Studio.app")].map(a=>({dir:a,info:path102.join(a,"Contents","Resources","product-info.json")})):h.system==="windows"?[path102.join(env2.ProgramFiles??"C:\\Program Files","Android","Android Studio")].map(a=>({dir:a,info:path102.join(a,"product-info.json")})):["/opt/android-studio",path102.join(h.home,"android-studio"),"/snap/android-studio/current"].map(a=>({dir:a,info:path102.join(a,"product-info.json")}));for(let a of apps2){if(!h.exists(a.dir))continue;let version;try{version=JSON.parse(h.read(a.info)??"{}").version}catch{}return{state:"found",where:a.dir,...version?{version}:{}}}let sdk=env2.ANDROID_HOME??env2.ANDROID_SDK_ROOT;return sdk&&h.exists(sdk)?{state:"found",where:sdk,note:"The Android tools are here, without the Android Studio app."}:missing2()}async function docker(h){let bin=onPath3(h,"docker");if(!bin)return missing2();let r=await h.run(bin,["--version"],{timeoutMs:1e4}),version=dockerVersionOf(r.out);return{state:"found",where:bin,...version?{version}:{}}}async function detectAll(h=realHost(),home2=squadHome()){let more3=await h.loginPath?.().catch(()=>null);more3&&(h.env.PATH=mergePath(h.env.PATH,more3));let notHere=id2=>{let e=CATALOG4.find(t=>t.id===id2);return!!(e?.systems&&!e.systems.includes(h.system))},safe2=async(id2,find2)=>{if(notHere(id2))return[id2,{state:"not-here"}];try{return[id2,await find2()]}catch{return[id2,missing2()]}},x=await safe2("xcode",()=>xcode(h)),found2=await Promise.all([safe2("flutter",()=>flutter(h)),safe2("simulator",()=>simulator(h,x[1].state==="found")),safe2("cocoapods",()=>cocoapods(h)),safe2("java",()=>java(h,home2)),safe2("maestro",()=>maestro(h,home2)),safe2("android-studio",()=>androidStudio(h)),safe2("docker",()=>docker(h))]);return{at:new Date().toISOString(),system:h.system,tools:Object.fromEntries([x,...found2])}}function foundFile(home2=squadHome()){return path102.join(toolsHome(home2),"found.json")}function readFound(home2=squadHome()){try{return JSON.parse(fs110.readFileSync(foundFile(home2),"utf8"))}catch{return null}}function writeFound(book,home2=squadHome()){let file3=foundFile(home2);fs110.mkdirSync(path102.dirname(file3),{recursive:!0});let tmp=`${file3}.${process.pid}.tmp`;fs110.writeFileSync(tmp,`${JSON.stringify(book,null,2)}
1484
+ `);return{head,branch:name==="HEAD"?"":name}}function head8(sha2){return sha2?sha2.slice(0,8):"nothing"}function listed(out20){return out20.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 kept2=new Map;if(!names2.length)return kept2;for(let rel of await protectedPaths(dir,names2)){let file3=path98.join(dir,rel);try{let st=fs105.lstatSync(file3);st.isSymbolicLink()?kept2.set(rel,{link:fs105.readlinkSync(file3)}):st.isFile()&&kept2.set(rel,{data:fs105.readFileSync(file3),mode:st.mode&511})}catch{}}return kept2}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=path98.join(dir,rel);makeParents(dir,rel),fs105.rmSync(file3,{force:!0,recursive:!0}),"link"in was?fs105.symlinkSync(was.link,file3):(fs105.writeFileSync(file3,was.data),fs105.chmodSync(file3,was.mode))}return[...changed,...added2].sort()}function makeParents(dir,rel){let at3=dir;for(let part of path98.dirname(rel).split(path98.sep).filter(p=>p&&p!==".")){at3=path98.join(at3,part);try{fs105.lstatSync(at3).isDirectory()||fs105.rmSync(at3,{force:!0})}catch{}fs105.mkdirSync(at3,{recursive:!0})}}function removeFile(dir,rel){fs105.rmSync(path98.join(dir,rel),{force:!0,recursive:!0});for(let d=path98.dirname(rel);d&&d!==".";d=path98.dirname(d))try{fs105.rmdirSync(path98.join(dir,d))}catch{break}}async function treeOf(dir){let tmp=path98.join(os17.tmpdir(),`squad-index-${randomUUID5()}`);try{let index=path98.resolve(dir,git(dir,["rev-parse","--git-path","index"]));if(fs105.existsSync(index)){fs105.copyFileSync(index,tmp);let st=fs105.statSync(index);fs105.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{fs105.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,path98.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=path98.relative(folder,file3);return r===""||!r.startsWith("..")&&!path98.isAbsolute(r)}import fs120 from"fs";import path111 from"path";import fs106 from"fs";import path99 from"path";var DOCKER_FILE=/^(?:docker-)?compose(?:[.-][\w.-]+)?\.ya?ml$|^Dockerfile(?:[.-][\w.-]+)?$|\.dockerfile$/i,MAX_DOCKER_FILES=30,storyFilesDir=bp=>path99.join(bp.localDir,"story-files"),storyFile2=(bp,f)=>path99.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(path99.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=path99.join(bp.root,r,clean4);if(text3.endsWith("/")||fs106.existsSync(onDisk)&&fs106.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(path99.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 out20=[];for(let repo of repoPaths(meta)){let files=git(path99.join(bp.root,repo),["ls-files","-z"],{allowFail:!0}).split("\0").filter(Boolean);for(let file3 of files)DOCKER_FILE.test(path99.basename(file3))&&!listed2(meta,"locked").some(x=>same3(x,{repo,path:file3}))&&out20.push({repo,path:file3})}return out20.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"&&fs106.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=path99.join(bp.root,f.repo,f.path);if(!fs106.existsSync(from))throw new Error(`There is no ${f.path} in your folder.`);let to=storyFile2(bp,f);return fs106.mkdirSync(path99.dirname(to),{recursive:!0,mode:448}),fs106.copyFileSync(from,to),fs106.chmodSync(to,384),f}function projectFilesView(bp){let meta=readMeta(bp);return{local:listed2(meta,"local").map(f=>({...f,ready:fs106.existsSync(storyFile2(bp,f)),inFolder:fs106.existsSync(path99.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 createHash10}from"crypto";import fs119 from"fs";import net3 from"net";import path110 from"path";var PHONE_COMMANDS=/(?:^|[\s;&|(])(?:flutter|xcodebuild)\s|\b(?:expo|react-native)\s/m;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()}:{}})),phone=new Set(reposOf(meta).filter(r=>PHONE_COMMANDS.test(Object.values(r.commands).join(`
1485
+ `))).map(r=>r.path)),known=all.filter(a=>a.url||a.start||phone.has(a.repo)),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=[],now3=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||now3-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 fs107 from"fs";import os18 from"os";import path100 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=os18.homedir(),system="/var/run/docker.sock"){let named=/^unix:\/\/(\/.+)$/.exec(env2.DOCKER_HOST?.trim()??"")?.[1];for(let place of[named,path100.join(home2,".docker","run","docker.sock"),system])if(place)try{return fs107.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=>fs107.existsSync(path100.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(path100.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}
1486
+ ${repo}`).digest("hex").slice(0,10)}`,config2=path100.join(squadHome(),"docker-config");fs107.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,run7){let socket=run7.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,run7.env.DOCKER_CONFIG],write:[...guard.write,run7.env.DOCKER_CONFIG]}}import fs108 from"fs";import path101 from"path";var lockFile2=bp=>path101.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 hold2=JSON.parse(fs108.readFileSync(lockFile2(bp),"utf8"));if(alive(hold2.pid))return hold2;fs108.rmSync(lockFile2(bp),{force:!0})}catch{}return null}function takeProduct(bp,holder,kind){let now3=productHold(bp);if(now3?.holder===holder)return null;if(now3)return now3;try{return fs108.mkdirSync(bp.localDir,{recursive:!0}),fs108.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&&fs108.rmSync(lockFile2(bp),{force:!0})}var WAIT_LIMIT_MS=45*6e4,TRY_YIELDS_AFTER_MS=15*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{fs108.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))}}import fs115 from"fs";import{spawn as spawn14}from"child_process";import fs112 from"fs";import path104 from"path";import{execFile as execFile6}from"child_process";import{createHash as createHash9}from"crypto";import fs111 from"fs";import path103 from"path";import{once}from"events";var MOBILE=["flutter","react-native","ios","android"],CROSS=["flutter","react-native"],ON_PHONE=["pictures","start-app","try-it"],CHECK_AGAIN="Come back here and press Check again.",OPEN_AGAIN="Close squad-kit and open it again, so it sees the new tool. Then press Check again.",CATALOG4=[{id:"git",name:"Git",gives:"keeps every version of your work safe",needs:[{kinds:"any",priority:"required",for:["build","checks"]}],min:"2.38",installer:"you",why:"it installs for the whole computer, which needs your computer's password",steps:{all:["Open Git's official download page.","Get Git for your computer and install it.",CHECK_AGAIN],windows:["Open Git's official download page.","Get Git for your computer and install it.",OPEN_AGAIN]},link:"https://git-scm.com/downloads",asToday:!0},{id:"ai-tool",name:"An AI tool",gives:"the squad itself: the Analyst, Architect, Builder, Reviewer and Tester",needs:[{kinds:"any",priority:"required",for:["build"]}],installer:"you",why:"it works on your own AI account, which only you can sign in to",steps:{all:["Open a terminal.","Run: npm install -g @anthropic-ai/claude-code","Run: claude, and sign in once.",CHECK_AGAIN]},link:"https://docs.anthropic.com/en/docs/claude-code/setup",asToday:!0},{id:"flutter",name:"Flutter",gives:"builds and tests the Flutter app",needs:[{kinds:["flutter"],priority:"required",for:["build","checks",...ON_PHONE]}],installer:"you",why:"it becomes part of your computer's own setup, and Flutter's installer walks you through it",steps:{mac:["Open Flutter's official install page.","Pick macOS, then iOS, and follow the steps there.","In a terminal, run: flutter doctor, to see that it works.",CHECK_AGAIN],windows:["Open Flutter's official install page.","Pick Windows, then Android, and follow the steps there.","In a terminal, run: flutter doctor, to see that it works.",OPEN_AGAIN],linux:["Open Flutter's official install page.","Pick Linux, then Android, and follow the steps there.","In a terminal, run: flutter doctor, to see that it works.",CHECK_AGAIN]},link:"https://docs.flutter.dev/get-started/install",update:{reaches:"every Flutter project",size:"about 1 GB",seconds:300},updateSteps:{all:["Open a terminal.","Run: flutter upgrade",CHECK_AGAIN],homebrew:["Open a terminal.","Run: brew upgrade --cask flutter",CHECK_AGAIN],fvm:["Open a terminal.","Run: fvm install stable","Run: fvm global stable",CHECK_AGAIN]}},{id:"xcode",name:"Xcode",gives:"the real iPhone, for pictures and Try it yourself",needs:[{kinds:CROSS,platform:"ios",priority:"required",for:ON_PHONE},{kinds:["ios"],priority:"required",for:["build","checks",...ON_PHONE]}],systems:["mac"],notHere:"needs a Mac",installer:"you",why:"it comes from the App Store and needs your Apple ID",steps:{mac:["Open the App Store and get Xcode. It is free, and big: it can take an hour.","Open Xcode once and let it finish setting up.",CHECK_AGAIN]},link:"https://apps.apple.com/app/xcode/id497799835"},{id:"simulator",name:"iPhone simulator",gives:"an iPhone on this Mac, for pictures, Start and Try it yourself",needs:[{kinds:CROSS,platform:"ios",priority:"required",for:ON_PHONE},{kinds:["ios"],priority:"required",for:["checks",...ON_PHONE]}],systems:["mac"],notHere:"needs a Mac",installer:"you",why:"it comes with Xcode, from Apple",steps:{mac:["Open Xcode.","Go to Xcode, then Settings, then Components.","Get the newest iOS.",CHECK_AGAIN]},link:"https://developer.apple.com/documentation/xcode/downloading-and-installing-additional-xcode-components"},{id:"cocoapods",name:"CocoaPods",gives:"brings in the app's iPhone libraries when it builds for the iPhone",needs:[{kinds:CROSS,platform:"ios",priority:"required",for:ON_PHONE}],recommended:"1.16.2",systems:["mac"],notHere:"needs a Mac",installer:"you",why:"it may need your computer's password",steps:{mac:["Open a terminal.","Run: sudo gem install cocoapods. It asks for your computer's password.",CHECK_AGAIN]},link:"https://guides.cocoapods.org/using/getting-started.html",updateSteps:{all:["Open a terminal.","Run: gem update cocoapods","If it says you don't have write permissions, run: sudo gem update cocoapods. It asks for your computer's password.",CHECK_AGAIN],homebrew:["Open a terminal.","Run: brew upgrade cocoapods",CHECK_AGAIN]}},{id:"maestro",name:"Maestro",gives:"taps through the phone app by itself, for the pictures",needs:[{kinds:MOBILE,priority:"required",for:["pictures"]}],installer:"squad-kit",link:"https://maestro.dev",getSeconds:80,with:["java"]},{id:"java",name:"Java",gives:"runs Maestro",needs:[{kinds:MOBILE,priority:"required",for:["pictures"]}],min:"17",installer:"squad-kit",link:"https://adoptium.net",getSeconds:30},{id:"android-studio",name:"Android Studio",gives:"an Android phone on this computer",needs:[{kinds:CROSS,platform:"android",priority:"recommended",for:ON_PHONE},{kinds:["android"],priority:"recommended",for:ON_PHONE}],installer:"you",why:"it is a big app with its own setup",steps:{all:["Open the official download page and get Android Studio.","Open it once and follow its setup.",CHECK_AGAIN]},link:"https://developer.android.com/studio",note:"Android pictures and Start come in the next squad-kit update."},{id:"docker",name:"Docker",gives:"runs the services the app starts with, like its database",needs:[{kinds:["docker"],priority:"recommended",for:["start-app"]}],installer:"you",why:"it installs for the whole computer, which needs your computer's password",steps:{all:["Open the official page and get Docker Desktop.","Open it once and let it start.",CHECK_AGAIN]},link:"https://docs.docker.com/get-started/get-docker/"}];function toolEntry(id2){return CATALOG4.find(t=>t.id===id2)}var KIND_WORDS={web:"web or Node.js code",flutter:"a Flutter app","react-native":"a React Native app",ios:"an iPhone app",android:"an Android app",docker:"Docker files"},PLATFORM_WORDS={ios:"iPhone",android:"Android",web:"web"},ACTION_WORDS={build:"Building",checks:"Checks",pictures:"Phone pictures","start-app":"Start","try-it":"Try it yourself"},ACTION_DOING={build:"build this story",checks:"run this story's checks",pictures:"take the phone pictures","start-app":"start the app on a phone","try-it":"open Try it yourself on a phone"},OWN_SOURCES={maestro:{version:"2.12.1",url:"https://github.com/mobile-dev-inc/maestro/releases/download/cli-2.12.1/maestro.zip",checksums:"https://github.com/mobile-dev-inc/maestro/releases/download/cli-2.12.1/checksums_sha256.txt",file:"maestro.zip",sha256:"54dbb0716425626604973cbb374c585ca30272375d9ce15177a1152765ddd9d7",size:349832427,env:{MAESTRO_CLI_NO_ANALYTICS:"1"}},java:{feature:21,api:"https://api.adoptium.net/v3/assets/latest"}};import{execFile as execFile5}from"child_process";import fs110 from"fs";import os19 from"os";import path102 from"path";function compareVersions(a,b){let nums=v=>(/^\d+(?:\.\d+)*/.exec(v.trim())?.[0]??"0").split(".").map(Number),x=nums(a),y=nums(b);for(let i=0;i<Math.max(x.length,y.length);i+=1){let d=(x[i]??0)-(y[i]??0);if(d)return d}return 0}var atLeast2=(found2,min)=>compareVersions(found2,min)>=0;function updateRecommended(found2,recommended){return found2&&recommended&&!atLeast2(found2,recommended)?recommended:null}var PAIR_RULES=[{tool:"flutter",platform:"ios",applies:v=>!!(v.flutter&&v.xcode&&compareVersions(v.xcode,"27")>=0&&compareVersions(v.flutter,"3.44.8")<0),needs:"3.44.8",why:"Xcode 27 needs it for iPhone builds",for:["pictures","start-app","try-it"]}];function tooOld(id2,min,versions2,platforms){let v=versions2[id2];if(!v)return null;if(min&&!atLeast2(v,min))return{needs:min};let rule=PAIR_RULES.find(r=>r.tool===id2&&(!r.platform||platforms.has(r.platform))&&r.applies(versions2));return rule?{needs:rule.needs,why:rule.why,for:rule.for}:null}import{spawn as spawn13}from"child_process";import fs109 from"fs";var START="__SQUAD_KIT_PATH_START__",END="__SQUAD_KIT_PATH_END__",PRINT_PATH=`printf '%s%s%s' '${START}' "$PATH" '${END}'`;function pathBetweenMarks(output){let from=output.lastIndexOf(START);if(from<0)return null;let to=output.indexOf(END,from+START.length);return to<0?null:output.slice(from+START.length,to).trim()||null}function mergePath(current,more3,sep=":"){let seen=new Set,out20=[];for(let d of[...(current??"").split(sep),...(more3??"").split(sep)])!d||seen.has(d)||(seen.add(d),out20.push(d));return out20.join(sep)}function loginShell(system,env2=process.env,exists3=fs109.existsSync){let own2=env2.SHELL?.trim();return own2&&exists3(own2)?own2:system==="mac"?"/bin/zsh":"/bin/bash"}function readLoginPath(system,opts={}){if(system==="windows")return Promise.resolve(null);let env2=opts.env??process.env,shell2=opts.shell??loginShell(system,env2);return new Promise(resolve2=>{let out20="",done2=!1,end=value=>{done2||(done2=!0,resolve2(value))},child;try{child=spawn13(shell2,["-ilc",PRINT_PATH],{env:env2,stdio:["ignore","pipe","ignore"],windowsHide:!0})}catch{resolve2(null);return}let timer=setTimeout(()=>{child.exitCode===null&&child.signalCode===null&&child.kill("SIGKILL"),child.stdout?.destroy(),end(null)},opts.limitMs??5e3);timer.unref(),child.stdout?.setEncoding("utf8"),child.stdout?.on("data",s=>{out20=(out20+s).slice(-256*1024);let found2=pathBetweenMarks(out20);found2&&end(found2)}),child.on("error",()=>{clearTimeout(timer),end(null)}),child.on("close",()=>{clearTimeout(timer),end(pathBetweenMarks(out20))})})}function systemOf(platform=process.platform){return platform==="darwin"?"mac":platform==="win32"?"windows":"linux"}function realHost(){return{system:systemOf(),home:os19.homedir(),env:process.env,run:(cmd15,args,opts={})=>new Promise(resolve2=>{execFile5(cmd15,args,{timeout:opts.timeoutMs??15e3,encoding:"utf8",windowsHide:!0,maxBuffer:4*1024*1024,env:{...process.env,...opts.env}},(err2,out20,errText)=>resolve2({ok:!err2,out:String(out20??""),err:String(errText??"")})).stdin?.end()}),exists:p=>fs110.existsSync(p),read:p=>{try{return fs110.readFileSync(p,"utf8")}catch{return null}},real:p=>{try{return fs110.realpathSync(p)}catch{return p}},list:dir=>{try{return fs110.readdirSync(dir)}catch{return[]}},writable:p=>{try{return fs110.accessSync(p,fs110.constants.W_OK),!0}catch{return!1}},loginPath:()=>readLoginPath(systemOf())}}function onPath3(h,name){let dirs=(h.env.PATH??h.env.Path??"").split(h.system==="windows"?";":":").filter(Boolean),names2=h.system==="windows"?[`${name}.exe`,`${name}.bat`,`${name}.cmd`,name]:[name];for(let d of dirs)for(let n of names2)if(h.exists(path102.join(d,n)))return path102.join(d,n);return null}function flutterVersionOf(text3){try{let j=JSON.parse(text3),v=j.frameworkVersion??j.flutterVersion;if(v)return v}catch{}return/Flutter\s+(\d+\.\d+\.\d+(?:[-+.\w]*)?)/.exec(text3)?.[1]??/^\s*(\d+\.\d+\.\d+)\s*$/.exec(text3)?.[1]??null}function xcodeVersionOf(text3){let v=/Xcode\s+(\d+(?:\.\d+)*)/.exec(text3)?.[1];if(!v)return null;let build=/Build version\s+(\S+)/.exec(text3)?.[1];return{version:v,...build?{build}:{}}}function javaVersionOf(text3){let v=/version\s+"([^"]+)"/.exec(text3)?.[1];if(!v)return null;let old=/^1\.(\d+)(.*)$/.exec(v);return(old?`${old[1]}${old[2]}`:v).replace(/_/g,".")}function podVersionOf(text3){return/^(\d+\.\d+(?:\.\d+)?)\s*$/m.exec(text3)?.[1]??null}function iosRuntimesOf(text3){try{let versions2=(JSON.parse(text3).runtimes??[]).filter(r=>(r.platform?r.platform==="iOS":/^iOS\b/.test(r.name??""))&&r.isAvailable!==!1&&r.version).map(r=>r.version);return[...new Set(versions2)].sort((a,b)=>compareVersions(b,a))}catch{return[]}}function dockerVersionOf(text3){return/Docker version\s+(\d+\.\d+(?:\.\d+)?)/.exec(text3)?.[1]??null}function maestroVersionOf(text3){let lines3=text3.trim().split(`
1487
+ `).reverse();for(let l of lines3){let m=/^\s*(\d+\.\d+\.\d+)\s*$/.exec(l);if(m)return m[1]}return null}function toolsHome(home2=squadHome()){return path102.join(home2,"tools")}function ownTool(id2,home2=squadHome()){let base2=path102.join(toolsHome(home2),id2),best=null,names2=[];try{names2=fs110.readdirSync(base2)}catch{return null}for(let v of names2)try{let mark=JSON.parse(fs110.readFileSync(path102.join(base2,v,"ready.json"),"utf8")),binary=path102.join(base2,v,mark.binary);if(!fs110.existsSync(binary))continue;(!best||compareVersions(mark.version,best.version)>0)&&(best={version:mark.version,dir:path102.join(base2,v),binary,...mark.seconds?{seconds:mark.seconds}:{}})}catch{}return best}var missing2=note2=>({state:"missing",...note2?{note:note2}:{}});function flutterPlaces(h){let home2=h.home;return h.system==="windows"?["C:\\src\\flutter","C:\\flutter",path102.join(home2,"flutter"),path102.join(home2,"dev","flutter")]:[path102.join(home2,"flutter"),path102.join(home2,"development","flutter"),path102.join(home2,"Applications","flutter"),path102.join(home2,"sdk","flutter"),path102.join(home2,"fvm","default"),"/opt/flutter","/usr/local/flutter"]}function flutterFileVersion(h,sdk){let json2=h.read(path102.join(sdk,"bin","cache","flutter.version.json")),fromJson=json2?flutterVersionOf(json2):null;if(fromJson)return fromJson;let plain=h.read(path102.join(sdk,"version"));return plain?flutterVersionOf(plain):null}async function flutter(h){let bin=onPath3(h,"flutter");if(bin){let sdk=path102.dirname(path102.dirname(h.real(bin))),version=flutterFileVersion(h,sdk);if(!version){let r=await h.run(bin,["--version","--machine"],{timeoutMs:3e4,env:{FLUTTER_SUPPRESS_ANALYTICS:"true",CI:"true"}});version=flutterVersionOf(r.out)??flutterVersionOf(r.err)}return{state:"found",where:sdk,...version?{version}:{}}}for(let sdk of flutterPlaces(h)){if(!h.exists(path102.join(sdk,"bin",h.system==="windows"?"flutter.bat":"flutter")))continue;let version=flutterFileVersion(h,sdk);return{state:"missing",where:sdk,...version?{version}:{},note:`Flutter${version?` ${version}`:""} is in ${sdk}, but not on the PATH squad-kit runs with, so the squad can't use it.`}}return missing2()}async function xcode(h){let sel=await h.run("xcode-select",["-p"],{timeoutMs:5e3}),dev=sel.out.trim();if(!sel.ok||!dev)return missing2();if(!/\.app\//.test(`${dev}/`))return missing2("Only Apple's command line tools are here, not Xcode itself.");let r=await h.run("xcodebuild",["-version"],{timeoutMs:15e3}),v=xcodeVersionOf(r.out);return r.ok&&v?{state:"found",version:v.version,where:dev.replace(/\/Contents\/Developer\/?$/,"")}:/licen[cs]e/i.test(r.err+r.out)?missing2("Xcode is here, but it has to be opened once to finish setting up."):missing2()}async function simulator(h,xcodeFound){if(!xcodeFound)return missing2("It comes with Xcode.");let r=await h.run("xcrun",["simctl","list","runtimes","-j"],{timeoutMs:2e4}),versions2=r.ok?iosRuntimesOf(r.out):[];return versions2.length?{state:"found",version:versions2[0],...versions2.length>1?{note:`iOS ${versions2.join(", iOS ")}`}:{}}:missing2("Xcode is here, without an iPhone simulator yet.")}async function cocoapods(h){let bin=onPath3(h,"pod");if(!bin)return missing2();let r=await h.run(bin,["--version"],{timeoutMs:2e4,env:{LANG:"en_US.UTF-8"}}),version=podVersionOf(r.out),brew=/\/Cellar\/cocoapods\//.test(h.real(bin));return{state:"found",where:bin,...version?{version}:{},...brew?{managedBy:"homebrew"}:{}}}async function java(h,home2){let own2=ownTool("java",home2);if(own2)return{state:"found",version:own2.version,where:own2.dir,own:!0};let fromHome=h.env.JAVA_HOME?path102.join(h.env.JAVA_HOME,"bin",h.system==="windows"?"java.exe":"java"):null,bin=fromHome&&h.exists(fromHome)?fromHome:onPath3(h,"java");if(!bin||h.system==="mac"&&h.real(bin)==="/usr/bin/java"&&![...h.list("/Library/Java/JavaVirtualMachines"),...h.list(path102.join(h.home,"Library","Java","JavaVirtualMachines"))].some(m=>m.endsWith(".jdk")))return missing2();let r=await h.run(bin,["-version"],{timeoutMs:15e3}),version=javaVersionOf(r.err)??javaVersionOf(r.out);return version?{state:"found",version,where:bin}:missing2()}async function maestro(h,home2){let own2=ownTool("maestro",home2);if(own2)return{state:"found",version:own2.version,where:own2.dir,own:!0};let bin=onPath3(h,"maestro");if(!bin)return missing2();let r=await h.run(bin,["--version"],{timeoutMs:3e4,env:{...OWN_SOURCES.maestro.env}}),version=maestroVersionOf(r.out);return{state:"found",where:bin,...version?{version}:{}}}function androidStudio(h){let env2=h.env,apps2=h.system==="mac"?["/Applications/Android Studio.app",path102.join(h.home,"Applications","Android Studio.app")].map(a=>({dir:a,info:path102.join(a,"Contents","Resources","product-info.json")})):h.system==="windows"?[path102.join(env2.ProgramFiles??"C:\\Program Files","Android","Android Studio")].map(a=>({dir:a,info:path102.join(a,"product-info.json")})):["/opt/android-studio",path102.join(h.home,"android-studio"),"/snap/android-studio/current"].map(a=>({dir:a,info:path102.join(a,"product-info.json")}));for(let a of apps2){if(!h.exists(a.dir))continue;let version;try{version=JSON.parse(h.read(a.info)??"{}").version}catch{}return{state:"found",where:a.dir,...version?{version}:{}}}let sdk=env2.ANDROID_HOME??env2.ANDROID_SDK_ROOT;return sdk&&h.exists(sdk)?{state:"found",where:sdk,note:"The Android tools are here, without the Android Studio app."}:missing2()}async function docker(h){let bin=onPath3(h,"docker");if(!bin)return missing2();let r=await h.run(bin,["--version"],{timeoutMs:1e4}),version=dockerVersionOf(r.out);return{state:"found",where:bin,...version?{version}:{}}}async function detectAll(h=realHost(),home2=squadHome()){let more3=await h.loginPath?.().catch(()=>null);more3&&(h.env.PATH=mergePath(h.env.PATH,more3));let notHere=id2=>{let e=CATALOG4.find(t=>t.id===id2);return!!(e?.systems&&!e.systems.includes(h.system))},safe2=async(id2,find2)=>{if(notHere(id2))return[id2,{state:"not-here"}];try{return[id2,await find2()]}catch{return[id2,missing2()]}},x=await safe2("xcode",()=>xcode(h)),found2=await Promise.all([safe2("flutter",()=>flutter(h)),safe2("simulator",()=>simulator(h,x[1].state==="found")),safe2("cocoapods",()=>cocoapods(h)),safe2("java",()=>java(h,home2)),safe2("maestro",()=>maestro(h,home2)),safe2("android-studio",()=>androidStudio(h)),safe2("docker",()=>docker(h))]);return{at:new Date().toISOString(),system:h.system,tools:Object.fromEntries([x,...found2])}}function foundFile(home2=squadHome()){return path102.join(toolsHome(home2),"found.json")}function readFound(home2=squadHome()){try{return JSON.parse(fs110.readFileSync(foundFile(home2),"utf8"))}catch{return null}}function writeFound(book,home2=squadHome()){let file3=foundFile(home2);fs110.mkdirSync(path102.dirname(file3),{recursive:!0});let tmp=`${file3}.${process.pid}.tmp`;fs110.writeFileSync(tmp,`${JSON.stringify(book,null,2)}
1488
1488
  `),fs110.renameSync(tmp,file3)}var GetTrouble=class extends Error{},now2=null,troubles=new Map;function gettingNow(){return now2?{...now2}:null}function lastTrouble(id2){return troubles.get(id2)}function setTrouble(id2,words2){words2?troubles.set(id2,words2):troubles.delete(id2)}function startOne(first2,job,after){if(now2)throw new GetTrouble(now2.step==="update"?`squad-kit is updating ${now2.name} now. One at a time: try again once it is done.`:`squad-kit is getting ${now2.name} now. One download at a time: press Get it again once it is done.`);return now2=first2,job(next=>{now2&&(now2=next)}).catch(err2=>writeLog({level:"warn",text:`${first2.step==="update"?"Updating":"Getting"} ${first2.name}: ${err2 instanceof Error?err2.stack??err2.message:String(err2)}`})).finally(()=>{now2=null,after?.()}),{...first2}}var nameOf=id2=>toolEntry(id2)?.name??id2;function reach(name){return new GetTrouble(`squad-kit could not reach ${name}'s official download. Check the internet connection, then press Get it again.`)}async function download(f,url,name,signal2){let res=await f(url,{signal:signal2,redirect:"follow"}).catch(()=>{throw reach(name)});if(!res.ok||!res.body)throw reach(name);let size2=Number(res.headers.get("content-length"))||void 0;return{body:res.body,...size2?{size:size2}:{}}}function findIn2(dir,tails){let tops=[dir,...safeList(dir).map(n=>path103.join(dir,n))];for(let top of tops)for(let tail3 of tails)if(fs111.existsSync(path103.join(top,...tail3)))return path103.join(top,...tail3);return null}function safeList(dir){try{return fs111.readdirSync(dir).filter(n=>!n.startsWith("."))}catch{return[]}}function maestroSource(f=fetch,system=systemOf()){let m=OWN_SOURCES.maestro;return{version:m.version,file:m.file,pack:"zip",size:m.size,async sha256(signal2){let res=await f(m.checksums,{signal:signal2,redirect:"follow"}).catch(()=>{throw reach("Maestro")});if(!res.ok)throw reach("Maestro");let line3=(await res.text()).split(`
1489
- `).find(l=>l.includes(m.file)),sum=/\b([0-9a-f]{64})\b/i.exec(line3??"")?.[1]?.toLowerCase();if(!sum||sum!==m.sha256)throw new GetTrouble("Maestro's published checksum is not the one squad-kit knows for this version, so squad-kit did not download it. Nothing was installed.");return sum},open:signal2=>download(f,m.url,"Maestro",signal2),binary:dir=>findIn2(dir,[["bin",system==="windows"?"maestro.bat":"maestro"]])}}async function javaSource(f=fetch,system=systemOf(),arch=process.arch,signal2){let j=OWN_SOURCES.java,os29=system==="windows"?"windows":system,chip=arch==="arm64"?"aarch64":arch==="x64"?"x64":arch,url=`${j.api}/${j.feature}/hotspot?architecture=${chip}&image_type=jre&os=${os29}&vendor=eclipse`,res=await f(url,{...signal2?{signal:signal2}:{},redirect:"follow"}).catch(()=>{throw reach("Java")});if(!res.ok)throw reach("Java");let pick2=(await res.json().catch(()=>[])).find(x=>x.binary?.package?.link&&x.binary.package.checksum),pkg=pick2?.binary?.package;if(!pick2||!pkg?.link||!pkg.checksum)throw new GetTrouble("squad-kit found no Java for this computer on Adoptium's official site. Nothing was installed.");let name=pkg.name??path103.basename(pkg.link);return{version:(pick2.release_name??`${j.feature}`).replace(/^jdk-?/,""),file:name,pack:name.endsWith(".zip")?"zip":"tar.gz",...pkg.size?{size:pkg.size}:{},sha256:async()=>pkg.checksum.toLowerCase(),open:s=>download(f,pkg.link,"Java",s),binary:dir=>findIn2(dir,[["Contents","Home","bin","java"],["bin",system==="windows"?"java.exe":"java"]])}}async function officialSource(id2,signal2){if(id2==="maestro")return maestroSource();if(id2==="java")return javaSource(fetch,systemOf(),process.arch,signal2);throw new GetTrouble(`squad-kit does not get ${nameOf(id2)} itself. The Tools page says how to install it.`)}var timesFile=home2=>path103.join(toolsHome(home2),"times.json");function measured(key,home2){try{return JSON.parse(fs111.readFileSync(timesFile(home2),"utf8"))[key]}catch{return}}function noteTime(key,seconds2,home2){try{let all=fs111.existsSync(timesFile(home2))?JSON.parse(fs111.readFileSync(timesFile(home2),"utf8")):{};fs111.mkdirSync(path103.dirname(timesFile(home2)),{recursive:!0}),fs111.writeFileSync(timesFile(home2),`${JSON.stringify({...all,[key]:Math.max(1,Math.round(seconds2))},null,2)}
1489
+ `).find(l=>l.includes(m.file)),sum=/\b([0-9a-f]{64})\b/i.exec(line3??"")?.[1]?.toLowerCase();if(!sum||sum!==m.sha256)throw new GetTrouble("Maestro's published checksum is not the one squad-kit knows for this version, so squad-kit did not download it. Nothing was installed.");return sum},open:signal2=>download(f,m.url,"Maestro",signal2),binary:dir=>findIn2(dir,[["bin",system==="windows"?"maestro.bat":"maestro"]])}}async function javaSource(f=fetch,system=systemOf(),arch=process.arch,signal2){let j=OWN_SOURCES.java,os30=system==="windows"?"windows":system,chip=arch==="arm64"?"aarch64":arch==="x64"?"x64":arch,url=`${j.api}/${j.feature}/hotspot?architecture=${chip}&image_type=jre&os=${os30}&vendor=eclipse`,res=await f(url,{...signal2?{signal:signal2}:{},redirect:"follow"}).catch(()=>{throw reach("Java")});if(!res.ok)throw reach("Java");let pick2=(await res.json().catch(()=>[])).find(x=>x.binary?.package?.link&&x.binary.package.checksum),pkg=pick2?.binary?.package;if(!pick2||!pkg?.link||!pkg.checksum)throw new GetTrouble("squad-kit found no Java for this computer on Adoptium's official site. Nothing was installed.");let name=pkg.name??path103.basename(pkg.link);return{version:(pick2.release_name??`${j.feature}`).replace(/^jdk-?/,""),file:name,pack:name.endsWith(".zip")?"zip":"tar.gz",...pkg.size?{size:pkg.size}:{},sha256:async()=>pkg.checksum.toLowerCase(),open:s=>download(f,pkg.link,"Java",s),binary:dir=>findIn2(dir,[["Contents","Home","bin","java"],["bin",system==="windows"?"java.exe":"java"]])}}async function officialSource(id2,signal2){if(id2==="maestro")return maestroSource();if(id2==="java")return javaSource(fetch,systemOf(),process.arch,signal2);throw new GetTrouble(`squad-kit does not get ${nameOf(id2)} itself. The Tools page says how to install it.`)}var timesFile=home2=>path103.join(toolsHome(home2),"times.json");function measured(key,home2){try{return JSON.parse(fs111.readFileSync(timesFile(home2),"utf8"))[key]}catch{return}}function noteTime(key,seconds2,home2){try{let all=fs111.existsSync(timesFile(home2))?JSON.parse(fs111.readFileSync(timesFile(home2),"utf8")):{};fs111.mkdirSync(path103.dirname(timesFile(home2)),{recursive:!0}),fs111.writeFileSync(timesFile(home2),`${JSON.stringify({...all,[key]:Math.max(1,Math.round(seconds2))},null,2)}
1490
1490
  `)}catch{}}function firstGuess(id2,home2=squadHome()){return measured(id2,home2)??toolEntry(id2)?.getSeconds??60}function run4(cmd15,args){return new Promise(resolve2=>{execFile6(cmd15,args,{timeout:10*6e4,windowsHide:!0,maxBuffer:16*1024*1024},err2=>resolve2(!err2))})}async function unpack(file3,dir,pack,system){return fs111.mkdirSync(dir,{recursive:!0}),pack==="tar.gz"?run4("tar",["-xzf",file3,"-C",dir]):system==="windows"?run4("tar",["-xf",file3,"-C",dir]):await run4("unzip",["-q","-o",file3,"-d",dir])||run4("tar",["-xf",file3,"-C",dir])}function clearHalfGets(home2=squadHome()){let base2=toolsHome(home2);for(let n of safeListAll(base2))n.startsWith(".part-")&&fs111.rmSync(path103.join(base2,n),{recursive:!0,force:!0});for(let e of CATALOG4.filter(t=>t.installer==="squad-kit")){let dir=path103.join(base2,e.id);for(let v of safeListAll(dir))isDir(path103.join(dir,v))&&!fs111.existsSync(path103.join(dir,v,"ready.json"))&&fs111.rmSync(path103.join(dir,v),{recursive:!0,force:!0})}}function safeListAll(dir){try{return fs111.readdirSync(dir)}catch{return[]}}function isDir(p){try{return fs111.statSync(p).isDirectory()}catch{return!1}}async function getTool(id2,source2,opts={}){let home2=opts.home??squadHome(),name=nameOf(id2),signal2=opts.signal??new AbortController().signal,started2=Date.now();fs111.mkdirSync(toolsHome(home2),{recursive:!0});let tmp=fs111.mkdtempSync(path103.join(toolsHome(home2),`.part-${id2}-`));try{let expected=(await source2.sha256(signal2)).toLowerCase();if(!/^[0-9a-f]{64}$/.test(expected))throw new GetTrouble(`squad-kit could not read ${name}'s published checksum, so it did not download it. Nothing was installed.`);let file3=path103.join(tmp,path103.basename(source2.file)),{body:body10,size:size2}=await source2.open(signal2),total=size2??source2.size,hash2=createHash9("sha256"),out20=fs111.createWriteStream(file3,{fd:fs111.openSync(file3,"w")}),closed=()=>out20.closed?Promise.resolve():once(out20,"close").then(()=>{},()=>{}),done2=0;try{for await(let chunk of body10){if(signal2.aborted)throw new GetTrouble(`The download of ${name} was stopped. Nothing was installed.`);hash2.update(chunk),done2+=chunk.length,out20.write(chunk)||await once(out20,"drain"),opts.onProgress?.({done:done2,...total?{total}:{},step:"download"})}out20.end(),await once(out20,"close")}catch(err2){throw out20.destroy(),await closed(),err2 instanceof GetTrouble?err2:(writeLog({level:"warn",text:`Getting ${name}: the download broke off: ${err2 instanceof Error?err2.message:String(err2)}`}),new GetTrouble(`The download of ${name} stopped before the end, so squad-kit threw it away. Nothing was installed. Press Get it again.`))}if(total&&done2!==total)throw new GetTrouble(`The download of ${name} stopped before the end, so squad-kit threw it away. Nothing was installed. Press Get it again.`);if(hash2.digest("hex")!==expected)throw new GetTrouble(`The download of ${name} did not match its published checksum, so squad-kit threw it away. Nothing was installed.`);opts.onProgress?.({done:done2,...total?{total}:{},step:"unpack"});let unpacked=path103.join(tmp,"unpacked"),system=opts.system??systemOf();if(!await unpack(file3,unpacked,source2.pack,system))throw new GetTrouble(`squad-kit could not unpack ${name}. Nothing was installed. Press Get it again.`);let bin=source2.binary(unpacked);if(!bin)throw new GetTrouble(`${name}'s download did not have the program squad-kit expected, so squad-kit did not use it. Nothing was installed.`);system!=="windows"&&fs111.chmodSync(bin,493);let binary=path103.relative(unpacked,bin),dir=path103.join(toolsHome(home2),id2,source2.version);fs111.rmSync(dir,{recursive:!0,force:!0}),fs111.mkdirSync(path103.dirname(dir),{recursive:!0}),fs111.renameSync(unpacked,dir);let seconds2=(Date.now()-started2)/1e3;return fs111.writeFileSync(path103.join(dir,"ready.json"),`${JSON.stringify({version:source2.version,binary,sha256:expected,at:new Date().toISOString(),seconds:Math.round(seconds2)},null,2)}
1491
1491
  `),noteTime(id2,seconds2,home2),{version:source2.version,dir,binary:path103.join(dir,binary),seconds:Math.round(seconds2)}}finally{fs111.rmSync(tmp,{recursive:!0,force:!0})}}function startGets(ids,opts={}){let home2=opts.home??squadHome(),first2=ids[0],job=async set2=>{clearHalfGets(home2);for(let id2 of ids){let guess=firstGuess(id2,home2),t0=Date.now(),g={tool:id2,name:nameOf(id2),startedAt:new Date().toISOString(),step:"download",done:0,secondsLeft:guess};set2(g),troubles.delete(id2);try{let source2=await(opts.source??officialSource)(id2);g={...g,version:source2.version,...source2.size?{total:source2.size}:{}},set2(g);let got=await getTool(id2,source2,{home:home2,onProgress:p=>{let elapsed=(Date.now()-t0)/1e3,rate=elapsed>2&&p.done?p.done/elapsed:0,left2=p.step==="unpack"?Math.max(3,guess*.1):rate&&p.total?(p.total-p.done)/rate+guess*.1:Math.max(5,guess-elapsed);g={...g,step:p.step,done:p.done,...p.total?{total:p.total}:{},secondsLeft:Math.round(left2)},set2(g)}});writeLog({level:"info",text:`Got ${nameOf(id2)} ${got.version} in ${got.seconds??0} s, into ${got.dir}`})}catch(err2){let words2=err2 instanceof GetTrouble?err2.message:`squad-kit could not get ${nameOf(id2)}. Nothing was installed. Press Get it again.`;err2 instanceof GetTrouble||writeLog({level:"warn",text:`Getting ${nameOf(id2)}: ${err2 instanceof Error?err2.stack??err2.message:String(err2)}`}),troubles.set(id2,words2);break}}};return startOne({tool:first2,name:nameOf(first2),startedAt:new Date().toISOString(),step:"download",done:0,secondsLeft:firstGuess(first2,home2)},job,opts.after)}var STEPS2={build:{key:"iphone-build",guess:300},boot:{key:"iphone-boot",guess:60},install:{key:"iphone-install",guess:15},"first-run":{key:"maestro-first",guess:60}};function stepSeconds2(step2,home2=squadHome()){return measured(STEPS2[step2].key,home2)??STEPS2[step2].guess}function noteStep(step2,seconds2,home2=squadHome()){noteTime(STEPS2[step2].key,seconds2,home2)}function aboutWords(seconds2){return seconds2<50?`about ${Math.max(5,Math.round(seconds2/5)*5)} seconds`:seconds2<90?"about a minute":`about ${Math.round(seconds2/60)} minutes`}function stepTimes(home2=squadHome()){return Object.fromEntries(Object.keys(STEPS2).map(s=>[s,stepSeconds2(s,home2)]))}var BUILD_DIR=path104.join("build","squad-kit-ios"),BUILD_MS=40*6e4,realRun2=(cmd15,args,o)=>new Promise(resolve2=>{let output="",done2=!1,child=spawn14(cmd15,args,{cwd:o.cwd,env:o.env,detached:process.platform!=="win32",stdio:["ignore","pipe","pipe"]}),keep=d=>output=(output+d.toString()).slice(-6e4);child.stdout?.on("data",keep),child.stderr?.on("data",keep);let end=()=>{done2||endGroup(child)},timer=setTimeout(()=>{output+=`
1492
1492
  It ran longer than ${Math.round(o.timeoutMs/6e4)} minutes, so squad-kit stopped it.`,end()},o.timeoutMs);o.signal?.addEventListener("abort",end,{once:!0});let finish2=(ok2,extra="")=>{done2||(done2=!0,clearTimeout(timer),o.signal?.removeEventListener("abort",end),resolve2({ok:ok2,output:`${output}${extra}`}))};child.on("error",err2=>finish2(!1,`
@@ -1496,9 +1496,9 @@ ${err2.message}`)),child.on("close",code=>finish2(code===0&&!o.signal?.aborted))
1496
1496
  `);async function buildForPhone(dir,kind,o={}){let home2=o.home??squadHome(),tree=await treeOf(dir),last=lastBuild(dir);if(last&&tree&&last.tree===tree&&fs112.existsSync(last.app))return o.log?.(`The app for the iPhone is built already from this code (${tree.slice(0,12)}), so squad-kit uses it`),last;let plan=buildPlan(dir,kind);if(!plan)throw new BuildFailed("squad-kit could not find the app's iPhone project in this story's copy.",`No .xcworkspace or .xcodeproj in ${kind==="react-native"?path104.join(dir,"ios"):dir}`);ownFolder2(dir),o.say?.(`Building the app for the iPhone, ${aboutWords(stepSeconds2("build",home2))}`);let t0=Date.now(),env2={...process.env,FLUTTER_SUPPRESS_ANALYTICS:"true",LANG:process.env.LANG||"en_US.UTF-8",COCOAPODS_DISABLE_STATS:"true"};for(let s of plan.steps){if(o.signal?.aborted)throw new Error("You stopped the squad.");o.log?.(`squad-kit runs ${s.what} in ${path104.join(dir,s.cwd)}: ${s.cmd} ${s.args.join(" ")}`);let r=await runner2(s.cmd,s.args,{cwd:path104.join(dir,s.cwd),env:env2,...o.signal?{signal:o.signal}:{},timeoutMs:BUILD_MS});if(o.signal?.aborted)throw new Error("You stopped the squad.");if(!r.ok)throw new BuildFailed("The app did not build for the iPhone.",`${s.cmd} ${s.args.join(" ")} in ${path104.join(dir,s.cwd)}:
1497
1497
  ${tail2(r.output)}`)}let app=appIn(plan.products);if(!app)throw new BuildFailed("The app did not build for the iPhone.",`The build ended well, but no .app is in ${plan.products}`);let seconds2=(Date.now()-t0)/1e3;noteStep("build",seconds2,home2);let built={app,...tree?{tree}:{},at:new Date().toISOString(),seconds:Math.round(seconds2)};return fs112.writeFileSync(recordFile(dir),`${JSON.stringify(built,null,2)}
1498
1498
  `),o.log?.(`squad-kit built the app for the iPhone in ${Math.round(seconds2)} s: ${app}`),built}import fs113 from"fs";import path105 from"path";function phonesHome(home2=squadHome()){return path105.join(home2,"phones")}function devicesFile(home2=squadHome()){return path105.join(phonesHome(home2),"devices.json")}function savedDevices(home2=squadHome()){try{return(JSON.parse(fs113.readFileSync(devicesFile(home2),"utf8")).devices??[]).filter(d=>d&&typeof d.id=="string"&&(d.size==="phone"||d.size==="tablet"))}catch{return[]}}function saveDevices(list6,home2=squadHome()){let file3=devicesFile(home2);fs113.mkdirSync(path105.dirname(file3),{recursive:!0});let tmp=`${file3}.${process.pid}.tmp`;fs113.writeFileSync(tmp,`${JSON.stringify({devices:list6},null,2)}
1499
- `),fs113.renameSync(tmp,file3)}function ownName(phone,size2){return`squad-kit ${phone.words[size2]}`}function pickModel(runtime,size2){let fits4=runtime.models.filter(m=>m.size===size2);return(size2==="phone"?fits4.find(m=>/\bPro$/.test(m.name)):fits4.find(m=>/^iPad Air\b/.test(m.name)))??fits4[0]??null}function pickRuntime(runtimes,size2,wanted){let able=runtimes.filter(r=>pickModel(r,size2));return wanted?able.find(r=>r.version===wanted||r.id===wanted)??null:able[0]??null}async function ownDevice(phone,size2,o={}){let home2=o.home??squadHome(),[runtimes,devices]=await Promise.all([phone.runtimes(),phone.devices()]),here=new Map(devices.map(d=>[d.id,d])),usable=new Set(runtimes.map(r=>r.id)),saved=savedDevices(home2),wanted=o.runtime?pickRuntime(runtimes,size2,o.runtime):null;if(o.runtime&&!wanted)throw new PhoneError(`This Mac has no iOS ${o.runtime} for the ${phone.words[size2]}.`,`No iOS ${o.runtime} runtime that can make a ${size2} in: ${runtimes.map(r=>r.version).join(", ")||"none"}`);let keep=saved.find(d=>d.size===size2&&here.has(d.id)&&usable.has(d.runtimeId)&&(!wanted||d.runtimeId===wanted.id));if(keep)return{...keep,state:here.get(keep.id).state};let runtime=wanted??pickRuntime(runtimes,size2);if(!runtime)throw new PhoneError(`This Mac has no iOS simulator that can run an ${phone.words[size2]} yet.`,`No runtime with a ${size2} model in: ${runtimes.map(r=>r.version).join(", ")||"none"}`);let model=pickModel(runtime,size2),name=ownName(phone,size2),id2=await phone.create(name,model.id,runtime.id),made={size:size2,id:id2,name,model:model.name,runtime:runtime.version,runtimeId:runtime.id,createdAt:new Date().toISOString()};return saveDevices([...savedDevices(home2).filter(d=>here.has(d.id)&&d.id!==id2),made],home2),{...made,state:"shutdown"}}import{execFile as execFile7}from"child_process";import fs114 from"fs";import os19 from"os";import path106 from"path";var IDLE_MS2=10*6e4,MIN_FREE_BYTES=2*1024**3,BUSY_LOOK_MS=15e3,BUSY_WAIT_MS=10*6e4,LOCK_LOOK_MS=2e3,VIEWER_LOOK_MS=5e3;function busyWords(word2){return`The computer is busy. squad-kit waits a moment before starting the ${word2}.`}function otherWords(word2){return`Another squad-kit on this computer is using the ${word2}. squad-kit waits for its turn.`}function freeMemoryOf(vmStat){let page=Number(/page size of (\d+) bytes/.exec(vmStat)?.[1]??4096),pages=name=>Number(new RegExp(`Pages ${name}:\\s+(\\d+)`).exec(vmStat)?.[1]??NaN),n=pages("free")+pages("inactive")+pages("speculative");return Number.isFinite(n)?n*page:null}var realHost2={freeBytes:()=>new Promise(resolve2=>{if(process.platform!=="darwin")return resolve2(os19.freemem());execFile7("vm_stat",[],{timeout:1e4,encoding:"utf8"},(err2,out20)=>resolve2(err2?os19.freemem():freeMemoryOf(String(out20))??os19.freemem()))}),load:()=>os19.loadavg()[0]??0,cores:()=>os19.cpus().length||4,alive:pid=>{try{return process.kill(pid,0),!0}catch(err2){return err2.code==="EPERM"}},sleep:(ms2,signal2)=>new Promise(resolve2=>{let timer=setTimeout(done2,ms2);function done2(){clearTimeout(timer),signal2?.removeEventListener("abort",done2),resolve2()}signal2?.addEventListener("abort",done2,{once:!0})})},host=realHost2;function busyLoad(cores){return Math.max(4,cores*1.2)}async function tooBusy(){let free=await host.freeBytes().catch(()=>null),load=host.load();return free!==null&&free<MIN_FREE_BYTES?`${(free/1024**3).toFixed(1)} GB of memory free`:load>busyLoad(host.cores())?`load ${load.toFixed(1)} on ${host.cores()} cores`:null}var lockFile3=home2=>path106.join(phonesHome(home2),"lock.json");function tryLock(home2){let file3=lockFile3(home2);fs114.mkdirSync(path106.dirname(file3),{recursive:!0});try{let held4=JSON.parse(fs114.readFileSync(file3,"utf8"));if(held4.pid&&held4.pid!==process.pid&&host.alive(held4.pid))return!1;fs114.rmSync(file3,{force:!0})}catch{fs114.rmSync(file3,{force:!0})}try{return fs114.writeFileSync(file3,JSON.stringify({pid:process.pid,at:new Date().toISOString()}),{flag:"wx"}),!0}catch{return!1}}function unlock(home2){try{JSON.parse(fs114.readFileSync(lockFile3(home2),"utf8")).pid===process.pid&&fs114.rmSync(lockFile3(home2),{force:!0})}catch{}}function heldByOther(home2){try{let held4=JSON.parse(fs114.readFileSync(lockFile3(home2),"utf8"));return held4.pid&&held4.pid!==process.pid&&host.alive(held4.pid)?held4.pid:null}catch{return null}}var usedFile=home2=>path106.join(phonesHome(home2),"used.json");function readUsed(home2){try{return JSON.parse(fs114.readFileSync(usedFile(home2),"utf8"))}catch{return{}}}function markUsed(home2,id2){try{let all=readUsed(home2);all[id2]=new Date().toISOString(),fs114.mkdirSync(path106.dirname(usedFile(home2)),{recursive:!0}),fs114.writeFileSync(usedFile(home2),`${JSON.stringify(all,null,2)}
1499
+ `),fs113.renameSync(tmp,file3)}function ownName(phone,size2){return`squad-kit ${phone.words[size2]}`}function pickModel(runtime,size2){let fits4=runtime.models.filter(m=>m.size===size2);return(size2==="phone"?fits4.find(m=>/\bPro$/.test(m.name)):fits4.find(m=>/^iPad Air\b/.test(m.name)))??fits4[0]??null}function pickRuntime(runtimes,size2,wanted){let able=runtimes.filter(r=>pickModel(r,size2));return wanted?able.find(r=>r.version===wanted||r.id===wanted)??null:able[0]??null}async function ownDevice(phone,size2,o={}){let home2=o.home??squadHome(),[runtimes,devices]=await Promise.all([phone.runtimes(),phone.devices()]),here=new Map(devices.map(d=>[d.id,d])),usable=new Set(runtimes.map(r=>r.id)),saved=savedDevices(home2),wanted=o.runtime?pickRuntime(runtimes,size2,o.runtime):null;if(o.runtime&&!wanted)throw new PhoneError(`This Mac has no iOS ${o.runtime} for the ${phone.words[size2]}.`,`No iOS ${o.runtime} runtime that can make a ${size2} in: ${runtimes.map(r=>r.version).join(", ")||"none"}`);let keep=saved.find(d=>d.size===size2&&here.has(d.id)&&usable.has(d.runtimeId)&&(!wanted||d.runtimeId===wanted.id));if(keep)return{...keep,state:here.get(keep.id).state};let runtime=wanted??pickRuntime(runtimes,size2);if(!runtime)throw new PhoneError(`This Mac has no iOS simulator that can run an ${phone.words[size2]} yet.`,`No runtime with a ${size2} model in: ${runtimes.map(r=>r.version).join(", ")||"none"}`);let model=pickModel(runtime,size2),name=ownName(phone,size2),id2=await phone.create(name,model.id,runtime.id),made={size:size2,id:id2,name,model:model.name,runtime:runtime.version,runtimeId:runtime.id,createdAt:new Date().toISOString()};return saveDevices([...savedDevices(home2).filter(d=>here.has(d.id)&&d.id!==id2),made],home2),{...made,state:"shutdown"}}import{execFile as execFile7}from"child_process";import fs114 from"fs";import os20 from"os";import path106 from"path";var IDLE_MS2=10*6e4,MIN_FREE_BYTES=2*1024**3,BUSY_LOOK_MS=15e3,BUSY_WAIT_MS=10*6e4,LOCK_LOOK_MS=2e3,VIEWER_LOOK_MS=5e3;function busyWords(word2){return`The computer is busy. squad-kit waits a moment before starting the ${word2}.`}function otherWords(word2){return`Another squad-kit on this computer is using the ${word2}. squad-kit waits for its turn.`}function freeMemoryOf(vmStat){let page=Number(/page size of (\d+) bytes/.exec(vmStat)?.[1]??4096),pages=name=>Number(new RegExp(`Pages ${name}:\\s+(\\d+)`).exec(vmStat)?.[1]??NaN),n=pages("free")+pages("inactive")+pages("speculative");return Number.isFinite(n)?n*page:null}var realHost2={freeBytes:()=>new Promise(resolve2=>{if(process.platform!=="darwin")return resolve2(os20.freemem());execFile7("vm_stat",[],{timeout:1e4,encoding:"utf8"},(err2,out20)=>resolve2(err2?os20.freemem():freeMemoryOf(String(out20))??os20.freemem()))}),load:()=>os20.loadavg()[0]??0,cores:()=>os20.cpus().length||4,alive:pid=>{try{return process.kill(pid,0),!0}catch(err2){return err2.code==="EPERM"}},sleep:(ms2,signal2)=>new Promise(resolve2=>{let timer=setTimeout(done2,ms2);function done2(){clearTimeout(timer),signal2?.removeEventListener("abort",done2),resolve2()}signal2?.addEventListener("abort",done2,{once:!0})})},host=realHost2;function busyLoad(cores){return Math.max(4,cores*1.2)}async function tooBusy(){let free=await host.freeBytes().catch(()=>null),load=host.load();return free!==null&&free<MIN_FREE_BYTES?`${(free/1024**3).toFixed(1)} GB of memory free`:load>busyLoad(host.cores())?`load ${load.toFixed(1)} on ${host.cores()} cores`:null}var lockFile3=home2=>path106.join(phonesHome(home2),"lock.json");function tryLock(home2){let file3=lockFile3(home2);fs114.mkdirSync(path106.dirname(file3),{recursive:!0});try{let held4=JSON.parse(fs114.readFileSync(file3,"utf8"));if(held4.pid&&held4.pid!==process.pid&&host.alive(held4.pid))return!1;fs114.rmSync(file3,{force:!0})}catch{fs114.rmSync(file3,{force:!0})}try{return fs114.writeFileSync(file3,JSON.stringify({pid:process.pid,at:new Date().toISOString()}),{flag:"wx"}),!0}catch{return!1}}function unlock(home2){try{JSON.parse(fs114.readFileSync(lockFile3(home2),"utf8")).pid===process.pid&&fs114.rmSync(lockFile3(home2),{force:!0})}catch{}}function heldByOther(home2){try{let held4=JSON.parse(fs114.readFileSync(lockFile3(home2),"utf8"));return held4.pid&&held4.pid!==process.pid&&host.alive(held4.pid)?held4.pid:null}catch{return null}}var usedFile=home2=>path106.join(phonesHome(home2),"used.json");function readUsed(home2){try{return JSON.parse(fs114.readFileSync(usedFile(home2),"utf8"))}catch{return{}}}function markUsed(home2,id2){try{let all=readUsed(home2);all[id2]=new Date().toISOString(),fs114.mkdirSync(path106.dirname(usedFile(home2)),{recursive:!0}),fs114.writeFileSync(usedFile(home2),`${JSON.stringify(all,null,2)}
1500
1500
  `)}catch{}}var HOLD_LOOK_MS=5e3,hold=null;function heldWords(word2,by2){return`squad-kit's ${word2} is open in ${by2}. The pictures wait until you close it.`}function holdPhone(phone,h){let mine2={...h,since:Date.now(),phone};return hold=mine2,()=>{if(hold===mine2){hold=null;for(let d of savedDevices())markUsed(squadHome(),d.id);inTurn2||armIdle(phone)}}}async function endHold(why="replaced"){let h=hold;if(!h)return;h.end(why);let until=Date.now()+3e4;for(;hold===h&&Date.now()<until;)await new Promise(r=>setTimeout(r,100));hold===h&&(hold=null)}async function waitForHold(word2,o){let said=!1,asked=null;for(;hold&&!o.person;){if(o.signal?.aborted)throw new Error(STOPPED4);let h=hold;said||(said=!0,o.say?.(heldWords(word2,h.by))),h.wanted=o.who??"a story",asked!==h&&Date.now()-h.since>h.yieldAfterMs&&(asked=h,o.log?.(`squad-kit's ${word2} was open in ${h.by} for over ${Math.round(h.yieldAfterMs/6e4)} minutes while ${h.wanted} waited: it closes the app there`),h.end("yield")),await host.sleep(HOLD_LOOK_MS,o.signal)}}var HELD=Symbol("held"),keptByOther=new Set,downListeners=[];function onDown(fn){downListeners.push(fn)}var queue=Promise.resolve(),inTurn2=0,idle=null,STOPPED4="You stopped the squad.";function phoneInUse(){return inTurn2>0}async function shutDownGently(phone,id2,log3){if(keptByOther.has(id2))return!1;await phone.shutdown(id2),await host.sleep(VIEWER_LOOK_MS);let now3=(await phone.devices().catch(()=>[])).find(d=>d.id===id2);if(now3&&(now3.state==="booted"||now3.state==="booting"))return keptByOther.add(id2),log3?.(`squad-kit shut down its phone ${id2}, and something else started it again (an open Simulator window): squad-kit leaves it up`),!1;for(let fn of downListeners)fn(id2);return!0}async function lockTurn(home2,word2,o){let said=!1;for(;!tryLock(home2);){if(o.signal?.aborted)throw new Error(STOPPED4);said||(said=!0,o.say?.(otherWords(word2))),await host.sleep(LOCK_LOOK_MS,o.signal)}}async function waitForRoom(word2,o){let why=await tooBusy();if(why){o.say?.(busyWords(word2)),o.log?.(`The computer is busy (${why}), so squad-kit waits before booting its ${word2}`);for(let look=0;why&&look<Math.ceil(BUSY_WAIT_MS/BUSY_LOOK_MS);look+=1){if(o.signal?.aborted)throw new Error(STOPPED4);await host.sleep(BUSY_LOOK_MS,o.signal),why=await tooBusy()}why&&o.log?.(`The computer is still busy (${why}) after ${Math.round(BUSY_WAIT_MS/6e4)} minutes: squad-kit boots its ${word2} anyway`)}}async function withPhone(phone,size2,job,o={}){for(;;){await waitForHold(phone.words[size2],o);let run7=queue.then(()=>turn(phone,size2,job,o));queue=run7.catch(()=>{});let r=await run7;if(r!==HELD)return r}}async function turn(phone,size2,job,o){if(o.signal?.aborted)throw new Error(STOPPED4);if(hold&&!o.person)return HELD;let home2=squadHome(),word2=phone.words[size2];inTurn2+=1,idle&&clearTimeout(idle),idle=null;try{await lockTurn(home2,word2,o);let device=await ownDevice(phone,size2,{...o.runtime?{runtime:o.runtime}:{},home:home2}),states=new Map((await phone.devices()).map(d=>[d.id,d.state]));for(let other of savedDevices(home2))other.id===device.id||states.get(other.id)!=="booted"||(o.log?.(`squad-kit shuts down its ${phone.words[other.size]} ${other.id}, so only one of its phones runs`),await shutDownGently(phone,other.id,o.log).catch(err2=>o.log?.(`Shutting down ${other.id} did not work: ${err2.message}`)));let fresh=!1;if(states.get(device.id)!=="booted"){if(await waitForRoom(word2,o),o.signal?.aborted)throw new Error(STOPPED4);o.say?.(`Starting squad-kit's ${word2}, ${aboutWords(stepSeconds2("boot",home2))}`);let t0=Date.now();await phone.boot(device.id,o.signal?{signal:o.signal}:{}),noteStep("boot",(Date.now()-t0)/1e3,home2),o.log?.(`squad-kit booted its ${word2} ${device.id} (${device.model}, iOS ${device.runtime}) in ${Math.round((Date.now()-t0)/1e3)} s`),fresh=!0}markUsed(home2,device.id);try{return await job({device:{...device},fresh})}finally{markUsed(home2,device.id)}}finally{unlock(home2),inTurn2-=1,inTurn2||armIdle(phone)}}function armIdle(phone){idle&&clearTimeout(idle),idle=setTimeout(()=>{idleNow(phone).catch(()=>{})},IDLE_MS2),idle.unref?.()}function idleNow(phone,now3=Date.now()){let run7=queue.then(async()=>{let home2=squadHome();if(hold||!tryLock(home2))return[];try{let used=readUsed(home2),states=new Map((await phone.devices()).map(d=>[d.id,d.state])),down=[];for(let d of savedDevices(home2)){if(states.get(d.id)!=="booted"){keptByOther.delete(d.id);continue}let last=Date.parse(used[d.id]??"");Number.isFinite(last)&&now3-last<IDLE_MS2||await shutDownGently(phone,d.id)&&down.push(d.id)}return down}finally{unlock(home2)}});return queue=run7.catch(()=>{}),run7}async function stopPhones(phone){let home2=squadHome();if(phone.notHere()||!savedDevices(home2).length)return{stopped:[],left:[],busy:!1};if(inTurn2||heldByOther(home2))return{stopped:[],left:[],busy:!0};if(idle&&clearTimeout(idle),idle=null,!tryLock(home2))return{stopped:[],left:[],busy:!0};try{let states=new Map((await phone.devices()).map(d=>[d.id,d.state])),stopped=[],left2=[];for(let d of savedDevices(home2))states.get(d.id)==="booted"&&(await shutDownGently(phone,d.id)?stopped.push(d.id):left2.push(d.id));return{stopped,left:left2,busy:!1}}finally{unlock(home2)}}function keptUp(id2){return keptByOther.has(id2)}function lastUsed(home2=squadHome()){return readUsed(home2)}var OPEN_SECONDS=5,WATCH_MS=3e3,STOPPED5="You stopped it.",PhoneOpen=class{constructor(s,phone=shotPhone()){this.s=s;this.phone=phone;s.language&&(this.v.language=s.language),this.settled=this.go()}s;phone;v={state:"starting"};ctl=new AbortController;plan=[];stepAt=Date.now();device=null;appId=null;release=null;timer=null;settled;view(){let v={...this.v};if(v.state!=="starting"||!v.step)return v;let at3=this.plan.findIndex(p=>p.step===v.step),now3=this.plan[at3],spent=(Date.now()-this.stepAt)/1e3,left2=Math.round(Math.max(3,(now3?.seconds??0)-spent)+this.plan.slice(at3+1).reduce((n,p)=>n+p.seconds,0));return{...v,secondsLeft:left2,line:`${this.stepWords(v.step)}, ${aboutWords(left2)} left`}}stepWords(step2){let word2=this.phone.words.phone;return{build:`Building the app for the ${word2}`,phone:`Starting squad-kit's ${word2}`,install:`Putting the app on the ${word2}`,open:`Opening the app in the ${word2} window`}[step2]}at(step2){if(this.ctl.signal.aborted)throw new Error(STOPPED5);this.v.step=step2,this.stepAt=Date.now()}async go(){let signal2=this.ctl.signal;try{await endHold("replaced");let built=lastBuild(this.s.dir),fresh=!!(built&&fs115.existsSync(built.app)&&built.tree&&built.tree===await treeOf(this.s.dir)),own2=await ownDevice(this.phone,"phone");this.device=own2,this.plan=[...fresh?[]:[{step:"build",seconds:stepSeconds2("build")}],...own2.state==="booted"?[]:[{step:"phone",seconds:stepSeconds2("boot")}],{step:"install",seconds:stepSeconds2("install")},{step:"open",seconds:OPEN_SECONDS}],this.at(fresh?"install":"build");let{app}=await buildForPhone(this.s.dir,this.s.kind,{signal:signal2,...this.s.log?{log:this.s.log}:{}}),info2=await this.phone.appInfo(app);if(this.appId=info2.appId,this.plan.some(p=>p.step==="phone")&&this.at("phone"),await withPhone(this.phone,"phone",async turn2=>{this.device=turn2.device,this.at("install"),await this.phone.install(turn2.device.id,app,{signal:signal2}),await this.phone.terminate(turn2.device.id,info2.appId),await this.phone.launch(turn2.device.id,info2.appId,this.s.language?{language:this.s.language}:{}),this.at("open"),await this.phone.openWindow(turn2.device.id),this.release=holdPhone(this.phone,{by:this.s.by,yieldAfterMs:this.s.yieldAfterMs,end:why=>{this.endBy(why)}})},{person:!0,signal:signal2,...this.s.log?{log:this.s.log}:{}}),signal2.aborted)throw new Error(STOPPED5);this.v={state:"running",device:this.device.model,since:new Date().toISOString(),...this.s.language?{language:this.s.language}:{}},this.s.log?.(`squad-kit opened ${info2.appId} on its ${this.device.model} ${this.device.id} for ${this.s.by}`),this.watch()}catch(err2){if(signal2.aborted)return;this.release?.(),this.release=null;let plain=err2 instanceof BuildFailed||err2 instanceof PhoneError?err2:null;this.v={state:"failed",error:plain?.message??`The app did not open on the ${this.phone.words.phone}.`,detail:(plain?.detail??err2?.stack??String(err2)).slice(-4e3)}}}watch(){let looking=!1;this.timer=setInterval(()=>{looking||this.v.state!=="running"||!this.device||(looking=!0,this.phone.windowShows(this.device.id).then(shows=>shows?void 0:this.endBy("closed")).catch(()=>{}).finally(()=>looking=!1))},this.s.watchMs??WATCH_MS),this.timer.unref?.()}async endBy(why){if(this.v.state!=="running")return;this.timer&&clearInterval(this.timer),this.timer=null;let word2=this.phone.words.phone,words2=why==="closed"?`You closed the ${word2} window, so squad-kit stopped the app.`:why==="yield"?`squad-kit closed the app on its ${word2}: a story needs the ${word2} for its pictures. Press ${this.s.again} again later.`:`squad-kit closed the app on its ${word2}, as you opened another one there.`;why!=="closed"&&this.device&&this.appId&&await this.phone.terminate(this.device.id,this.appId).catch(()=>{}),this.v={state:"stopped",ended:words2,...this.v.device?{device:this.v.device}:{}},this.release?.(),this.release=null,this.s.onEnd?.(words2)}async stop(){this.ctl.abort(),this.timer&&clearInterval(this.timer),this.timer=null,await this.settled.catch(()=>{});let dev=this.device;dev&&this.appId&&await this.phone.terminate(dev.id,this.appId).catch(()=>{}),this.release?.(),this.release=null,dev&&await shutDownGently(this.phone,dev.id,this.s.log).catch(()=>{}),this.v.state!=="failed"&&(this.v={state:"stopped",...this.v.device?{device:this.v.device}:{}})}};import path109 from"path";import fs117 from"fs";import{spawn as spawn15}from"child_process";import fs116 from"fs";import path107 from"path";var nameOf2=id2=>toolEntry(id2)?.name??id2,cant=why=>`squad-kit can't update it here: ${why}.`,FLUTTER_REPO=/^(?:https:\/\/|ssh:\/\/git@|git@)github\.com[/:]flutter\/flutter(?:\.git)?\/?$/i,CHANNELS=["stable","beta","main","master"],MANAGER_WORDS={homebrew:"Homebrew",fvm:"FVM",snap:"a snap package"};function managedByOf(dir){let parts2=dir.split(/[\\/]+/);if(parts2.includes("Caskroom"))return"homebrew";if(parts2.includes("fvm")||parts2.includes(".fvm"))return"fvm";if(parts2.includes("snap"))return"snap"}async function flutterUpdateLook(h,f){if(f.state!=="found"||!f.where)return;let sdk=f.where,managedBy=managedByOf(sdk),by2=managedBy?{managedBy}:{};if(h.system==="windows")return{...by2,cant:cant("on Windows, you update Flutter yourself for now")};if(managedBy)return{...by2,cant:cant(`${MANAGER_WORDS[managedBy]} looks after this Flutter, and updates it itself`)};if(!h.exists(path107.join(sdk,".git")))return{cant:cant("this Flutter was not installed the usual way, so it cannot update itself")};if(!h.writable(sdk)||!h.writable(path107.join(sdk,".git")))return{cant:cant("your Flutter folder needs your computer's password to change")};let git2=(...args)=>h.run("git",["-C",sdk,...args],{timeoutMs:1e4,env:{GIT_OPTIONAL_LOCKS:"0",GIT_TERMINAL_PROMPT:"0"}}),origin=await git2("remote","get-url","origin");if(!origin.ok||!FLUTTER_REPO.test(origin.out.trim()))return{cant:cant("this Flutter does not come from Flutter's own site")};let head=await git2("symbolic-ref","-q","HEAD"),branch=head.out.trim().replace(/^refs\/heads\//,"");if(!head.ok||!branch)return{cant:cant("your Flutter is set to stay on one version")};if(!CHANNELS.includes(branch))return{cant:cant(`your Flutter follows a branch of its own, ${branch}, not one of Flutter's channels`)};let status=await git2("status","--porcelain");return status.ok?status.out.trim()?{cant:cant("your Flutter folder has changes of its own")}:{}:{cant:cant("it could not look inside your Flutter folder")}}async function addUpdateLooks(h,book){let f=book.tools.flutter;if(f?.state!=="found")return book;let update=await flutterUpdateLook(h,f).catch(()=>({cant:cant("it could not look inside your Flutter folder")}));return update?{...book,tools:{...book.tools,flutter:{...f,update}}}:book}var about=seconds2=>{let m=Math.max(1,Math.round(seconds2/60));return m===1?"about a minute":`about ${m} minutes`};function updateGuess(id2,home2=squadHome()){return measured(`${id2}-update`,home2)??toolEntry(id2)?.update?.seconds??300}function updateWarning(e,system,seconds2){let u=e.update;return`This updates ${e.name} for ${u.reaches} on ${system==="mac"?"this Mac":"this computer"}, not only this one. Older projects may need small changes to build again. It downloads ${u.size} and takes ${about(seconds2)}.`}var UPDATE_ENV={FLUTTER_SUPPRESS_ANALYTICS:"true",CI:"true",NO_COLOR:"1"},UPDATE_LIMIT_MS=30*6e4,DOCTOR_LIMIT_MS=5*6e4,LET_GO_MS=2e3,VERSION_MS=2*6e4,UPDATE_IN=/^Running flutter doctor\b|^Flutter \d+\.\d+\.\d+\S*\s+•\s+channel\b/i,limitWords=ms2=>{let[n,unit]=ms2>=6e4?[Math.round(ms2/6e4),"minute"]:[Math.max(1,Math.round(ms2/1e3)),"second"];return`${n} ${unit}${n===1?"":"s"}`},FIRST_STEP="Looking for the new Flutter",FLUTTER_STEPS=[{test:/^Upgrading Flutter\b|^Downloading\b/i,words:"Downloading the new Flutter"},{test:/^Building flutter tool|^Running pub (?:upgrade|get)|^Resolving dependencies|^Got dependencies|^Running flutter doctor/i,words:"Getting Flutter ready"},{test:/^Doctor summary|^Flutter is already up to date/i,words:"Checking the new Flutter"}];function usefulLine(raw){let l=raw.replace(/\x1b\[[0-9;?]*[A-Za-z]/g,"").trim();return l.length<4||/^Unhandled exception:?$/i.test(l)||/^#\d+\s|^at\s|\((?:package|dart|file):|^(?:package|dart):/.test(l)||/% Total|% Received|^[\d\s.,%:kKMG-]+$/.test(l)||/^[\s─━═│┃┌┐└┘├┤╔╗╚╝•·=*_-]+$/.test(l)?null:l.length>240?`${l.slice(0,239)}\u2026`:l}function endAll(child){if(process.platform!=="win32")return endGroup(child);child.kill("SIGTERM"),setTimeout(()=>{child.exitCode===null&&child.signalCode===null&&child.kill("SIGKILL")},1e4).unref()}var runProgram=(cmd15,args,opts)=>new Promise(resolve2=>{let child;try{child=track(spawn15(cmd15,args,{cwd:opts.cwd,env:opts.env,stdio:["ignore","pipe","pipe"],windowsHide:!0,detached:OWN_GROUP}))}catch(err2){resolve2({code:null,error:err2 instanceof Error?err2.message:String(err2)});return}for(let stream of[child.stdout,child.stderr]){let rest="";stream?.setEncoding("utf8"),stream?.on("data",s=>{let lines3=(rest+s).split(/\r\n|\r|\n/);rest=lines3.pop()??"";for(let l of lines3)opts.onLine(l)}),stream?.on("end",()=>{rest&&opts.onLine(rest),rest=""})}let done2=!1,stop=()=>endAll(child),finish2=r=>{done2||(done2=!0,opts.signal.removeEventListener("abort",stop),resolve2(r))};opts.signal.aborted?stop():opts.signal.addEventListener("abort",stop,{once:!0}),child.on("error",err2=>finish2({code:null,error:err2.message})),whenExited(child,LET_GO_MS).then(code=>{endAll(child),finish2({code})})}),runner3=runProgram;function versionFile(sdk){for(let file3 of[path107.join(sdk,"bin","cache","flutter.version.json"),path107.join(sdk,"version")])try{let v=flutterVersionOf(fs116.readFileSync(file3,"utf8"));if(v)return v}catch{}return null}async function versionSaid(bin,sdk,run7){let lines3=[];return(await run7(bin,["--version","--machine"],{cwd:sdk,env:{...process.env,...UPDATE_ENV},signal:AbortSignal.timeout(VERSION_MS),onLine:l=>lines3.push(l)})).error?null:flutterVersionOf(lines3.join(`
1501
- `))}async function updateTool(id2,sdk,opts={}){let name=nameOf2(id2);if(id2!=="flutter")throw new GetTrouble(`squad-kit can't update ${name} itself. The Tools page shows the steps.`);let system=opts.system??systemOf(),bin=path107.join(sdk,"bin",system==="windows"?"flutter.bat":"flutter"),run7=opts.run??runner3,before=null,version=opts.version??(async(dir,when2)=>{let file3=versionFile(dir);return when2==="before"||file3&&before&&compareVersions(file3,before)>0?file3:await versionSaid(bin,dir,run7).catch(()=>null)??file3}),was=version(sdk,"before").catch(()=>null),stop=new AbortController,limitMs=opts.limitMs??UPDATE_LIMIT_MS,doctorMs=opts.doctorMs??DOCTOR_LIMIT_MS,timer=setTimeout(()=>stop.abort("time"),limitMs),doctor,inAt=0,fromOutside=()=>stop.abort("stopped");opts.signal?.aborted&&fromOutside(),opts.signal?.addEventListener("abort",fromOutside,{once:!0});let started2=Date.now(),last=null,at3=-1;try{let r=await run7(bin,["upgrade"],{cwd:sdk,env:{...process.env,...UPDATE_ENV},signal:stop.signal,onLine:raw=>{let line3=usefulLine(raw);if(!line3)return;last=line3;let i=FLUTTER_STEPS.findIndex(s=>s.test.test(line3));i>at3&&(at3=i,opts.onStep?.(FLUTTER_STEPS[i].words)),!inAt&&UPDATE_IN.test(line3)&&(inAt=Date.now(),doctor=setTimeout(()=>stop.abort("doctor"),doctorMs))}}),said=last?` ${name} said: "${last}"`:"";before=await was;let limit=stop.signal.reason==="time"||stop.signal.reason==="doctor"?stop.signal.reason:null;if(limit){let after=before?await version(sdk,"after").catch(()=>null):null;if(before&&after&&compareVersions(after,before)>0)return writeLog({level:"info",text:`Updating ${name}: ${limit==="doctor"?`its own check took more than ${limitWords(doctorMs)}`:`it took more than ${limitWords(limitMs)}`}, so squad-kit ended it. ${name} went from ${before} to ${after}, so the update is done.`}),{seconds:((inAt||Date.now())-started2)/1e3};throw limit==="time"?new GetTrouble(`The update of ${name} took more than ${limitWords(limitMs)}, so squad-kit stopped it. Check the internet connection, then try again, or do it yourself with the steps.`):new GetTrouble(`The update of ${name} did not finish: its own check took more than ${limitWords(doctorMs)}, so squad-kit stopped it. Try again, or do it yourself with the steps.`)}if(stop.signal.aborted)throw new GetTrouble(`The update of ${name} was stopped before the end. Try again, or do it yourself with the steps.`);if(r.error)throw writeLog({level:"warn",text:`Updating ${name}: could not start ${bin}: ${r.error}`}),new GetTrouble(`squad-kit could not start ${name}'s update. Do it yourself with the steps.`);if(r.code!==0)throw new GetTrouble(`The update of ${name} did not finish.${said} Try again, or do it yourself with the steps.`);return{seconds:(Date.now()-started2)/1e3}}finally{clearTimeout(timer),clearTimeout(doctor),opts.signal?.removeEventListener("abort",fromOutside)}}function startUpdate(id2,sdk,opts={}){let home2=opts.home??squadHome(),name=nameOf2(id2),guess=updateGuess(id2,home2),t0=Date.now(),first2={tool:id2,name,startedAt:new Date(t0).toISOString(),step:"update",done:0,total:guess,secondsLeft:guess,words:FIRST_STEP};return startOne(first2,async set2=>{let g=first2,show3=(words2=g.words)=>{let elapsed=(Date.now()-t0)/1e3;g={...g,...words2?{words:words2}:{},done:Math.min(Math.round(elapsed),Math.round(guess*.95)),secondsLeft:Math.max(10,Math.round(guess-elapsed))},set2(g)},tick2=setInterval(()=>show3(),1e3);tick2.unref(),setTrouble(id2);try{let r=await updateTool(id2,sdk,{...opts.system?{system:opts.system}:{},...opts.run?{run:opts.run}:{},...opts.limitMs?{limitMs:opts.limitMs}:{},...opts.doctorMs?{doctorMs:opts.doctorMs}:{},...opts.version?{version:opts.version}:{},...opts.signal?{signal:opts.signal}:{},onStep:w=>show3(w)});noteTime(`${id2}-update`,r.seconds,home2),writeLog({level:"info",text:`Updated ${name} in ${Math.round(r.seconds)} s, in ${sdk}`})}catch(err2){let words2=err2 instanceof GetTrouble?err2.message:`squad-kit could not update ${name}. Try again, or do it yourself with the steps.`;err2 instanceof GetTrouble?writeLog({level:"warn",text:`Updating ${name}: ${words2}`}):writeLog({level:"warn",text:`Updating ${name}: ${err2 instanceof Error?err2.stack??err2.message:String(err2)}`}),setTrouble(id2,words2)}finally{clearInterval(tick2)}},opts.after)}var host2=realHost;var kept=null;function foundNow(){let file3=foundFile(),mtime=0;try{mtime=fs117.statSync(file3).mtimeMs}catch{return null}return kept?.file===file3&&kept.mtime===mtime||(kept={file:file3,mtime,book:readFound()}),kept.book}var running3=null,lastCheck=0,readyKept=null,READY_MS=6e4;function readyNow(){return(!readyKept||Date.now()-readyKept.at>READY_MS)&&(readyKept={at:Date.now(),value:computerReady()}),readyKept.value}function checkTools(){if(running3)return running3;let asWas=gettingNow()?.step==="update"?foundNow():null;if(asWas)return Promise.resolve(asWas);let home2=squadHome();return readyKept=null,running3=(async()=>{gettingNow()||clearHalfGets(home2);let book=await addUpdateLooks(host2(),await detectAll(host2(),home2));return writeFound(book,home2),kept=null,book})().finally(()=>{running3=null,lastCheck=Date.now()}),running3}var LOOK_AGAIN_MS=10*6e4,LOOKED_AT_MS=6e4;function checkSoon(within3=LOOKED_AT_MS){running3||Date.now()-lastCheck<within3||checkTools().catch(()=>{})}var RANK={required:3,recommended:2,optional:1};function matches(e,places){let needs=e.needs.filter(n=>n.kinds==="any"||places.some(p=>n.kinds.some(k=>p.kinds.includes(k))&&(!n.platform||p.platforms.includes(n.platform))));if(!needs.length)return null;let priority=needs.reduce((a,n)=>RANK[n.priority]>RANK[a]?n.priority:a,"optional"),uniq=xs=>Object.keys(ACTION_WORDS).filter(a=>xs.includes(a));return{priority,actions:uniq(needs.flatMap(n=>n.for)),required:uniq(needs.filter(n=>n.priority==="required").flatMap(n=>n.for))}}function stepsOf(e,system){return[...e.steps?.[system]??e.steps?.all??[]]}function updateStepsOf(e,system,f){let u=e.updateSteps;return u?[...(f?.update?.managedBy?u[f.update.managedBy]:void 0)??u.all??stepsOf(e,system)]:stepsOf(e,system)}function asToday(e,ready){if(e.id==="git"){let version=gitVersion()??void 0;return ready.git.step==="install-git"?{state:"missing",name:e.name}:ready.git.step==="update-git"?{state:"too-old",name:e.name,...version?{version}:{},needs:e.min}:{state:"ready",name:e.name,...version?{version}:{}}}let name=ready.tool.label??e.name;return ready.tool.step==="install-tool"?{state:"missing",name}:ready.tool.step==="sign-in"?{state:"missing",name,note:`${name} is here. Sign in once, and the squad can work.`}:{state:"ready",name}}function linesFor(places,book,system,ready){let platforms=new Set(places.flatMap(p=>p.platforms)),versions2=Object.fromEntries(Object.entries(book?.tools??{}).flatMap(([id2,f])=>f?.state==="found"&&f.version?[[id2,f.version]]:[])),getting=gettingNow(),out20=[];for(let e of CATALOG4){let m=matches(e,places);if(!m||e.asToday&&!ready)continue;let base2={id:e.id,gives:e.gives,priority:m.priority,installer:e.installer,...e.installer==="you"&&e.why?{reason:e.why}:{},steps:stepsOf(e,system),link:e.link,usedFor:m.actions.map(a=>ACTION_WORDS[a]).join(", "),actions:m.actions,command:e.installer==="squad-kit"?`squad tools get ${e.id}`:"squad tools check"},line3;if(e.asToday)line3={...base2,...asToday(e,ready),blocks:[]};else{let f=book?.tools[e.id],old=f?.state==="found"?tooOld(e.id,e.min,versions2,platforms):null,mine2=getting?.tool===e.id,state=mine2?getting.step==="update"?"updating":"getting":!f||f.state==="missing"?"missing":f.state==="not-here"?"not-here":old?"too-old":"ready",blocks2=state==="ready"?[]:state==="too-old"?m.required.filter(a=>!old?.for||old.for.includes(a)):m.required,trouble=lastTrouble(e.id),olderCopy=state==="too-old"||state==="updating",look=e.update&&state==="too-old"?f?.update:void 0,seconds2=look&&!look.cant?updateGuess(e.id):0;line3={...base2,...olderCopy&&e.updateSteps?{steps:updateStepsOf(e,system,f)}:{},name:e.name,state,...f?.version?{version:f.version}:{},...old?{needs:old.needs,...old.why?{why:old.why}:{}}:{},...f?.note||e.note?{note:[f?.note,e.note].filter(Boolean).join(" ")}:{},...state==="not-here"&&e.notHere?{notHere:e.notHere}:{},blocks:blocks2,...mine2?{getting}:{},...trouble&&state!=="ready"?{trouble}:{},...look&&!look.cant?{update:{warning:updateWarning(e,system,seconds2),seconds:seconds2,command:`squad tools update ${e.id}`}}:{},...look?.cant?{cantUpdate:look.cant}:{}}}out20.push(line3)}return out20}var list2=xs=>xs.length<=1?xs[0]??"":`${xs.slice(0,-1).join(", ")} and ${xs[xs.length-1]}`;function projectWords(places){let parts2=[...new Set(places.flatMap(p=>p.kinds.map(k=>{let phones=k==="flutter"||k==="react-native"?p.platforms.map(x=>PLATFORM_WORDS[x]):[];return`${KIND_WORDS[k]}${phones.length?` for ${list2(phones)}`:""}`})))];return parts2.length?`This project has ${list2(parts2)}.`:"squad-kit found no app here that needs more than every project needs."}function metaOf(bp){try{return boardExists(bp)?readMeta(bp):null}catch{return null}}async function projectTools(bp){let places=projectKinds(bp.root,metaOf(bp)),book=foundNow()??await checkTools(),system=book.system??systemOf();return{system,checkedAt:book.at??null,checking:!!running3,summary:projectWords(places),places,lines:linesFor(places,book,system,readyNow()),getting:gettingNow()}}var TOOLS_CHECKING="Checking this computer's tools for this story",GOES_ON=n=>`Goes on by itself once ${n===1?"it's":"they're"} there`,CANT_HERE="It can't go on on this computer";function waitsForTools(status){return status?status===TOOLS_CHECKING||status.startsWith("Waits for ")&&(/\. Goes on by itself once (?:it's|they're) there$/.test(status)||status.endsWith(`. ${CANT_HERE}`)):!1}function leftWords(seconds2){return seconds2<90?`about ${Math.max(5,Math.round(seconds2/5)*5)} seconds left`:`about ${Math.round(seconds2/60)} minutes left`}function gettingBlock(action,lines3){let g=gettingNow();if(!g||g.step==="update"||!lines3.length||!lines3.every(l=>(l.state==="getting"||l.state==="missing")&&l.installer==="squad-kit")||!lines3.some(l=>l.id===g.tool))return null;let names2=list2(lines3.map(l=>l.name)),left2=`${g.step==="download"&&g.total?`${Math.min(99,Math.floor(g.done/g.total*100))}%, `:g.step==="unpack"?"unpacking, ":""}${leftWords(g.secondsLeft)}`;return{action,tools:lines3.map(l=>({id:l.id,name:l.name,state:l.state})),text:`squad-kit is getting ${names2} now (${left2}): it needs ${lines3.length===1?"it":"them"} to ${ACTION_DOING[action]}.`,status:`Waits for ${names2}, which squad-kit is getting now (${left2}). ${GOES_ON(lines3.length)}`,link:`/tools#${lines3[0].id}`}}function blockOf(action,lines3){let getting=gettingBlock(action,lines3);if(getting)return getting;let absent=lines3.filter(l=>l.state==="missing"||l.state==="getting"),old=lines3.filter(l=>l.state==="too-old"),updating=lines3.filter(l=>l.state==="updating"),away=lines3.filter(l=>l.state==="not-here"),reasons=[...absent.length?[`${list2(absent.map(l=>l.name))} ${absent.length===1?"isn't":"aren't"} on this computer`]:[],...old.map(l=>`${l.name}${l.version?` ${l.version}`:""} is too old: it needs ${l.needs} or newer`),...updating.map(l=>`squad-kit is updating ${l.name} now`),...away.map(l=>`${l.name} ${toolEntry(l.id)?.notHere??"can't run on this computer"}`)],onlyAway=away.length===lines3.length,text3=`squad-kit can't ${ACTION_DOING[action]} ${onlyAway?"on this computer":"yet"}: ${reasons.join("; ")}.`,what=list2([...absent.map(l=>l.name),...[...old,...updating].map(l=>`a newer ${l.name}`),...away.map(l=>l.name)]),status=onlyAway?`Waits for ${what}, which ${away.length===1?toolEntry(away[0].id)?.notHere??"needs another computer":"need another computer"}. ${CANT_HERE}`:`Waits for ${what} on this computer. ${GOES_ON(lines3.length)}`;return{action,tools:lines3.map(l=>({id:l.id,name:l.name,state:l.state,...l.version?{version:l.version}:{},...l.needs?{needs:l.needs}:{}})),text:text3,status,link:`/tools#${lines3[0].id}`}}var kindsKept=new Map,KINDS_MS=15e3;function placesFor(bp,meta,story2){let key=JSON.stringify([bp.root,story2?.worktree??null,story2?.parts??null,story2?.repo??null,story2?.copies?.map(c=>c.dir)??null]),k=kindsKept.get(key);if(k&&Date.now()-k.at<KINDS_MS)return k.places;let m=meta(),places=projectKinds(bp.root,m,story2);if(!places.length&&m?.scan?.empty){let k2=kindsOfCommands(m.commands??{});k2.kinds.length&&places.push({place:".",...k2})}return kindsKept.size>200&&kindsKept.clear(),kindsKept.set(key,{at:Date.now(),places}),places}function blockedBy(bp,action,story2){let places=placesFor(bp,()=>metaOf(bp),story2);if(!CATALOG4.filter(e=>!e.asToday&&matches(e,places)?.required.includes(action)).length)return null;let book=foundNow();if(!book)return checkSoon(0),{action,tools:[],text:`squad-kit is checking this computer's tools before it can ${ACTION_DOING[action]}.`,status:TOOLS_CHECKING,link:"/tools",checking:!0};let lines3=linesFor(places,book,book.system).filter(l=>l.blocks.includes(action));return lines3.length?(checkSoon(LOOK_AGAIN_MS),blockOf(action,lines3)):null}var ACTION_OF={build:"build",unstick:"build",test:"checks"};function toolsHold(bp,t,phase){let action=ACTION_OF[phase];return action?blockedBy(bp,action,t):null}function toolWaitOf(bp,t,lookedAt2=!1){if(!waitsForTools(t.status))return{};lookedAt2&&checkSoon();let action=t.squad?.phase==="proof"?"pictures":ACTION_OF[t.squad?.phase??""]??"build",b=blockedBy(bp,action,t);return b?{toolWait:b}:{}}var fits2=(e,book)=>{let f=e?book?.tools[e.id]:void 0;return!!(e&&f?.state==="found"&&(!e.min||!f.version||atLeast2(f.version,e.min)))};function getToolNow(id2,opts={}){let e=toolEntry(id2);if(!e)throw new Error(`squad-kit knows no tool called "${id2}". See them all: squad tools`);if(e.installer!=="squad-kit")throw new Error(`squad-kit can't get ${e.name} itself: ${e.why??"you install it"}. The Tools page shows the steps.`);let book=foundNow();if(fits2(e,book))throw new Error(`${e.name}${book?.tools[e.id]?.version?` ${book.tools[e.id].version}`:""} is here already, so there is nothing to get.`);let also=(e.with??[]).filter(w=>!fits2(toolEntry(w),book));return startGets([e.id,...also],{...opts.source?{source:opts.source}:{},after:()=>{checkTools().catch(()=>{})}})}async function updateToolNow(bp,id2,opts={}){let e=toolEntry(id2);if(!e)throw new Error(`squad-kit knows no tool called "${id2}". See them all: squad tools`);if(!e.update)throw new Error(`squad-kit can't update ${e.name} itself. The Tools page shows the steps.`);let l=(await projectTools(bp)).lines.find(x=>x.id===e.id);if(!l)throw new Error(`This project doesn't use ${e.name}, so squad-kit leaves it as it is.`);if(l.state==="updating")throw new Error(`squad-kit is updating ${e.name} now.`);if(l.state==="ready")throw new Error(`${e.name}${l.version?` ${l.version}`:""} is new enough already, so there is nothing to update.`);if(l.state!=="too-old")throw new Error(`${e.name} isn't on this computer, so there is nothing to update. The Tools page shows how to install it.`);if(l.cantUpdate)throw new Error(`${l.cantUpdate} The Tools page shows the steps.`);if(!l.update)throw new Error(`squad-kit has not looked at your ${e.name} folder yet. Press Check again, then try again.`);if(!opts.ok)throw new Error(`${l.update.warning} squad-kit updates it only after you say OK.`);let f=foundNow()?.tools[e.id],look=f?await flutterUpdateLook(host2(),f):void 0;if(!f?.where||!look||look.cant)throw new Error(`${look?.cant??"squad-kit can't update it here: it could not look inside its folder."} The Tools page shows the steps.`);return startUpdate(e.id,f.where,{after:()=>{checkTools().catch(()=>{})}})}import fs118 from"fs";import path108 from"path";var list3=dir=>{try{return fs118.readdirSync(dir)}catch{return[]}};function arbLanguage(file3){let m=/(?:^|_)([a-z]{2,3})(?:[_-](?:[A-Z]{2}|[A-Z][a-z]{3}))?\.arb$/.exec(file3);return m?m[1]:null}function arbDir(dir){try{let yaml21=fs118.readFileSync(path108.join(dir,"l10n.yaml"),"utf8"),named=/^arb-dir:\s*["']?([^"'\n#]+?)["']?\s*$/m.exec(yaml21)?.[1];if(named)return path108.join(dir,named)}catch{}return path108.join(dir,"lib","l10n")}function lprojLanguages(dir,depth=3){let out20=[];for(let name of list3(dir)){let m=/^([a-z]{2,3})(?:[-_][A-Za-z]{2,4})?\.lproj$/.exec(name);m?out20.push(m[1]):depth>0&&!name.startsWith(".")&&!/^(node_modules|Pods|build|DerivedData|Carthage)$/.test(name)&&isDir3(path108.join(dir,name))&&out20.push(...lprojLanguages(path108.join(dir,name),depth-1))}return out20}function isDir3(p){try{return fs118.statSync(p).isDirectory()}catch{return!1}}function appLanguages(dir,kind){if(kind==="react-native")return[];let found2=kind==="flutter"?list3(arbDir(dir)).map(arbLanguage).filter(l=>!!l):lprojLanguages(dir),all=[...new Set(found2)].sort((a,b)=>a==="en"?-1:b==="en"?1:a.localeCompare(b));return all.length>1?all:[]}var WEB_VERSION="Web version";function metaOf2(bp){try{return boardExists(bp)?readMeta(bp):null}catch{return null}}function iphoneApp(bp,story2){for(let p of placesOf(bp.root,metaOf2(bp),story2)){let k=kindsOf(p.dir);if(k.kinds.includes("flutter")&&k.platforms.includes("ios"))return{dir:p.dir,kind:"flutter"};if(k.kinds.includes("react-native")&&k.platforms.includes("ios"))return{dir:p.dir,kind:"react-native"};if(k.kinds.includes("ios"))return{dir:p.dir,kind:"ios"}}return null}function tapTools(){let own2=ownTool("maestro"),found2=readFound()?.tools.maestro,maestro2=own2?.binary??(found2?.state==="found"&&found2.where?found2.where:null);if(!maestro2)return null;let java2=ownTool("java"),javaHome=java2?path109.dirname(path109.dirname(java2.binary)):void 0;return{maestro:maestro2,...javaHome?{javaHome}:{}}}var list4=xs=>xs.length<=1?xs[0]??"":`${xs.slice(0,-1).join(", ")} and ${xs[xs.length-1]}`;function webLine(block3){let word2=shotPhone().words.phone,away=block3.tools.filter(x=>x.state==="not-here"),xcode2=block3.tools.some(x=>x.id==="xcode"&&x.state==="missing"),tools4=block3.tools.filter(x=>!(xcode2&&x.id==="simulator")),needs=away.length?"a Mac":list4(tools4.map(x=>x.state==="too-old"||x.state==="updating"?`a newer ${x.name}`:x.name));return`${word2} pictures need ${needs||"more tools"}. squad-kit took pictures of the web version meanwhile.`}var source;function getOwnTools(block3){if(process.env.SQUAD_KIT_NO_AUTO_GET==="1"&&!source||block3.checking||!block3.tools.length||!block3.tools.every(x=>toolEntry(x.id)?.installer==="squad-kit"&&(x.state==="missing"||x.state==="getting")))return!1;let now3=gettingNow();if(now3)return block3.tools.some(x=>x.id===now3.tool);if(block3.tools.some(x=>lastTrouble(x.id)))return!1;let first2=block3.tools.find(x=>x.id==="maestro")??block3.tools[0];try{return getToolNow(first2.id,source?{source}:{}),!0}catch{return!!gettingNow()}}async function untilGot(signal2,ms2=20*6e4){let until=Date.now()+ms2;for(;gettingNow()&&Date.now()<until&&!signal2?.aborted;)await new Promise(r=>setTimeout(r,1e3))}async function picturesWay(bp,story2,web){let app=iphoneApp(bp,story2);if(!app)return{way:"browser"};let block3=blockedBy(bp,"pictures",story2);if(block3?.checking&&(await checkTools().catch(()=>{}),block3=blockedBy(bp,"pictures",story2)),block3&&getOwnTools(block3))return{way:"wait",block:blockedBy(bp,"pictures",story2)??block3,getting:!0};let tools4=block3?null:tapTools();if(!block3&&tools4&&!shotPhone().notHere())return{way:"phone",...app,tools:tools4};let why=block3??{action:"pictures",tools:[{id:"maestro",name:"Maestro",state:"missing"}],text:"squad-kit can't take the phone pictures yet: Maestro isn't on this computer.",status:"Waits for Maestro on this computer. Goes on by itself once it's there",link:"/tools#maestro"};return web?{way:"web",line:webLine(why),link:why.link}:{way:"wait",block:why}}function picturesHold(bp,story2,web){if(!iphoneApp(bp,story2))return null;let block3=blockedBy(bp,"pictures",story2);return block3?getOwnTools(block3)?blockedBy(bp,"pictures",story2)??block3:web?null:block3:null}function phoneStartPlan(bp,action,story2){let app=iphoneApp(bp,story2);if(!app)return null;let languages=appLanguages(app.dir,app.kind),notHere=shotPhone().notHere();if(notHere)return{...app,ready:!1,why:notHere,link:"/tools#xcode",languages};let block3=blockedBy(bp,action,story2);return{...app,ready:!block3,...block3?{why:block3.text,link:block3.link}:{},languages}}function phoneAppView(plan,web,open3,on=!1){return{ready:plan.ready,...plan.why?{why:plan.why}:{},...plan.link?{link:plan.link}:{},languages:plan.languages,web,...on?{on:!0}:{},...open3?{run:open3.view()}:{}}}function languageProblem(language,languages){return language===void 0?null:/^[a-z]{2,3}$/.test(language)?languages.length&&!languages.includes(language)?`This app comes in ${languages.join(", ")}. Pick one of them.`:null:`"${language.slice(0,20)}" is not a language: use two letters, like "ar" or "en".`}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 path110.join(bp.localDir,"apps",app.name.replace(/[^\w.-]+/g,"-")||"app")}async function updateCopy(root,repo,dir,how2={}){let src=path110.join(root,repo.path),commit=latestBase(src,repo.base,repo.remote||void 0).sha;return copyTurn(dir,async()=>{if(await clearHalfCopy(src,dir,how2.signal),copyIsWhole(dir))try{return await gitLater(dir,["checkout","--force","--detach",commit],how2.signal?{signal:how2.signal}:{}),{commit,fresh:!1}}catch(err2){if(err2 instanceof GitStopped)throw err2}return await makeCopy(src,dir,["--force","--detach"],commit,how2),{commit,fresh:!0}})}var ENV_FILE2=/^\.env(\.[\w.-]+)?$/,ENV_EXAMPLE=/\.(example|sample|template|dist)$/;async function localEnvFiles(src){let found2=new Set,list6=args=>gitRecords(src,["ls-files","-z","--others","--exclude-standard",...args],"\0",f=>{let name=path110.basename(f);!f.endsWith("/")&&ENV_FILE2.test(name)&&!ENV_EXAMPLE.test(name)&&found2.add(f)}).catch(()=>{});return await list6([]),await list6(["--ignored","--directory"]),[...found2]}async function copyEnvFiles(src,dir,named=[]){let files=[...new Set([...await localEnvFiles(src),...named.filter(f=>fs119.existsSync(path110.join(src,f)))])];for(let f of files){let to=path110.join(dir,f);fs119.mkdirSync(path110.dirname(to),{recursive:!0}),fs119.copyFileSync(path110.join(src,f),to),fs119.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=path110.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],`
1501
+ `))}async function updateTool(id2,sdk,opts={}){let name=nameOf2(id2);if(id2!=="flutter")throw new GetTrouble(`squad-kit can't update ${name} itself. The Tools page shows the steps.`);let system=opts.system??systemOf(),bin=path107.join(sdk,"bin",system==="windows"?"flutter.bat":"flutter"),run7=opts.run??runner3,before=null,version=opts.version??(async(dir,when2)=>{let file3=versionFile(dir);return when2==="before"||file3&&before&&compareVersions(file3,before)>0?file3:await versionSaid(bin,dir,run7).catch(()=>null)??file3}),was=version(sdk,"before").catch(()=>null),stop=new AbortController,limitMs=opts.limitMs??UPDATE_LIMIT_MS,doctorMs=opts.doctorMs??DOCTOR_LIMIT_MS,timer=setTimeout(()=>stop.abort("time"),limitMs),doctor,inAt=0,fromOutside=()=>stop.abort("stopped");opts.signal?.aborted&&fromOutside(),opts.signal?.addEventListener("abort",fromOutside,{once:!0});let started2=Date.now(),last=null,at3=-1;try{let r=await run7(bin,["upgrade"],{cwd:sdk,env:{...process.env,...UPDATE_ENV},signal:stop.signal,onLine:raw=>{let line3=usefulLine(raw);if(!line3)return;last=line3;let i=FLUTTER_STEPS.findIndex(s=>s.test.test(line3));i>at3&&(at3=i,opts.onStep?.(FLUTTER_STEPS[i].words)),!inAt&&UPDATE_IN.test(line3)&&(inAt=Date.now(),doctor=setTimeout(()=>stop.abort("doctor"),doctorMs))}}),said=last?` ${name} said: "${last}"`:"";before=await was;let limit=stop.signal.reason==="time"||stop.signal.reason==="doctor"?stop.signal.reason:null;if(limit){let after=before?await version(sdk,"after").catch(()=>null):null;if(before&&after&&compareVersions(after,before)>0)return writeLog({level:"info",text:`Updating ${name}: ${limit==="doctor"?`its own check took more than ${limitWords(doctorMs)}`:`it took more than ${limitWords(limitMs)}`}, so squad-kit ended it. ${name} went from ${before} to ${after}, so the update is done.`}),{seconds:((inAt||Date.now())-started2)/1e3};throw limit==="time"?new GetTrouble(`The update of ${name} took more than ${limitWords(limitMs)}, so squad-kit stopped it. Check the internet connection, then try again, or do it yourself with the steps.`):new GetTrouble(`The update of ${name} did not finish: its own check took more than ${limitWords(doctorMs)}, so squad-kit stopped it. Try again, or do it yourself with the steps.`)}if(stop.signal.aborted)throw new GetTrouble(`The update of ${name} was stopped before the end. Try again, or do it yourself with the steps.`);if(r.error)throw writeLog({level:"warn",text:`Updating ${name}: could not start ${bin}: ${r.error}`}),new GetTrouble(`squad-kit could not start ${name}'s update. Do it yourself with the steps.`);if(r.code!==0)throw new GetTrouble(`The update of ${name} did not finish.${said} Try again, or do it yourself with the steps.`);return{seconds:(Date.now()-started2)/1e3}}finally{clearTimeout(timer),clearTimeout(doctor),opts.signal?.removeEventListener("abort",fromOutside)}}function startUpdate(id2,sdk,opts={}){let home2=opts.home??squadHome(),name=nameOf2(id2),guess=updateGuess(id2,home2),t0=Date.now(),first2={tool:id2,name,startedAt:new Date(t0).toISOString(),step:"update",done:0,total:guess,secondsLeft:guess,words:FIRST_STEP};return startOne(first2,async set2=>{let g=first2,show3=(words2=g.words)=>{let elapsed=(Date.now()-t0)/1e3;g={...g,...words2?{words:words2}:{},done:Math.min(Math.round(elapsed),Math.round(guess*.95)),secondsLeft:Math.max(10,Math.round(guess-elapsed))},set2(g)},tick2=setInterval(()=>show3(),1e3);tick2.unref(),setTrouble(id2);try{let r=await updateTool(id2,sdk,{...opts.system?{system:opts.system}:{},...opts.run?{run:opts.run}:{},...opts.limitMs?{limitMs:opts.limitMs}:{},...opts.doctorMs?{doctorMs:opts.doctorMs}:{},...opts.version?{version:opts.version}:{},...opts.signal?{signal:opts.signal}:{},onStep:w=>show3(w)});noteTime(`${id2}-update`,r.seconds,home2),writeLog({level:"info",text:`Updated ${name} in ${Math.round(r.seconds)} s, in ${sdk}`})}catch(err2){let words2=err2 instanceof GetTrouble?err2.message:`squad-kit could not update ${name}. Try again, or do it yourself with the steps.`;err2 instanceof GetTrouble?writeLog({level:"warn",text:`Updating ${name}: ${words2}`}):writeLog({level:"warn",text:`Updating ${name}: ${err2 instanceof Error?err2.stack??err2.message:String(err2)}`}),setTrouble(id2,words2)}finally{clearInterval(tick2)}},opts.after)}var host2=realHost;var kept=null;function foundNow(){let file3=foundFile(),mtime=0;try{mtime=fs117.statSync(file3).mtimeMs}catch{return null}return kept?.file===file3&&kept.mtime===mtime||(kept={file:file3,mtime,book:readFound()}),kept.book}var running3=null,lastCheck=0,readyKept=null,READY_MS=6e4;function readyNow(){return(!readyKept||Date.now()-readyKept.at>READY_MS)&&(readyKept={at:Date.now(),value:computerReady()}),readyKept.value}function checkTools(){if(running3)return running3;let asWas=gettingNow()?.step==="update"?foundNow():null;if(asWas)return Promise.resolve(asWas);let home2=squadHome();return readyKept=null,running3=(async()=>{gettingNow()||clearHalfGets(home2);let book=await addUpdateLooks(host2(),await detectAll(host2(),home2));return writeFound(book,home2),kept=null,book})().finally(()=>{running3=null,lastCheck=Date.now()}),running3}var LOOK_AGAIN_MS=10*6e4,LOOKED_AT_MS=6e4;function checkSoon(within3=LOOKED_AT_MS){running3||Date.now()-lastCheck<within3||checkTools().catch(()=>{})}var RANK={required:3,recommended:2,optional:1};function matches(e,places){let needs=e.needs.filter(n=>n.kinds==="any"||places.some(p=>n.kinds.some(k=>p.kinds.includes(k))&&(!n.platform||p.platforms.includes(n.platform))));if(!needs.length)return null;let priority=needs.reduce((a,n)=>RANK[n.priority]>RANK[a]?n.priority:a,"optional"),uniq=xs=>Object.keys(ACTION_WORDS).filter(a=>xs.includes(a));return{priority,actions:uniq(needs.flatMap(n=>n.for)),required:uniq(needs.filter(n=>n.priority==="required").flatMap(n=>n.for))}}function stepsOf(e,system){return[...e.steps?.[system]??e.steps?.all??[]]}function updateStepsOf(e,system,f){let u=e.updateSteps;if(!u)return stepsOf(e,system);let by2=f?.update?.managedBy??f?.managedBy;return[...(by2?u[by2]:void 0)??u.all??stepsOf(e,system)]}function asToday(e,ready){if(e.id==="git"){let version=gitVersion()??void 0;return ready.git.step==="install-git"?{state:"missing",name:e.name}:ready.git.step==="update-git"?{state:"too-old",name:e.name,...version?{version}:{},needs:e.min}:{state:"ready",name:e.name,...version?{version}:{}}}let name=ready.tool.label??e.name;return ready.tool.step==="install-tool"?{state:"missing",name}:ready.tool.step==="sign-in"?{state:"missing",name,note:`${name} is here. Sign in once, and the squad can work.`}:{state:"ready",name}}function linesFor(places,book,system,ready){let platforms=new Set(places.flatMap(p=>p.platforms)),versions2=Object.fromEntries(Object.entries(book?.tools??{}).flatMap(([id2,f])=>f?.state==="found"&&f.version?[[id2,f.version]]:[])),getting=gettingNow(),out20=[];for(let e of CATALOG4){let m=matches(e,places);if(!m||e.asToday&&!ready)continue;let base2={id:e.id,gives:e.gives,priority:m.priority,installer:e.installer,...e.installer==="you"&&e.why?{reason:e.why}:{},steps:stepsOf(e,system),link:e.link,usedFor:m.actions.map(a=>ACTION_WORDS[a]).join(", "),actions:m.actions,command:e.installer==="squad-kit"?`squad tools get ${e.id}`:"squad tools check"},line3;if(e.asToday)line3={...base2,...asToday(e,ready),blocks:[]};else{let f=book?.tools[e.id],old=f?.state==="found"?tooOld(e.id,e.min,versions2,platforms):null,mine2=getting?.tool===e.id,state=mine2?getting.step==="update"?"updating":"getting":!f||f.state==="missing"?"missing":f.state==="not-here"?"not-here":old?"too-old":"ready",blocks2=state==="ready"?[]:state==="too-old"?m.required.filter(a=>!old?.for||old.for.includes(a)):m.required,trouble=lastTrouble(e.id),olderCopy=state==="too-old"||state==="updating",later=state==="ready"?updateRecommended(f?.version,e.recommended):null,look=e.update&&state==="too-old"?f?.update:void 0,seconds2=look&&!look.cant?updateGuess(e.id):0;line3={...base2,...(olderCopy||later)&&e.updateSteps?{steps:updateStepsOf(e,system,f)}:{},name:e.name,state,...f?.version?{version:f.version}:{},...old?{needs:old.needs,...old.why?{why:old.why}:{}}:{},...later?{recommended:later}:{},...f?.note||e.note||later?{note:[later?`An update to ${later} or newer is recommended.`:"",f?.note,e.note].filter(Boolean).join(" ")}:{},...state==="not-here"&&e.notHere?{notHere:e.notHere}:{},blocks:blocks2,...mine2?{getting}:{},...trouble&&state!=="ready"?{trouble}:{},...look&&!look.cant?{update:{warning:updateWarning(e,system,seconds2),seconds:seconds2,command:`squad tools update ${e.id}`}}:{},...look?.cant?{cantUpdate:look.cant}:{}}}out20.push(line3)}return out20}var list2=xs=>xs.length<=1?xs[0]??"":`${xs.slice(0,-1).join(", ")} and ${xs[xs.length-1]}`;function projectWords(places){let parts2=[...new Set(places.flatMap(p=>p.kinds.map(k=>{let phones=k==="flutter"||k==="react-native"?p.platforms.map(x=>PLATFORM_WORDS[x]):[];return`${KIND_WORDS[k]}${phones.length?` for ${list2(phones)}`:""}`})))];return parts2.length?`This project has ${list2(parts2)}.`:"squad-kit found no app here that needs more than every project needs."}function metaOf(bp){try{return boardExists(bp)?readMeta(bp):null}catch{return null}}async function projectTools(bp){let places=projectKinds(bp.root,metaOf(bp)),book=foundNow()??await checkTools(),system=book.system??systemOf();return{system,checkedAt:book.at??null,checking:!!running3,summary:projectWords(places),places,lines:linesFor(places,book,system,readyNow()),getting:gettingNow()}}var TOOLS_CHECKING="Checking this computer's tools for this story",GOES_ON=n=>`Goes on by itself once ${n===1?"it's":"they're"} there`,CANT_HERE="It can't go on on this computer";function waitsForTools(status){return status?status===TOOLS_CHECKING||status.startsWith("Waits for ")&&(/\. Goes on by itself once (?:it's|they're) there$/.test(status)||status.endsWith(`. ${CANT_HERE}`)):!1}function leftWords(seconds2){return seconds2<90?`about ${Math.max(5,Math.round(seconds2/5)*5)} seconds left`:`about ${Math.round(seconds2/60)} minutes left`}function gettingBlock(action,lines3){let g=gettingNow();if(!g||g.step==="update"||!lines3.length||!lines3.every(l=>(l.state==="getting"||l.state==="missing")&&l.installer==="squad-kit")||!lines3.some(l=>l.id===g.tool))return null;let names2=list2(lines3.map(l=>l.name)),left2=`${g.step==="download"&&g.total?`${Math.min(99,Math.floor(g.done/g.total*100))}%, `:g.step==="unpack"?"unpacking, ":""}${leftWords(g.secondsLeft)}`;return{action,tools:lines3.map(l=>({id:l.id,name:l.name,state:l.state})),text:`squad-kit is getting ${names2} now (${left2}): it needs ${lines3.length===1?"it":"them"} to ${ACTION_DOING[action]}.`,status:`Waits for ${names2}, which squad-kit is getting now (${left2}). ${GOES_ON(lines3.length)}`,link:`/tools#${lines3[0].id}`}}function blockOf(action,lines3){let getting=gettingBlock(action,lines3);if(getting)return getting;let absent=lines3.filter(l=>l.state==="missing"||l.state==="getting"),old=lines3.filter(l=>l.state==="too-old"),updating=lines3.filter(l=>l.state==="updating"),away=lines3.filter(l=>l.state==="not-here"),reasons=[...absent.length?[`${list2(absent.map(l=>l.name))} ${absent.length===1?"isn't":"aren't"} on this computer`]:[],...old.map(l=>`${l.name}${l.version?` ${l.version}`:""} is too old: it needs ${l.needs} or newer`),...updating.map(l=>`squad-kit is updating ${l.name} now`),...away.map(l=>`${l.name} ${toolEntry(l.id)?.notHere??"can't run on this computer"}`)],onlyAway=away.length===lines3.length,text3=`squad-kit can't ${ACTION_DOING[action]} ${onlyAway?"on this computer":"yet"}: ${reasons.join("; ")}.`,what=list2([...absent.map(l=>l.name),...[...old,...updating].map(l=>`a newer ${l.name}`),...away.map(l=>l.name)]),status=onlyAway?`Waits for ${what}, which ${away.length===1?toolEntry(away[0].id)?.notHere??"needs another computer":"need another computer"}. ${CANT_HERE}`:`Waits for ${what} on this computer. ${GOES_ON(lines3.length)}`;return{action,tools:lines3.map(l=>({id:l.id,name:l.name,state:l.state,...l.version?{version:l.version}:{},...l.needs?{needs:l.needs}:{}})),text:text3,status,link:`/tools#${lines3[0].id}`}}var kindsKept=new Map,KINDS_MS=15e3;function placesFor(bp,meta,story2){let key=JSON.stringify([bp.root,story2?.worktree??null,story2?.parts??null,story2?.repo??null,story2?.copies?.map(c=>c.dir)??null]),k=kindsKept.get(key);if(k&&Date.now()-k.at<KINDS_MS)return k.places;let m=meta(),places=projectKinds(bp.root,m,story2);if(!places.length&&m?.scan?.empty){let k2=kindsOfCommands(m.commands??{});k2.kinds.length&&places.push({place:".",...k2})}return kindsKept.size>200&&kindsKept.clear(),kindsKept.set(key,{at:Date.now(),places}),places}function blockedBy(bp,action,story2){let places=placesFor(bp,()=>metaOf(bp),story2);if(!CATALOG4.filter(e=>!e.asToday&&matches(e,places)?.required.includes(action)).length)return null;let book=foundNow();if(!book)return checkSoon(0),{action,tools:[],text:`squad-kit is checking this computer's tools before it can ${ACTION_DOING[action]}.`,status:TOOLS_CHECKING,link:"/tools",checking:!0};let lines3=linesFor(places,book,book.system).filter(l=>l.blocks.includes(action));return lines3.length?(checkSoon(LOOK_AGAIN_MS),blockOf(action,lines3)):null}var ACTION_OF={build:"build",unstick:"build",test:"checks"};function toolsHold(bp,t,phase){let action=ACTION_OF[phase];return action?blockedBy(bp,action,t):null}function toolWaitOf(bp,t,lookedAt2=!1){if(!waitsForTools(t.status))return{};lookedAt2&&checkSoon();let action=t.squad?.phase==="proof"?"pictures":ACTION_OF[t.squad?.phase??""]??"build",b=blockedBy(bp,action,t);return b?{toolWait:b}:{}}var fits2=(e,book)=>{let f=e?book?.tools[e.id]:void 0;return!!(e&&f?.state==="found"&&(!e.min||!f.version||atLeast2(f.version,e.min)))};function getToolNow(id2,opts={}){let e=toolEntry(id2);if(!e)throw new Error(`squad-kit knows no tool called "${id2}". See them all: squad tools`);if(e.installer!=="squad-kit")throw new Error(`squad-kit can't get ${e.name} itself: ${e.why??"you install it"}. The Tools page shows the steps.`);let book=foundNow();if(fits2(e,book))throw new Error(`${e.name}${book?.tools[e.id]?.version?` ${book.tools[e.id].version}`:""} is here already, so there is nothing to get.`);let also=(e.with??[]).filter(w=>!fits2(toolEntry(w),book));return startGets([e.id,...also],{...opts.source?{source:opts.source}:{},after:()=>{checkTools().catch(()=>{})}})}async function updateToolNow(bp,id2,opts={}){let e=toolEntry(id2);if(!e)throw new Error(`squad-kit knows no tool called "${id2}". See them all: squad tools`);if(!e.update)throw new Error(`squad-kit can't update ${e.name} itself. The Tools page shows the steps.`);let l=(await projectTools(bp)).lines.find(x=>x.id===e.id);if(!l)throw new Error(`This project doesn't use ${e.name}, so squad-kit leaves it as it is.`);if(l.state==="updating")throw new Error(`squad-kit is updating ${e.name} now.`);if(l.state==="ready")throw new Error(`${e.name}${l.version?` ${l.version}`:""} is new enough already, so there is nothing to update.`);if(l.state!=="too-old")throw new Error(`${e.name} isn't on this computer, so there is nothing to update. The Tools page shows how to install it.`);if(l.cantUpdate)throw new Error(`${l.cantUpdate} The Tools page shows the steps.`);if(!l.update)throw new Error(`squad-kit has not looked at your ${e.name} folder yet. Press Check again, then try again.`);if(!opts.ok)throw new Error(`${l.update.warning} squad-kit updates it only after you say OK.`);let f=foundNow()?.tools[e.id],look=f?await flutterUpdateLook(host2(),f):void 0;if(!f?.where||!look||look.cant)throw new Error(`${look?.cant??"squad-kit can't update it here: it could not look inside its folder."} The Tools page shows the steps.`);return startUpdate(e.id,f.where,{after:()=>{checkTools().catch(()=>{})}})}import fs118 from"fs";import path108 from"path";var list3=dir=>{try{return fs118.readdirSync(dir)}catch{return[]}};function arbLanguage(file3){let m=/(?:^|_)([a-z]{2,3})(?:[_-](?:[A-Z]{2}|[A-Z][a-z]{3}))?\.arb$/.exec(file3);return m?m[1]:null}function arbDir(dir){try{let yaml21=fs118.readFileSync(path108.join(dir,"l10n.yaml"),"utf8"),named=/^arb-dir:\s*["']?([^"'\n#]+?)["']?\s*$/m.exec(yaml21)?.[1];if(named)return path108.join(dir,named)}catch{}return path108.join(dir,"lib","l10n")}function lprojLanguages(dir,depth=3){let out20=[];for(let name of list3(dir)){let m=/^([a-z]{2,3})(?:[-_][A-Za-z]{2,4})?\.lproj$/.exec(name);m?out20.push(m[1]):depth>0&&!name.startsWith(".")&&!/^(node_modules|Pods|build|DerivedData|Carthage)$/.test(name)&&isDir3(path108.join(dir,name))&&out20.push(...lprojLanguages(path108.join(dir,name),depth-1))}return out20}function isDir3(p){try{return fs118.statSync(p).isDirectory()}catch{return!1}}function appLanguages(dir,kind){if(kind==="react-native")return[];let found2=kind==="flutter"?list3(arbDir(dir)).map(arbLanguage).filter(l=>!!l):lprojLanguages(dir),all=[...new Set(found2)].sort((a,b)=>a==="en"?-1:b==="en"?1:a.localeCompare(b));return all.length>1?all:[]}var WEB_VERSION="Web version";function metaOf2(bp){try{return boardExists(bp)?readMeta(bp):null}catch{return null}}function iphoneApp(bp,story2){for(let p of placesOf(bp.root,metaOf2(bp),story2)){let k=kindsOf(p.dir);if(k.kinds.includes("flutter")&&k.platforms.includes("ios"))return{dir:p.dir,kind:"flutter"};if(k.kinds.includes("react-native")&&k.platforms.includes("ios"))return{dir:p.dir,kind:"react-native"};if(k.kinds.includes("ios"))return{dir:p.dir,kind:"ios"}}return null}function tapTools(){let own2=ownTool("maestro"),found2=readFound()?.tools.maestro,maestro2=own2?.binary??(found2?.state==="found"&&found2.where?found2.where:null);if(!maestro2)return null;let java2=ownTool("java"),javaHome=java2?path109.dirname(path109.dirname(java2.binary)):void 0;return{maestro:maestro2,...javaHome?{javaHome}:{}}}var list4=xs=>xs.length<=1?xs[0]??"":`${xs.slice(0,-1).join(", ")} and ${xs[xs.length-1]}`;function webLine(block3){let word2=shotPhone().words.phone,away=block3.tools.filter(x=>x.state==="not-here"),xcode2=block3.tools.some(x=>x.id==="xcode"&&x.state==="missing"),tools4=block3.tools.filter(x=>!(xcode2&&x.id==="simulator")),needs=away.length?"a Mac":list4(tools4.map(x=>x.state==="too-old"||x.state==="updating"?`a newer ${x.name}`:x.name));return`${word2} pictures need ${needs||"more tools"}. squad-kit took pictures of the web version meanwhile.`}var source;function getOwnTools(block3){if(process.env.SQUAD_KIT_NO_AUTO_GET==="1"&&!source||block3.checking||!block3.tools.length||!block3.tools.every(x=>toolEntry(x.id)?.installer==="squad-kit"&&(x.state==="missing"||x.state==="getting")))return!1;let now3=gettingNow();if(now3)return block3.tools.some(x=>x.id===now3.tool);if(block3.tools.some(x=>lastTrouble(x.id)))return!1;let first2=block3.tools.find(x=>x.id==="maestro")??block3.tools[0];try{return getToolNow(first2.id,source?{source}:{}),!0}catch{return!!gettingNow()}}async function untilGot(signal2,ms2=20*6e4){let until=Date.now()+ms2;for(;gettingNow()&&Date.now()<until&&!signal2?.aborted;)await new Promise(r=>setTimeout(r,1e3))}async function picturesWay(bp,story2,web){let app=iphoneApp(bp,story2);if(!app)return{way:"browser"};let block3=blockedBy(bp,"pictures",story2);if(block3?.checking&&(await checkTools().catch(()=>{}),block3=blockedBy(bp,"pictures",story2)),block3&&getOwnTools(block3))return{way:"wait",block:blockedBy(bp,"pictures",story2)??block3,getting:!0};let tools4=block3?null:tapTools();if(!block3&&tools4&&!shotPhone().notHere())return{way:"phone",...app,tools:tools4};let why=block3??{action:"pictures",tools:[{id:"maestro",name:"Maestro",state:"missing"}],text:"squad-kit can't take the phone pictures yet: Maestro isn't on this computer.",status:"Waits for Maestro on this computer. Goes on by itself once it's there",link:"/tools#maestro"};return web?{way:"web",line:webLine(why),link:why.link}:{way:"wait",block:why}}function picturesHold(bp,story2,web){if(!iphoneApp(bp,story2))return null;let block3=blockedBy(bp,"pictures",story2);return block3?getOwnTools(block3)?blockedBy(bp,"pictures",story2)??block3:web?null:block3:null}function phoneStartPlan(bp,action,story2){let app=iphoneApp(bp,story2);if(!app)return null;let languages=appLanguages(app.dir,app.kind),notHere=shotPhone().notHere();if(notHere)return{...app,ready:!1,why:notHere,link:"/tools#xcode",languages};let block3=blockedBy(bp,action,story2);return{...app,ready:!block3,...block3?{why:block3.text,link:block3.link}:{},languages}}function phoneAppView(plan,web,open3,on=!1){return{ready:plan.ready,...plan.why?{why:plan.why}:{},...plan.link?{link:plan.link}:{},languages:plan.languages,web,...on?{on:!0}:{},...open3?{run:open3.view()}:{}}}function languageProblem(language,languages){return language===void 0?null:/^[a-z]{2,3}$/.test(language)?languages.length&&!languages.includes(language)?`This app comes in ${languages.join(", ")}. Pick one of them.`:null:`"${language.slice(0,20)}" is not a language: use two letters, like "ar" or "en".`}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 path110.join(bp.localDir,"apps",app.name.replace(/[^\w.-]+/g,"-")||"app")}async function updateCopy(root,repo,dir,how2={}){let src=path110.join(root,repo.path),commit=latestBase(src,repo.base,repo.remote||void 0).sha;return copyTurn(dir,async()=>{if(await clearHalfCopy(src,dir,how2.signal),copyIsWhole(dir))try{return await gitLater(dir,["checkout","--force","--detach",commit],how2.signal?{signal:how2.signal}:{}),{commit,fresh:!1}}catch(err2){if(err2 instanceof GitStopped)throw err2}return await makeCopy(src,dir,["--force","--detach"],commit,how2),{commit,fresh:!0}})}var ENV_FILE2=/^\.env(\.[\w.-]+)?$/,ENV_EXAMPLE=/\.(example|sample|template|dist)$/;async function localEnvFiles(src){let found2=new Set,list6=args=>gitRecords(src,["ls-files","-z","--others","--exclude-standard",...args],"\0",f=>{let name=path110.basename(f);!f.endsWith("/")&&ENV_FILE2.test(name)&&!ENV_EXAMPLE.test(name)&&found2.add(f)}).catch(()=>{});return await list6([]),await list6(["--ignored","--directory"]),[...found2]}async function copyEnvFiles(src,dir,named=[]){let files=[...new Set([...await localEnvFiles(src),...named.filter(f=>fs119.existsSync(path110.join(src,f)))])];for(let f of files){let to=path110.join(dir,f);fs119.mkdirSync(path110.dirname(to),{recursive:!0}),fs119.copyFileSync(path110.join(src,f),to),fs119.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=path110.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],`
1502
1502
  `,l=>{changesSetup(l.split(" ")[1]??"")&&lines3.push(l)}).catch(()=>{}),createHash10("sha1").update(lines3.join(`
1503
1503
  `)).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=createHash10("sha1");for(let f of[...files].sort()){hash2.update(`${f}\0`);try{hash2.update(fs119.readFileSync(path110.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(host3,port){return new Promise(resolve2=>{let s=net3.connect({host:host3,port}),done2=yes=>{s.destroy(),resolve2(yes)};s.setTimeout(800,()=>setImmediate(()=>done2(!1))),s.once("connect",()=>done2(!0)),s.once("error",()=>done2(!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 out20=execFileSync12("lsof",["-nP",`-iTCP:${port}`,"-sTCP:LISTEN","-Fpc"],{encoding:"utf8",stdio:["ignore","pipe","ignore"],timeout:3e3}),pid=Number(/^p(\d+)/m.exec(out20)?.[1]);return pid?{pid,program:/^c(.+)$/m.exec(out20)?.[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 sleep4=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(fs119.readFileSync(path110.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 path110.join(this.bp.localDir,"apps.json")}records(){try{return JSON.parse(fs119.readFileSync(this.file(),"utf8"))}catch{return{}}}record(repo,change){let all=this.records(),r=all[repo]??{};change(r),all[repo]=r,fs119.mkdirSync(this.bp.localDir,{recursive:!0}),fs119.writeFileSync(this.file(),`${JSON.stringify(all,null,2)}
1504
1504
  `,"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 run7=this.runs.get(app.repo),dir=appCopyDir(this.bp,app),p=this.phoneOf(app),on=!!(run7?.onPhone&&(run7.state==="starting"||run7.state==="running")),base2={...app,dir,...p?{phone:phoneAppView(p.plan,!!app.start,run7?.phone,on)}:{}};if(run7){let at3=run7.address??app.url;return{...base2,state:run7.state,...run7.state==="starting"&&run7.step?{step:run7.step}:{},...run7.address&&run7.state!=="failed"?{address:run7.address}:{},startedAt:run7.startedAt,...run7.since&&run7.state==="running"?{since:run7.since}:{},...run7.error?{error:run7.error}:{},...run7.detail?{detail:run7.detail}:{},...run7.busy?{busy:run7.busy}:{},...run7.commit?{commit:run7.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(`
@@ -1563,10 +1563,10 @@ ${said}`]:[]].join(`
1563
1563
  The project's tests run with: ${how2.command}
1564
1564
 
1565
1565
  ${each}`,made=createTicket(bp,{title:title2,ask:ask2,kind:"bug",oldTests:left2.map(testKey),waitsToStart:!0,...how2.repo?{repo:how2.repo}:{},bySquad:`squad-kit made this story while checking ${from.id}: ${one2?"a test fails":"tests fail"} on main without its change. It waits here until you start it`});for(let t of left2)out20.push({title:testTitle(t),file:t.file,story:made.id});return out20}function oldGateText(n){return n===1?"1 old test also fails on main":`${n} old tests also fail on main`}function oldLine(old,goesOn,waiting2=[]){let titles=old.map(o=>o.title).join("; "),stories=[...new Set(old.map(o=>o.story).filter(s=>!!s))],one2=old.length===1,idle2=stories.filter(s=>waiting2.includes(s)),who4=idle2.length===stories.length?idle2.length===1?"it waits":"they wait":`${idle2.join(" and ")} ${idle2.length===1?"waits":"wait"}`,waits=idle2.length?`; ${who4} on the board until you start ${idle2.length===1?"it":"them"}`:"",made=stories.length?` squad-kit made ${stories.join(" and ")} for ${one2?"it":"them"}${waits}.`:"";return`${one2?"An old test fails":`${old.length} old tests fail`} on main too, without this story's change: ${titles}. ${one2?"It is":"They are"} not this story's${goesOn?", so the work goes on":""}.${made}`}function checkWords(name){return name==="start"?"the app starts":name}function noTestsRan(results){let test=results.find(r=>r.name==="test");return!test||testCount(test.output)===0}function notCheckedText(results){let others=results.filter(r=>r.ok&&r.name!=="test"&&r.name!=="start").map(r=>r.name),started2=results.some(r=>r.ok&&r.name==="start"),passed=[others.length?`${others.join(", ")} ${others.length===1?"passes":"pass"}`:"",started2?"the app starts":""].filter(Boolean).join(", ");return`No tests ran${passed?` \xB7 ${passed}`:""}`}function ranOf(results){return results.map(r=>{let tests=r.name==="test"?testCount(r.output):null;return{name:r.name,command:r.command,ok:r.ok,...tests!==null?{tests}:{},...r.failedFirst!==void 0?{failedOnce:!0}:{}}})}function noTestsQuestion(results){return`${results.some(r=>r.name==="test")?"The project's tests found nothing to run":"This project has no tests yet"}, so squad-kit cannot check that the work does what it should.`}function noTestsDetail(results,suggestion){let test=results.find(r=>r.name==="test");return[test?`The test command (${test.command}) found no tests.`:"No test command is saved. Add one in Settings, under How the squad checks and runs this project.",suggestion?`The Architect suggests: ${suggestion}`:""].filter(Boolean).join(`
1566
- `)}function holdTestCommand(had,given){let test=given.test?.trim();if(!test)return{apply:given};let apply={...given};delete apply.test;let now3=had.test?.trim();return now3?test===now3?{apply}:{apply,change:{from:now3,to:test}}:{apply,suggest:test}}import fs124 from"fs";import{inflateSync}from"zlib";var MIN_SIZE={width:200,height:100},MAX_PIXELS=4e7,MAX_BYTES2=30*1024*1024,SLACK_MS2=2e3,PNG_SIGNATURE=Buffer.from([137,80,78,71,13,10,26,10]);function readPicture(buf){try{if(buf.subarray(0,8).equals(PNG_SIGNATURE))return readPng(buf);if(buf[0]===255&&buf[1]===216)return readJpeg(buf);if(buf.toString("latin1",0,4)==="RIFF"&&buf.toString("latin1",8,12)==="WEBP")return readWebp(buf);if(/^GIF8[79]a$/.test(buf.toString("latin1",0,6)))return{format:"gif",width:buf.readUInt16LE(6),height:buf.readUInt16LE(8)}}catch{return null}return null}function readPng(buf){let pos=8,width=0,height=0,depth=0,color=0,interlace=0,palette=null,alpha=null,data=[];for(;pos+8<=buf.length;){let len=buf.readUInt32BE(pos),type=buf.toString("latin1",pos+4,pos+8),body10=buf.subarray(pos+8,pos+8+len);if(body10.length<len)return null;if(type==="IHDR")width=body10.readUInt32BE(0),height=body10.readUInt32BE(4),depth=body10[8],color=body10[9],interlace=body10[12];else if(type==="PLTE")palette=body10;else if(type==="tRNS")alpha=body10;else if(type==="IDAT")data.push(body10);else if(type==="IEND")break;pos+=12+len}if(!width||!height||!data.length)return null;let picture={format:"png",width,height};if(interlace||width*height>MAX_PIXELS)return picture;let channels={0:1,2:3,3:1,4:2,6:4}[color];if(!channels||![1,2,4,8,16].includes(depth))return null;let raw=inflateSync(Buffer.concat(data)),bitsPerPixel=channels*depth,stride=Math.ceil(width*bitsPerPixel/8),bpp=Math.max(1,bitsPerPixel>>3);if(raw.length<(stride+1)*height)return null;let rows2=unfilter(raw,stride,height,bpp),px=new Uint8Array(width*height*4),sample=(row3,i)=>{if(depth===8)return row3[i];if(depth===16)return row3[i*2];let per=8/depth,byte=row3[Math.floor(i/per)],shift=8-depth*(i%per+1);return byte>>shift&(1<<depth)-1},scale=v=>depth>=8?v:Math.round(v*255/((1<<depth)-1));for(let y=0;y<height;y++){let row3=rows2[y];for(let x=0;x<width;x++){let o=(y*width+x)*4;if(color===3){let k=sample(row3,x);px[o]=palette?.[k*3]??0,px[o+1]=palette?.[k*3+1]??0,px[o+2]=palette?.[k*3+2]??0,px[o+3]=alpha?.[k]??255}else if(color===0||color===4){let g=scale(sample(row3,x*channels));px[o]=px[o+1]=px[o+2]=g,px[o+3]=color===4?sample(row3,x*channels+1):255}else px[o]=sample(row3,x*channels),px[o+1]=sample(row3,x*channels+1),px[o+2]=sample(row3,x*channels+2),px[o+3]=color===6?sample(row3,x*channels+3):255}}return{...picture,pixels:px}}function unfilter(raw,stride,height,bpp){let rows2=[],prev=Buffer.alloc(stride);for(let y=0;y<height;y++){let at3=y*(stride+1),filter=raw[at3],row3=Buffer.from(raw.subarray(at3+1,at3+1+stride));for(let i=0;i<stride;i++){let a=i>=bpp?row3[i-bpp]:0,b=prev[i],c=i>=bpp?prev[i-bpp]:0,add2=0;if(filter===1)add2=a;else if(filter===2)add2=b;else if(filter===3)add2=a+b>>1;else if(filter===4){let p=a+b-c,pa=Math.abs(p-a),pb=Math.abs(p-b),pc=Math.abs(p-c);add2=pa<=pb&&pa<=pc?a:pb<=pc?b:c}row3[i]=row3[i]+add2&255}rows2.push(row3),prev=row3}return rows2}function readJpeg(buf){let pos=2;for(;pos+9<buf.length;){if(buf[pos]!==255)return null;let marker=buf[pos+1];if(marker>=192&&marker<=207&&![196,200,204].includes(marker))return{format:"jpeg",height:buf.readUInt16BE(pos+5),width:buf.readUInt16BE(pos+7)};pos+=2+buf.readUInt16BE(pos+2)}return null}function readWebp(buf){let chunk=buf.toString("latin1",12,16);if(chunk==="VP8 ")return{format:"webp",width:buf.readUInt16LE(26)&16383,height:buf.readUInt16LE(28)&16383};if(chunk==="VP8L"){let bits=buf.readUInt32LE(21);return{format:"webp",width:(bits&16383)+1,height:(bits>>14&16383)+1}}return chunk==="VP8X"?{format:"webp",width:buf.readUIntLE(24,3)+1,height:buf.readUIntLE(27,3)+1}:null}function isBlank(p){if(!p.pixels)return!1;let total=p.width*p.height,counts3=new Map;for(let i=0;i<p.pixels.length;i+=4){let key=p.pixels[i]>>3<<15|p.pixels[i+1]>>3<<10|p.pixels[i+2]>>3<<5|p.pixels[i+3]>>3;counts3.set(key,(counts3.get(key)??0)+1)}let most=0;for(let n of counts3.values())most=Math.max(most,n);return total-most<=total/1e4}function samePicture(a,b){if(a.equals(b))return!0;let pa=readPicture(a),pb=readPicture(b);return!pa?.pixels||!pb?.pixels||pa.width!==pb.width||pa.height!==pb.height?!1:Buffer.from(pa.pixels.buffer,pa.pixels.byteOffset,pa.pixels.byteLength).equals(Buffer.from(pb.pixels.buffer,pb.pixels.byteOffset,pb.pixels.byteLength))}function shotSizeOf(file3){try{let p=readPicture(fs124.readFileSync(file3));return p?p.width<800||p.height>p.width*1.6?"phone":p.height>p.width?"tablet":"desktop":null}catch{return null}}function shotProblem(file3,since){let st;try{st=fs124.statSync(file3)}catch{return"the file is missing"}if(!st.isFile()||st.size===0)return"the file is empty";if(st.size>MAX_BYTES2)return"the file is too big to be a screenshot";if(st.mtimeMs<since-SLACK_MS2)return"it was saved before this step started, so it is not a new screenshot";let p=readPicture(fs124.readFileSync(file3));return p?p.width<MIN_SIZE.width||p.height<MIN_SIZE.height?`it is only ${p.width} by ${p.height} pixels`:isBlank(p)?"it is blank: one color and nothing on it":null:"it is not a picture squad-kit can open"}import fs125 from"fs";import path115 from"path";import{randomBytes as randomBytes7}from"crypto";var SIZES={desktop:{width:1280,height:800},phone:{width:390,height:844}},STEP_MS=1e4,CALL_MS2=15e4,SETUP_MS2=15*6e4;var MAX_PAUSE_MS=5e3,SNAPSHOT_CHARS=4e3,MAX_STEPS=15,MAX_PAGE=300,MAX_VALUE=500,MAX_NAME2=200,STOPPED7="You stopped the squad.",NO_BROWSER="squad-kit could not set up its screenshot browser. Check the internet connection, then press Try again.",NOT_HERE="squad-kit only opens an app on this computer, and the app address in Settings points somewhere else.",ShotsUnavailable=class extends Error{constructor(message,detail){super(message);this.detail=detail}detail},LOOPBACK=new Set(["localhost","127.0.0.1","[::1]"]),LOCAL_NAMES=new Set([...LOOPBACK,"0.0.0.0","[::]"]);function pathOnApp(address,appUrl){let a,b;try{a=new URL(address),b=new URL(appUrl)}catch{return null}let port=u=>u.port||(u.protocol==="https:"?"443":"80");return a.protocol!==b.protocol||a.username||a.password||!LOCAL_NAMES.has(a.hostname)||!LOOPBACK.has(b.hostname)||port(a)!==port(b)?null:`${a.pathname}${a.search}${a.hash}`}function appOrigin(url){let u;try{u=new URL(url)}catch{return null}return u.protocol!=="http:"&&u.protocol!=="https:"?null:LOOPBACK.has(u.hostname)?u.origin:null}function pageProblem(p){if(typeof p!="string"||!p.trim())return'page is missing: give a path in the app, like "/book"';let s=p.trim();if(s.length>MAX_PAGE)return`page is longer than ${MAX_PAGE} characters`;if(/^[a-z][a-z0-9+.-]*:/i.test(s))return`page "${s.slice(0,60)}" is a full address: give only the path in the app, like "/book"`;if(!s.startsWith("/"))return`page "${s.slice(0,60)}" is not a path in the app: it starts with "/", like "/book"`;if(s.startsWith("//")||s.includes("\\"))return`page "${s.slice(0,60)}" names another host: give only the path in the app`;if(/[\s\u0000-\u001f\u007f]/.test(s))return"page has spaces or control characters";let decoded=s;try{decoded=decodeURIComponent(s)}catch{return"page is not a valid address path"}return/(^|[/\\])\.\.([/\\?#]|$)/.test(decoded)||decoded.startsWith("//")||decoded.includes("\\")?'page may not go up a folder ("..") or name another host':null}function addressIn2(origin,p){if(pageProblem(p))return null;let u=new URL(p.trim(),origin);return u.origin===origin?u.href:null}function pathOf(url){try{let u=new URL(url);return`${u.pathname}${u.search}`}catch{return url}}var ROLES=new Set("alert alertdialog application article banner blockquote button caption cell checkbox code columnheader combobox complementary contentinfo definition deletion dialog directory document emphasis feed figure form generic grid gridcell group heading img insertion link list listbox listitem log main marquee math menu menubar menuitem menuitemcheckbox menuitemradio meter navigation none note option paragraph presentation progressbar radio radiogroup region row rowgroup rowheader scrollbar search searchbox separator slider spinbutton status strong subscript superscript switch tab table tablist tabpanel term textbox time timer toolbar tooltip tree treegrid treeitem".split(" ")),ACTIONS=new Set(["goto","click","fill","select","check","uncheck","press","hover","wait"]),NEEDS_TARGET=new Set(["click","fill","select","check","uncheck","hover"]),WAYS=["role","label","placeholder","text","testId","css"],STEP_FIELDS=new Set(["action","target","value","path","key","ms"]),TARGET_FIELDS=new Set(["role","name",...WAYS]),KEY=/^(Shift\+)?([A-Za-z0-9]|Enter|Tab|Escape|Backspace|Delete|Space|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|Home|End|PageUp|PageDown)$/,filled=v=>typeof v=="string"&&v.trim()!=="";function oneWay(t){let way=WAYS.find(w=>filled(t[w]));return way?way==="role"?{role:t.role.trim().toLowerCase(),...filled(t.name)?{name:t.name}:{}}:{[way]:t[way]}:{}}function targetProblem(t){if(!t||typeof t!="object"||Array.isArray(t))return"target must be an object";let extra=Object.keys(t).find(k=>!TARGET_FIELDS.has(k));if(extra)return`target has a field squad-kit does not take: "${extra.slice(0,40)}"`;for(let k of TARGET_FIELDS){let v=t[k];if(v!==void 0&&typeof v!="string")return`target.${k} must be text`;if(typeof v=="string"&&v.length>MAX_NAME2)return`target.${k} is longer than ${MAX_NAME2} characters`}let w=oneWay(t);return Object.keys(w).length?w.role&&!ROLES.has(w.role)?`"${t.role}" is not a role (like button, link, textbox, checkbox, heading)`:null:"target names nothing: give role and name, label, text, placeholder, testId or css"}function stepProblem(s){if(!s||typeof s!="object"||Array.isArray(s)||!ACTIONS.has(s.action))return`"${String(s?.action).slice(0,40)}" is not a step squad-kit knows`;let extra=Object.keys(s).find(k=>!STEP_FIELDS.has(k));if(extra)return`a step has a field squad-kit does not take: "${extra.slice(0,40)}"`;if(s.value!==void 0&&typeof s.value!="string")return"value must be text";if(typeof s.value=="string"&&s.value.length>MAX_VALUE)return`value is longer than ${MAX_VALUE} characters`;if(s.action==="goto"){let p=pageProblem(s.path);return p?`goto: ${p.replace(/^page/,"path")}`:null}return s.path!==void 0?"only goto takes a path":s.action==="press"&&(!filled(s.key)||!KEY.test(s.key))?'press needs key: one letter or digit, or a key like "Enter" or "Tab"':s.action!=="press"&&s.key!==void 0?"only press takes a key":s.ms!==void 0&&(typeof s.ms!="number"||!Number.isFinite(s.ms)||s.ms<0)?"ms must be a number of milliseconds":s.action==="fill"&&typeof s.value!="string"?"fill needs value":s.action==="select"&&!filled(s.value)?"select needs value: the option to pick":s.action==="wait"&&!s.target&&typeof s.ms!="number"?"wait needs target (what to wait for) or ms":NEEDS_TARGET.has(s.action)&&!s.target?`${s.action} needs target`:s.target!==void 0?targetProblem(s.target):null}function stepsProblem(steps){if(steps===void 0)return null;if(!Array.isArray(steps))return"steps must be a list";if(steps.length>MAX_STEPS)return`there are ${steps.length} steps, and squad-kit takes at most ${MAX_STEPS}`;for(let[i,st]of steps.entries()){let p=stepProblem(st);if(p)return`step ${i+1}: ${p}`}return null}function targetWords(t){let w=oneWay(t);return w.role?w.name?`${w.role} named "${w.name}"`:w.role:w.label?`field labelled "${w.label}"`:w.placeholder?`field with the placeholder "${w.placeholder}"`:w.text?`text "${w.text}"`:w.testId?`test id "${w.testId}"`:`css "${w.css??""}"`}function stepWords2(s){let t=s.target?targetWords(s.target):"";switch(s.action){case"goto":return`open ${s.path}`;case"fill":return`fill the ${t} with "${s.value}"`;case"select":return`pick "${s.value}" in the ${t}`;case"press":return`press ${s.key}${t?` in the ${t}`:""}`;case"wait":return t?`wait for the ${t}`:`wait ${Math.min(s.ms??0,MAX_PAUSE_MS)} ms`;default:return`${s.action} the ${t}`}}var ROLE_WORDS={button:"button",link:"link",textbox:"field",searchbox:"search field",combobox:"list",listbox:"list",spinbutton:"field",checkbox:"tick box",switch:"switch",radio:"choice",option:"choice",heading:"heading",tab:"tab",menuitem:"menu item",img:"picture",dialog:"window",row:"row",cell:"cell"};function plainThing(t){let w=oneWay(t);return w.role?w.name?`the ${ROLE_WORDS[w.role]??"part"} "${w.name}"`:`the ${ROLE_WORDS[w.role]??"part of the page"}`:w.label?`the field "${w.label}"`:w.placeholder?`the field "${w.placeholder}"`:w.text?`the words "${w.text}"`:"a part of the page the Tester named"}var cap2=s=>`${s.charAt(0).toUpperCase()}${s.slice(1)}`,firstLine2=s=>(s.split(`
1566
+ `)}function holdTestCommand(had,given){let test=given.test?.trim();if(!test)return{apply:given};let apply={...given};delete apply.test;let now3=had.test?.trim();return now3?test===now3?{apply}:{apply,change:{from:now3,to:test}}:{apply,suggest:test}}import fs124 from"fs";import{inflateSync}from"zlib";var MIN_SIZE={width:200,height:100},MAX_PIXELS=4e7,MAX_BYTES2=30*1024*1024,SLACK_MS2=2e3,PNG_SIGNATURE=Buffer.from([137,80,78,71,13,10,26,10]);function readPicture(buf){try{if(buf.subarray(0,8).equals(PNG_SIGNATURE))return readPng(buf);if(buf[0]===255&&buf[1]===216)return readJpeg(buf);if(buf.toString("latin1",0,4)==="RIFF"&&buf.toString("latin1",8,12)==="WEBP")return readWebp(buf);if(/^GIF8[79]a$/.test(buf.toString("latin1",0,6)))return{format:"gif",width:buf.readUInt16LE(6),height:buf.readUInt16LE(8)}}catch{return null}return null}function readPng(buf){let pos=8,width=0,height=0,depth=0,color=0,interlace=0,palette=null,alpha=null,data=[];for(;pos+8<=buf.length;){let len=buf.readUInt32BE(pos),type=buf.toString("latin1",pos+4,pos+8),body10=buf.subarray(pos+8,pos+8+len);if(body10.length<len)return null;if(type==="IHDR")width=body10.readUInt32BE(0),height=body10.readUInt32BE(4),depth=body10[8],color=body10[9],interlace=body10[12];else if(type==="PLTE")palette=body10;else if(type==="tRNS")alpha=body10;else if(type==="IDAT")data.push(body10);else if(type==="IEND")break;pos+=12+len}if(!width||!height||!data.length)return null;let picture={format:"png",width,height};if(interlace||width*height>MAX_PIXELS)return picture;let channels={0:1,2:3,3:1,4:2,6:4}[color];if(!channels||![1,2,4,8,16].includes(depth))return null;let raw=inflateSync(Buffer.concat(data)),bitsPerPixel=channels*depth,stride=Math.ceil(width*bitsPerPixel/8),bpp=Math.max(1,bitsPerPixel>>3);if(raw.length<(stride+1)*height)return null;let rows2=unfilter(raw,stride,height,bpp),px=new Uint8Array(width*height*4),sample=(row3,i)=>{if(depth===8)return row3[i];if(depth===16)return row3[i*2];let per=8/depth,byte=row3[Math.floor(i/per)],shift=8-depth*(i%per+1);return byte>>shift&(1<<depth)-1},scale=v=>depth>=8?v:Math.round(v*255/((1<<depth)-1));for(let y=0;y<height;y++){let row3=rows2[y];for(let x=0;x<width;x++){let o=(y*width+x)*4;if(color===3){let k=sample(row3,x);px[o]=palette?.[k*3]??0,px[o+1]=palette?.[k*3+1]??0,px[o+2]=palette?.[k*3+2]??0,px[o+3]=alpha?.[k]??255}else if(color===0||color===4){let g=scale(sample(row3,x*channels));px[o]=px[o+1]=px[o+2]=g,px[o+3]=color===4?sample(row3,x*channels+1):255}else px[o]=sample(row3,x*channels),px[o+1]=sample(row3,x*channels+1),px[o+2]=sample(row3,x*channels+2),px[o+3]=color===6?sample(row3,x*channels+3):255}}return{...picture,pixels:px}}function unfilter(raw,stride,height,bpp){let rows2=[],prev=Buffer.alloc(stride);for(let y=0;y<height;y++){let at3=y*(stride+1),filter=raw[at3],row3=Buffer.from(raw.subarray(at3+1,at3+1+stride));for(let i=0;i<stride;i++){let a=i>=bpp?row3[i-bpp]:0,b=prev[i],c=i>=bpp?prev[i-bpp]:0,add2=0;if(filter===1)add2=a;else if(filter===2)add2=b;else if(filter===3)add2=a+b>>1;else if(filter===4){let p=a+b-c,pa=Math.abs(p-a),pb=Math.abs(p-b),pc=Math.abs(p-c);add2=pa<=pb&&pa<=pc?a:pb<=pc?b:c}row3[i]=row3[i]+add2&255}rows2.push(row3),prev=row3}return rows2}function readJpeg(buf){let pos=2;for(;pos+9<buf.length;){if(buf[pos]!==255)return null;let marker=buf[pos+1];if(marker>=192&&marker<=207&&![196,200,204].includes(marker))return{format:"jpeg",height:buf.readUInt16BE(pos+5),width:buf.readUInt16BE(pos+7)};pos+=2+buf.readUInt16BE(pos+2)}return null}function readWebp(buf){let chunk=buf.toString("latin1",12,16);if(chunk==="VP8 ")return{format:"webp",width:buf.readUInt16LE(26)&16383,height:buf.readUInt16LE(28)&16383};if(chunk==="VP8L"){let bits=buf.readUInt32LE(21);return{format:"webp",width:(bits&16383)+1,height:(bits>>14&16383)+1}}return chunk==="VP8X"?{format:"webp",width:buf.readUIntLE(24,3)+1,height:buf.readUIntLE(27,3)+1}:null}function isBlank(p){if(!p.pixels)return!1;let total=p.width*p.height,counts3=new Map;for(let i=0;i<p.pixels.length;i+=4){let key=p.pixels[i]>>3<<15|p.pixels[i+1]>>3<<10|p.pixels[i+2]>>3<<5|p.pixels[i+3]>>3;counts3.set(key,(counts3.get(key)??0)+1)}let most=0;for(let n of counts3.values())most=Math.max(most,n);return total-most<=total/1e4}function samePicture(a,b){if(a.equals(b))return!0;let pa=readPicture(a),pb=readPicture(b);return!pa?.pixels||!pb?.pixels||pa.width!==pb.width||pa.height!==pb.height?!1:Buffer.from(pa.pixels.buffer,pa.pixels.byteOffset,pa.pixels.byteLength).equals(Buffer.from(pb.pixels.buffer,pb.pixels.byteOffset,pb.pixels.byteLength))}function shotSizeOf(file3){try{let p=readPicture(fs124.readFileSync(file3));return p?p.width<800||p.height>p.width*1.6?"phone":p.height>p.width?"tablet":"desktop":null}catch{return null}}function shotProblem(file3,since){let st;try{st=fs124.statSync(file3)}catch{return"the file is missing"}if(!st.isFile()||st.size===0)return"the file is empty";if(st.size>MAX_BYTES2)return"the file is too big to be a screenshot";if(st.mtimeMs<since-SLACK_MS2)return"it was saved before this step started, so it is not a new screenshot";let p=readPicture(fs124.readFileSync(file3));return p?p.width<MIN_SIZE.width||p.height<MIN_SIZE.height?`it is only ${p.width} by ${p.height} pixels`:isBlank(p)?"it is blank: one color and nothing on it":null:"it is not a picture squad-kit can open"}import fs125 from"fs";import path115 from"path";import{randomBytes as randomBytes7}from"crypto";var SIZES={desktop:{width:1280,height:800},phone:{width:390,height:844}},STEP_MS=1e4,CALL_MS2=15e4,SETUP_MS2=15*6e4;var MAX_PAUSE_MS=5e3,SNAPSHOT_CHARS=4e3,MAX_STEPS=15,MAX_PAGE=300,MAX_VALUE=500,MAX_NAME2=200,STOPPED7="You stopped the squad.",NO_BROWSER="squad-kit could not set up its screenshot browser. Check the internet connection, then press Try again.",NOT_HERE="squad-kit only opens an app on this computer, and the app address in Settings points somewhere else.",ShotsUnavailable=class extends Error{constructor(message,detail){super(message);this.detail=detail}detail},LOOPBACK=new Set(["localhost","127.0.0.1","[::1]"]),LOCAL_NAMES=new Set([...LOOPBACK,"0.0.0.0","[::]"]);function pathOnApp(address,appUrl){let a,b;try{a=new URL(address),b=new URL(appUrl)}catch{return null}let port=u=>u.port||(u.protocol==="https:"?"443":"80");return a.protocol!==b.protocol||a.username||a.password||!LOCAL_NAMES.has(a.hostname)||!LOOPBACK.has(b.hostname)||port(a)!==port(b)?null:`${a.pathname}${a.search}${a.hash}`}function appOrigin(url){let u;try{u=new URL(url)}catch{return null}return u.protocol!=="http:"&&u.protocol!=="https:"?null:LOOPBACK.has(u.hostname)?u.origin:null}function pageProblem(p){if(typeof p!="string"||!p.trim())return'page is missing: give a path in the app, like "/book"';let s=p.trim();if(s.length>MAX_PAGE)return`page is longer than ${MAX_PAGE} characters`;if(/^[a-z][a-z0-9+.-]*:/i.test(s))return`page "${s.slice(0,60)}" is a full address: give only the path in the app, like "/book"`;if(!s.startsWith("/"))return`page "${s.slice(0,60)}" is not a path in the app: it starts with "/", like "/book"`;if(s.startsWith("//")||s.includes("\\"))return`page "${s.slice(0,60)}" names another host: give only the path in the app`;if(/[\s\u0000-\u001f\u007f]/.test(s))return"page has spaces or control characters";let decoded=s;try{decoded=decodeURIComponent(s)}catch{return"page is not a valid address path"}return/(^|[/\\])\.\.([/\\?#]|$)/.test(decoded)||decoded.startsWith("//")||decoded.includes("\\")?'page may not go up a folder ("..") or name another host':null}function addressIn2(origin,p){if(pageProblem(p))return null;let u=new URL(p.trim(),origin);return u.origin===origin?u.href:null}function pathOf(url){try{let u=new URL(url);return`${u.pathname}${u.search}`}catch{return url}}var ROLES=new Set("alert alertdialog application article banner blockquote button caption cell checkbox code columnheader combobox complementary contentinfo definition deletion dialog directory document emphasis feed figure form generic grid gridcell group heading img insertion link list listbox listitem log main marquee math menu menubar menuitem menuitemcheckbox menuitemradio meter navigation none note option paragraph presentation progressbar radio radiogroup region row rowgroup rowheader scrollbar search searchbox separator slider spinbutton status strong subscript superscript switch tab table tablist tabpanel term textbox time timer toolbar tooltip tree treegrid treeitem".split(" ")),ACTIONS=new Set(["goto","click","fill","select","check","uncheck","press","hover","wait"]),NEEDS_TARGET=new Set(["click","fill","select","check","uncheck","hover"]),WAYS=["role","label","placeholder","text","testId","css"],STEP_FIELDS=new Set(["action","target","value","path","key","ms"]),TARGET_FIELDS=new Set(["role","name",...WAYS]),KEY=/^(Shift\+)?([A-Za-z0-9]|Enter|Tab|Escape|Backspace|Delete|Space|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|Home|End|PageUp|PageDown)$/,filled=v=>typeof v=="string"&&v.trim()!=="";function oneWay(t){let way=WAYS.find(w=>filled(t[w]));return way?way==="role"?{role:t.role.trim().toLowerCase(),...filled(t.name)?{name:t.name}:{}}:{[way]:t[way]}:{}}function targetProblem(t){if(!t||typeof t!="object"||Array.isArray(t))return"target must be an object";let extra=Object.keys(t).find(k=>!TARGET_FIELDS.has(k));if(extra)return`target has a field squad-kit does not take: "${extra.slice(0,40)}"`;for(let k of TARGET_FIELDS){let v=t[k];if(v!==void 0&&typeof v!="string")return`target.${k} must be text`;if(typeof v=="string"&&v.length>MAX_NAME2)return`target.${k} is longer than ${MAX_NAME2} characters`}let w=oneWay(t);return Object.keys(w).length?w.role&&!ROLES.has(w.role)?`"${t.role}" is not a role (like button, link, textbox, checkbox, heading)`:null:"target names nothing: give role and name, label, text, placeholder, testId or css"}function stepProblem(s){if(!s||typeof s!="object"||Array.isArray(s)||!ACTIONS.has(s.action))return`"${String(s?.action).slice(0,40)}" is not a step squad-kit knows`;let extra=Object.keys(s).find(k=>!STEP_FIELDS.has(k));if(extra)return`a step has a field squad-kit does not take: "${extra.slice(0,40)}"`;if(s.value!==void 0&&typeof s.value!="string")return"value must be text";if(typeof s.value=="string"&&s.value.length>MAX_VALUE)return`value is longer than ${MAX_VALUE} characters`;if(s.action==="goto"){let p=pageProblem(s.path);return p?`goto: ${p.replace(/^page/,"path")}`:null}return s.path!==void 0?"only goto takes a path":s.action==="press"&&(!filled(s.key)||!KEY.test(s.key))?'press needs key: one letter or digit, or a key like "Enter" or "Tab"':s.action!=="press"&&s.key!==void 0?"only press takes a key":s.ms!==void 0&&(typeof s.ms!="number"||!Number.isFinite(s.ms)||s.ms<0)?"ms must be a number of milliseconds":s.action==="fill"&&typeof s.value!="string"?"fill needs value":s.action==="select"&&!filled(s.value)?"select needs value: the option to pick":s.action==="wait"&&!s.target&&typeof s.ms!="number"?"wait needs target (what to wait for) or ms":NEEDS_TARGET.has(s.action)&&!s.target?`${s.action} needs target`:s.target!==void 0?targetProblem(s.target):null}function stepsProblem(steps){if(steps===void 0)return null;if(!Array.isArray(steps))return"steps must be a list";if(steps.length>MAX_STEPS)return`there are ${steps.length} steps, and squad-kit takes at most ${MAX_STEPS}`;for(let[i,st]of steps.entries()){let p=stepProblem(st);if(p)return`step ${i+1}: ${p}`}return null}function targetWords(t){let w=oneWay(t);return w.role?w.name?`${w.role} named "${w.name}"`:w.role:w.label?`field labelled "${w.label}"`:w.placeholder?`field with the placeholder "${w.placeholder}"`:w.text?`text "${w.text}"`:w.testId?`test id "${w.testId}"`:`css "${w.css??""}"`}function stepWords3(s){let t=s.target?targetWords(s.target):"";switch(s.action){case"goto":return`open ${s.path}`;case"fill":return`fill the ${t} with "${s.value}"`;case"select":return`pick "${s.value}" in the ${t}`;case"press":return`press ${s.key}${t?` in the ${t}`:""}`;case"wait":return t?`wait for the ${t}`:`wait ${Math.min(s.ms??0,MAX_PAUSE_MS)} ms`;default:return`${s.action} the ${t}`}}var ROLE_WORDS={button:"button",link:"link",textbox:"field",searchbox:"search field",combobox:"list",listbox:"list",spinbutton:"field",checkbox:"tick box",switch:"switch",radio:"choice",option:"choice",heading:"heading",tab:"tab",menuitem:"menu item",img:"picture",dialog:"window",row:"row",cell:"cell"};function plainThing(t){let w=oneWay(t);return w.role?w.name?`the ${ROLE_WORDS[w.role]??"part"} "${w.name}"`:`the ${ROLE_WORDS[w.role]??"part of the page"}`:w.label?`the field "${w.label}"`:w.placeholder?`the field "${w.placeholder}"`:w.text?`the words "${w.text}"`:"a part of the page the Tester named"}var cap2=s=>`${s.charAt(0).toUpperCase()}${s.slice(1)}`,firstLine2=s=>(s.split(`
1567
1567
  `).find(l=>l.trim())??s).trim().slice(0,200);function failureWords(step2,err2,stepMs=STEP_MS){let t=step2?.target;if(err2 instanceof StepFailure)switch(err2.kind){case"missing":return t?{why:`no ${targetWords(t)} on the page`,plain:`squad-kit could not find ${plainThing(t)} on the screen.`}:{why:"nothing to act on",plain:"The screen was not as expected."};case"hidden":return{why:`the ${t?targetWords(t):"part"} is on the page but not visible`,plain:`${cap2(t?plainThing(t):"the part it needed")} is there but hidden.`};case"disabled":return{why:`the ${t?targetWords(t):"part"} is turned off (disabled)`,plain:`${cap2(t?plainThing(t):"the part it needed")} is there but cannot be used yet.`};case"option":return step2?.value?{why:`no option "${step2.value}" in the ${t?targetWords(t):"list"}`,plain:`The list has no choice "${step2.value}".`}:{why:`no such option in the ${t?targetWords(t):"list"}`,plain:"The list does not have that choice."};case"blocked":return{why:`the page tried to open ${err2.address??"another site"}, outside the app; squad-kit only opens the app`,plain:"The app tried to open a page outside itself, which squad-kit does not do."};case"timeout":return{why:`it did not finish within ${Math.round(stepMs/1e3)} seconds`,plain:"The screen took too long to react."};case"closed":return{why:"the page closed",plain:"The page closed by itself."};default:return{why:firstLine2(err2.message),plain:"The screen did not react as expected."}}return/ECONNREFUSED|ERR_CONNECTION_REFUSED|ERR_CONNECTION_RESET|ERR_EMPTY_RESPONSE/i.test(err2?.message??"")?{why:"the app did not answer",plain:"The app stopped answering."}:{why:firstLine2(err2?.message??String(err2)),plain:"The screen did not react as expected."}}function trimSnapshot(text3,max=SNAPSHOT_CHARS){let s=(text3??"").trim();if(s.length<=max)return s;let cut=s.lastIndexOf(`
1568
1568
  `,max);return`${s.slice(0,cut>max/2?cut:max)}
1569
- \u2026 (cut short)`}var JARGON=/\b(sandbox(ed)?|mcp|ports?|pids?|process id|playwright|chromium|chrome|mach|sigtrap|sigkill|sigterm|selectors?|locators?|localhost|127\.0\.0\.1|https?|npx|npm|pnpm|pip|stack ?trace|exit (code|status)|permission denied|eperm|eacces|econnrefused|enoent|bootstrap_check_in|kill(ed)?|shell|bash|zsh|stderr|stdout|headless|seatbelt|cli|api|json|yaml|xpath|url|sudo|chmod|env(ironment)? var(iable)?s?|traceback|exception|undefined|git|config(uration)? files?)\b|\.squad\b|\.env\b|(^|[\s"'(])(~\/|\/(Users|home|private|tmp|var|etc|opt)\/)|[\w.-]+\/[\w./-]*\.(m?[jt]sx?|json|py|ya?ml|toml|sh|md|lock|cfg|ini)\b/i,VARIABLE=/\b[A-Z][A-Z0-9]*_[A-Z0-9_]+\b/;var NOTE_JARGON=new RegExp(JARGON.source.replace("chromium|chrome|","chromium|").replace("traceback|exception|","traceback|")+"|\\b(python3?|docker|curl|wget|node_modules|stderr|devtools)\\b","i"),NOTE_ERROR=/\b[A-Z][a-z]\w*(Error|Exception)\b|\bE(ACCES|PERM|NOENT|ADDRINUSE|ADDRNOTAVAIL|CONNREFUSED|CONNRESET|CONNABORTED|PIPE|TIMEDOUT|EXIST|ISDIR|NOTDIR|NOTEMPTY|MFILE|NOTFOUND|AI_AGAIN|HOSTUNREACH|NETUNREACH|BUSY|INVAL|NOSPC|ROFS)\b/,NOTE_STATUS=/\b(status code|http|exit code|exit status)\s*:?\s*\d{1,3}\b|\berrno\b/i,STACK=/\bat \S+ \(?[^\s()]+:\d+:\d+\)?/;function isPlainNote(sentence){return!NOTE_JARGON.test(sentence)&&!VARIABLE.test(sentence)&&!NOTE_ERROR.test(sentence)&&!NOTE_STATUS.test(sentence)&&!STACK.test(sentence)}function plainParts(text3,technical=()=>!1){let kept2=[],left2=[];for(let s of text3.split(/(?<=[.!?])\s+|\n+/).map(x=>x.trim()).filter(Boolean))(isPlainNote(s)&&!technical(s)?kept2:left2).push(s);return{kept:kept2.join(" "),left:left2.join(" ")}}function within(p,ms2,what){let timer,late2=new Promise((_,reject)=>{timer=setTimeout(()=>reject(new StepFailure("timeout",`${what} took longer than ${Math.round(ms2/1e3)} seconds`)),ms2)});return Promise.race([p,late2]).finally(()=>clearTimeout(timer))}async function openBrowser(signal2,onLine){if(signal2?.aborted)throw new Error(STOPPED7);let launching=shotBrowser().launch(onLine?{onLine}:{}),stopped,onStop=new Promise((_,reject)=>{stopped=()=>reject(new Error(STOPPED7)),signal2?.addEventListener("abort",stopped,{once:!0})});try{return await Promise.race([launching,onStop])}catch(err2){throw signal2?.aborted?(launching.then(b=>b.close()).catch(()=>{}),new Error(STOPPED7)):err2 instanceof BrowserSetupError?new ShotsUnavailable(NO_BROWSER,`${err2.message}: ${err2.detail}`):new ShotsUnavailable(NO_BROWSER,err2.message??String(err2))}finally{stopped&&signal2?.removeEventListener("abort",stopped)}}var BrowserSession=class{constructor(o={}){this.o=o;o.signal?.aborted?this.closed=!0:o.signal?.addEventListener("abort",()=>{this.close()},{once:!0})}o;started=null;closed=!1;browser(){return this.closed?Promise.reject(new Error(STOPPED7)):(this.started||(this.started=openBrowser(this.o.signal,this.o.onLine),this.started.catch(()=>this.started=null)),this.started)}async ready(){try{return await this.browser(),!0}catch{return!1}}async close(){this.closed=!0;let b=this.started;this.started=null,await b?.then(x=>x.close()).catch(()=>{})}};async function open(page,origin,p,ms2){let url=addressIn2(origin,p);if(!url)throw new StepFailure("other",`"${p}" is not a path in the app`);return within(page.goto(url,ms2),ms2+5e3,`opening ${p}`)}function statusWords(status){return status&&status>=400?` The page answered with error status ${status}.`:""}function stillInApp(page,origin){let at3=page.url();if(!(at3!=="about:blank"&&appOrigin(at3)===origin))throw new StepFailure("blocked",`The page is at ${at3}`,at3)}async function snapshotOf(page){try{return trimSnapshot(await within(page.snapshot(),6e3,"reading the page"))}catch{return""}}async function walk2(browser,ask2,c){let stepMs=c.stepMs??STEP_MS,phone=ask2.size==="phone",page=null,at3=0,step2=null;try{let left2=()=>Math.max(1e3,Math.min(stepMs,c.deadline-Date.now()));page=await within(browser.page({origin:c.origin,...c.also?.length?{also:c.also}:{},...SIZES[phone?"phone":"desktop"],phone}),left2(),"opening a page");let status=await open(page,c.origin,ask2.page,left2());stillInApp(page,c.origin);for(let[i,raw]of(ask2.steps??[]).entries()){if(at3=i+1,step2={...raw,...raw.target?{target:oneWay(raw.target)}:{},...raw.action==="wait"&&typeof raw.ms=="number"?{ms:Math.max(0,Math.min(raw.ms,MAX_PAUSE_MS))}:{}},Date.now()>=c.deadline)throw new StepFailure("timeout","The call ran out of time");let ms2=left2();step2.action==="goto"?status=await open(page,c.origin,step2.path,ms2):await within(page.act(step2,ms2),ms2+2e3,stepWords2(step2)),stillInApp(page,c.origin)}return await page.settle(3e3),stillInApp(page,c.origin),{page,status}}catch(err2){if(c.signal?.aborted)throw await page?.close().catch(()=>{}),new Error(STOPPED7);let words2=failureWords(step2,err2,stepMs),where=page?page.url():"";return{page,failed:{step:at3,what:at3&&step2?stepWords2(step2):`open ${ask2.page}`,...words2,page:where&&where!=="about:blank"&&appOrigin(where)===c.origin?pathOf(where):ask2.page,snapshot:page?await snapshotOf(page):"",...err2 instanceof StepFailure&&err2.kind==="blocked"?{left:!0}:{}}}}}async function shootOne(session,ask2,c){let browser=await session.browser(),r=await walk2(browser,ask2,c);try{if("failed"in r)return r.failed;let png=await within(r.page.screenshot(),2e4,"taking the picture");return png?.length?{png,page:pathOf(r.page.url()),...r.status?{status:r.status}:{}}:{step:0,what:"save the picture",why:"the browser gave no picture",plain:"squad-kit could not save the picture.",page:pathOf(r.page.url()),snapshot:""}}catch(err2){if(c.signal?.aborted)throw new Error(STOPPED7);return{step:0,what:"save the picture",...failureWords(null,err2,c.stepMs),page:ask2.page,snapshot:""}}finally{await r.page?.close().catch(()=>{})}}async function lookAt(session,ask2,c){let browser=await session.browser(),r=await walk2(browser,ask2,c);try{return"failed"in r?r.failed:{page:pathOf(r.page.url()),snapshot:await snapshotOf(r.page),...r.status?{status:r.status}:{}}}finally{await r.page?.close().catch(()=>{})}}function writeNew(file3,bytes){let c=fs125.constants,fd;try{fd=fs125.openSync(file3,c.O_WRONLY|c.O_CREAT|c.O_EXCL|(c.O_NOFOLLOW??0),420)}catch(err2){if(err2.code==="EEXIST")return!1;throw err2}try{fs125.writeFileSync(fd,bytes)}finally{fs125.closeSync(fd)}return!0}async function prepareShots(o={}){let mod=shotBrowser();if(!mod.prepare)return;if(o.signal?.aborted)throw new Error(STOPPED7);let timer,onStop,limit=new Promise((_,reject)=>{timer=setTimeout(()=>reject(new BrowserSetupError("Setting up the browser took too long",`It did not finish within ${Math.round((o.ms??SETUP_MS2)/6e4)} minutes`)),o.ms??SETUP_MS2),onStop=()=>reject(new Error(STOPPED7)),o.signal?.addEventListener("abort",onStop,{once:!0})});try{await Promise.race([mod.prepare(o.onLine?{onLine:o.onLine}:{}),limit])}catch(err2){throw o.signal?.aborted?new Error(STOPPED7):err2 instanceof BrowserSetupError?new ShotsUnavailable(NO_BROWSER,`${err2.message}: ${err2.detail}`):new ShotsUnavailable(NO_BROWSER,err2.message??String(err2))}finally{clearTimeout(timer),onStop&&o.signal?.removeEventListener("abort",onStop)}}async function takeOne(url,file3,o={}){let origin=appOrigin(url);if(!origin)throw new ShotsUnavailable(NOT_HERE,`${url} is not an http(s) address on this computer`);await prepareShots(o);let session=new BrowserSession(o),hard=setTimeout(()=>{session.close()},9e4);try{let page=pathOf(url)||"/",r=await shootOne(session,{page:page.startsWith("/")?page:"/"},{origin,stepMs:3e4,deadline:Date.now()+6e4,...o.signal?{signal:o.signal}:{}});if(!("png"in r))throw new ShotsUnavailable(r.plain,`${r.what}: ${r.why}`);fs125.mkdirSync(path115.dirname(file3),{recursive:!0});let part=path115.join(path115.dirname(file3),`.${path115.basename(file3)}.${randomBytes7(6).toString("hex")}.part`);if(!writeNew(part,r.png))throw new ShotsUnavailable("squad-kit could not save the picture.",`${part} is taken`);return fs125.renameSync(part,file3),{page:r.page}}finally{clearTimeout(hard),await session.close()}}function shotsErrorDetail(err2){return err2 instanceof ShotsUnavailable?err2.detail:err2?.message??String(err2)}import fs126 from"fs";import path116 from"path";import{randomBytes as randomBytes8}from"crypto";import zlib2 from"zlib";var MAX_ROWS=200,MAX_COLUMNS=40,MAX_SHEETS=12,MAX_ENTRIES3=2e3,MAX_UNPACKED2=32*1024*1024,END_OF_DIRECTORY=101010256,DIRECTORY_ENTRY=33639248,LOCAL_ENTRY=67324752;function unzip(zip2,wanted){let end=-1;for(let at4=zip2.length-22;at4>=Math.max(0,zip2.length-22-65535);at4-=1)if(zip2.readUInt32LE(at4)===END_OF_DIRECTORY){end=at4;break}if(end<0)throw new Error("not a zip");let count3=zip2.readUInt16LE(end+10);if(count3>MAX_ENTRIES3)throw new Error("too many files");let at3=zip2.readUInt32LE(end+16),files=new Map;for(let i=0;i<count3;i+=1){if(zip2.readUInt32LE(at3)!==DIRECTORY_ENTRY)throw new Error("a broken directory");let method=zip2.readUInt16LE(at3+10),packed=zip2.readUInt32LE(at3+20),nameLength=zip2.readUInt16LE(at3+28),extra=zip2.readUInt16LE(at3+30),comment=zip2.readUInt16LE(at3+32),local=zip2.readUInt32LE(at3+42),name=zip2.toString("utf8",at3+46,at3+46+nameLength);if(at3+=46+nameLength+extra+comment,!wanted(name))continue;if(zip2.readUInt32LE(local)!==LOCAL_ENTRY)throw new Error("a broken entry");let start=local+30+zip2.readUInt16LE(local+26)+zip2.readUInt16LE(local+28),body10=zip2.subarray(start,start+packed),data=method===8?zlib2.inflateRawSync(body10,{maxOutputLength:MAX_UNPACKED2}):method===0?body10:null;data&&files.set(name,data.toString("utf8").replace(/<(\/?)[\w.-]+:/g,"<$1"))}return files}var WORDS={lt:"<",gt:">",amp:"&",quot:'"',apos:"'"};function decode2(text3){return text3.replace(/&(lt|gt|amp|quot|apos|#\d+|#x[0-9a-f]+);/gi,(_,e)=>{if(e[0]!=="#")return WORDS[e.toLowerCase()]??"";let code=e[1]==="x"||e[1]==="X"?parseInt(e.slice(2),16):parseInt(e.slice(1),10);return code>=0&&code<=1114111?String.fromCodePoint(code):""})}var textIn=xml=>[...xml.matchAll(/<t(?:\s[^>]*)?>([\s\S]*?)<\/t>/g)].map(m=>decode2(m[1])).join("");function attributes(tag){let out20={};for(let m of tag.matchAll(/([\w:]+)\s*=\s*"([^"]*)"/g))out20[m[1]]=decode2(m[2]);return out20}function columnOf(ref2,next){let letters=/^[A-Z]+/i.exec(ref2??"")?.[0];if(!letters)return next;let n=0;for(let ch of letters.toUpperCase())n=n*26+ch.charCodeAt(0)-64;return n-1}function sheetRows(xml,shared){let rows2=[],total=0;for(let row3 of xml.matchAll(/<row\b[^>]*?(?:\/>|>([\s\S]*?)<\/row>)/g)){if(total+=1,rows2.length>=MAX_ROWS)continue;let cells=[],next=0;for(let c of(row3[1]??"").matchAll(/<c\b([^>]*?)(?:\/>|>([\s\S]*?)<\/c>)/g)){let{r,t}=attributes(c[1]),column=columnOf(r,next);if(next=column+1,column>=MAX_COLUMNS)continue;let inner=c[2]??"",value=/<v(?:\s[^>]*)?>([\s\S]*?)<\/v>/.exec(inner)?.[1],text3=t==="s"?shared[Number(value)]??"":t==="inlineStr"?textIn(inner):decode2(value??"");for(;cells.length<column;)cells.push("");cells[column]=text3}rows2.push(cells)}return{rows:rows2,total}}function readWorkbook(file3){try{let parts2=unzip(file3,n=>n==="xl/workbook.xml"||n==="xl/_rels/workbook.xml.rels"||n==="xl/sharedStrings.xml"||/^xl\/worksheets\/[^/]+\.xml$/.test(n)),book=parts2.get("xl/workbook.xml"),links=parts2.get("xl/_rels/workbook.xml.rels");if(!book||!links)return null;let shared=[...(parts2.get("xl/sharedStrings.xml")??"").matchAll(/<si(?:\s[^>]*)?>([\s\S]*?)<\/si>/g)].map(m=>textIn(m[1])),target=new Map([...links.matchAll(/<Relationship\b[^>]*>/g)].map(m=>{let a=attributes(m[0]);return[a.Id??"",a.Target??""]})),sheets=[];for(let m of[...book.matchAll(/<sheet\b[^>]*>/g)].slice(0,MAX_SHEETS)){let a=attributes(m[0]),to=target.get(a["r:id"]??"")?.replace(/^\/?(xl\/)?/,""),xml=to?parts2.get(`xl/${to}`):void 0;xml&&sheets.push({name:a.name??"",...sheetRows(xml,shared)})}return sheets.length?sheets:null}catch{return null}}function sheetsText(sheets){return sheets.map(s=>[`Sheet "${s.name}", ${s.total} ${s.total===1?"row":"rows"}${s.total>s.rows.length?`, the first ${s.rows.length} shown`:""}:`,...s.rows.map(r=>r.join(" | "))].join(`
1569
+ \u2026 (cut short)`}var JARGON=/\b(sandbox(ed)?|mcp|ports?|pids?|process id|playwright|chromium|chrome|mach|sigtrap|sigkill|sigterm|selectors?|locators?|localhost|127\.0\.0\.1|https?|npx|npm|pnpm|pip|stack ?trace|exit (code|status)|permission denied|eperm|eacces|econnrefused|enoent|bootstrap_check_in|kill(ed)?|shell|bash|zsh|stderr|stdout|headless|seatbelt|cli|api|json|yaml|xpath|url|sudo|chmod|env(ironment)? var(iable)?s?|traceback|exception|undefined|git|config(uration)? files?)\b|\.squad\b|\.env\b|(^|[\s"'(])(~\/|\/(Users|home|private|tmp|var|etc|opt)\/)|[\w.-]+\/[\w./-]*\.(m?[jt]sx?|json|py|ya?ml|toml|sh|md|lock|cfg|ini)\b/i,VARIABLE=/\b[A-Z][A-Z0-9]*_[A-Z0-9_]+\b/;var NOTE_JARGON=new RegExp(JARGON.source.replace("chromium|chrome|","chromium|").replace("traceback|exception|","traceback|")+"|\\b(python3?|docker|curl|wget|node_modules|stderr|devtools)\\b","i"),NOTE_ERROR=/\b[A-Z][a-z]\w*(Error|Exception)\b|\bE(ACCES|PERM|NOENT|ADDRINUSE|ADDRNOTAVAIL|CONNREFUSED|CONNRESET|CONNABORTED|PIPE|TIMEDOUT|EXIST|ISDIR|NOTDIR|NOTEMPTY|MFILE|NOTFOUND|AI_AGAIN|HOSTUNREACH|NETUNREACH|BUSY|INVAL|NOSPC|ROFS)\b/,NOTE_STATUS=/\b(status code|http|exit code|exit status)\s*:?\s*\d{1,3}\b|\berrno\b/i,STACK=/\bat \S+ \(?[^\s()]+:\d+:\d+\)?/;function isPlainNote(sentence){return!NOTE_JARGON.test(sentence)&&!VARIABLE.test(sentence)&&!NOTE_ERROR.test(sentence)&&!NOTE_STATUS.test(sentence)&&!STACK.test(sentence)}function plainParts(text3,technical=()=>!1){let kept2=[],left2=[];for(let s of text3.split(/(?<=[.!?])\s+|\n+/).map(x=>x.trim()).filter(Boolean))(isPlainNote(s)&&!technical(s)?kept2:left2).push(s);return{kept:kept2.join(" "),left:left2.join(" ")}}function within(p,ms2,what){let timer,late2=new Promise((_,reject)=>{timer=setTimeout(()=>reject(new StepFailure("timeout",`${what} took longer than ${Math.round(ms2/1e3)} seconds`)),ms2)});return Promise.race([p,late2]).finally(()=>clearTimeout(timer))}async function openBrowser(signal2,onLine){if(signal2?.aborted)throw new Error(STOPPED7);let launching=shotBrowser().launch(onLine?{onLine}:{}),stopped,onStop=new Promise((_,reject)=>{stopped=()=>reject(new Error(STOPPED7)),signal2?.addEventListener("abort",stopped,{once:!0})});try{return await Promise.race([launching,onStop])}catch(err2){throw signal2?.aborted?(launching.then(b=>b.close()).catch(()=>{}),new Error(STOPPED7)):err2 instanceof BrowserSetupError?new ShotsUnavailable(NO_BROWSER,`${err2.message}: ${err2.detail}`):new ShotsUnavailable(NO_BROWSER,err2.message??String(err2))}finally{stopped&&signal2?.removeEventListener("abort",stopped)}}var BrowserSession=class{constructor(o={}){this.o=o;o.signal?.aborted?this.closed=!0:o.signal?.addEventListener("abort",()=>{this.close()},{once:!0})}o;started=null;closed=!1;browser(){return this.closed?Promise.reject(new Error(STOPPED7)):(this.started||(this.started=openBrowser(this.o.signal,this.o.onLine),this.started.catch(()=>this.started=null)),this.started)}async ready(){try{return await this.browser(),!0}catch{return!1}}async close(){this.closed=!0;let b=this.started;this.started=null,await b?.then(x=>x.close()).catch(()=>{})}};async function open(page,origin,p,ms2){let url=addressIn2(origin,p);if(!url)throw new StepFailure("other",`"${p}" is not a path in the app`);return within(page.goto(url,ms2),ms2+5e3,`opening ${p}`)}function statusWords(status){return status&&status>=400?` The page answered with error status ${status}.`:""}function stillInApp(page,origin){let at3=page.url();if(!(at3!=="about:blank"&&appOrigin(at3)===origin))throw new StepFailure("blocked",`The page is at ${at3}`,at3)}async function snapshotOf(page){try{return trimSnapshot(await within(page.snapshot(),6e3,"reading the page"))}catch{return""}}async function walk2(browser,ask2,c){let stepMs=c.stepMs??STEP_MS,phone=ask2.size==="phone",page=null,at3=0,step2=null;try{let left2=()=>Math.max(1e3,Math.min(stepMs,c.deadline-Date.now()));page=await within(browser.page({origin:c.origin,...c.also?.length?{also:c.also}:{},...SIZES[phone?"phone":"desktop"],phone}),left2(),"opening a page");let status=await open(page,c.origin,ask2.page,left2());stillInApp(page,c.origin);for(let[i,raw]of(ask2.steps??[]).entries()){if(at3=i+1,step2={...raw,...raw.target?{target:oneWay(raw.target)}:{},...raw.action==="wait"&&typeof raw.ms=="number"?{ms:Math.max(0,Math.min(raw.ms,MAX_PAUSE_MS))}:{}},Date.now()>=c.deadline)throw new StepFailure("timeout","The call ran out of time");let ms2=left2();step2.action==="goto"?status=await open(page,c.origin,step2.path,ms2):await within(page.act(step2,ms2),ms2+2e3,stepWords3(step2)),stillInApp(page,c.origin)}return await page.settle(3e3),stillInApp(page,c.origin),{page,status}}catch(err2){if(c.signal?.aborted)throw await page?.close().catch(()=>{}),new Error(STOPPED7);let words2=failureWords(step2,err2,stepMs),where=page?page.url():"";return{page,failed:{step:at3,what:at3&&step2?stepWords3(step2):`open ${ask2.page}`,...words2,page:where&&where!=="about:blank"&&appOrigin(where)===c.origin?pathOf(where):ask2.page,snapshot:page?await snapshotOf(page):"",...err2 instanceof StepFailure&&err2.kind==="blocked"?{left:!0}:{}}}}}async function shootOne(session,ask2,c){let browser=await session.browser(),r=await walk2(browser,ask2,c);try{if("failed"in r)return r.failed;let png=await within(r.page.screenshot(),2e4,"taking the picture");return png?.length?{png,page:pathOf(r.page.url()),...r.status?{status:r.status}:{}}:{step:0,what:"save the picture",why:"the browser gave no picture",plain:"squad-kit could not save the picture.",page:pathOf(r.page.url()),snapshot:""}}catch(err2){if(c.signal?.aborted)throw new Error(STOPPED7);return{step:0,what:"save the picture",...failureWords(null,err2,c.stepMs),page:ask2.page,snapshot:""}}finally{await r.page?.close().catch(()=>{})}}async function lookAt(session,ask2,c){let browser=await session.browser(),r=await walk2(browser,ask2,c);try{return"failed"in r?r.failed:{page:pathOf(r.page.url()),snapshot:await snapshotOf(r.page),...r.status?{status:r.status}:{}}}finally{await r.page?.close().catch(()=>{})}}function writeNew(file3,bytes){let c=fs125.constants,fd;try{fd=fs125.openSync(file3,c.O_WRONLY|c.O_CREAT|c.O_EXCL|(c.O_NOFOLLOW??0),420)}catch(err2){if(err2.code==="EEXIST")return!1;throw err2}try{fs125.writeFileSync(fd,bytes)}finally{fs125.closeSync(fd)}return!0}async function prepareShots(o={}){let mod=shotBrowser();if(!mod.prepare)return;if(o.signal?.aborted)throw new Error(STOPPED7);let timer,onStop,limit=new Promise((_,reject)=>{timer=setTimeout(()=>reject(new BrowserSetupError("Setting up the browser took too long",`It did not finish within ${Math.round((o.ms??SETUP_MS2)/6e4)} minutes`)),o.ms??SETUP_MS2),onStop=()=>reject(new Error(STOPPED7)),o.signal?.addEventListener("abort",onStop,{once:!0})});try{await Promise.race([mod.prepare(o.onLine?{onLine:o.onLine}:{}),limit])}catch(err2){throw o.signal?.aborted?new Error(STOPPED7):err2 instanceof BrowserSetupError?new ShotsUnavailable(NO_BROWSER,`${err2.message}: ${err2.detail}`):new ShotsUnavailable(NO_BROWSER,err2.message??String(err2))}finally{clearTimeout(timer),onStop&&o.signal?.removeEventListener("abort",onStop)}}async function takeOne(url,file3,o={}){let origin=appOrigin(url);if(!origin)throw new ShotsUnavailable(NOT_HERE,`${url} is not an http(s) address on this computer`);await prepareShots(o);let session=new BrowserSession(o),hard=setTimeout(()=>{session.close()},9e4);try{let page=pathOf(url)||"/",r=await shootOne(session,{page:page.startsWith("/")?page:"/"},{origin,stepMs:3e4,deadline:Date.now()+6e4,...o.signal?{signal:o.signal}:{}});if(!("png"in r))throw new ShotsUnavailable(r.plain,`${r.what}: ${r.why}`);fs125.mkdirSync(path115.dirname(file3),{recursive:!0});let part=path115.join(path115.dirname(file3),`.${path115.basename(file3)}.${randomBytes7(6).toString("hex")}.part`);if(!writeNew(part,r.png))throw new ShotsUnavailable("squad-kit could not save the picture.",`${part} is taken`);return fs125.renameSync(part,file3),{page:r.page}}finally{clearTimeout(hard),await session.close()}}function shotsErrorDetail(err2){return err2 instanceof ShotsUnavailable?err2.detail:err2?.message??String(err2)}import fs126 from"fs";import path116 from"path";import{randomBytes as randomBytes8}from"crypto";import zlib2 from"zlib";var MAX_ROWS=200,MAX_COLUMNS=40,MAX_SHEETS=12,MAX_ENTRIES3=2e3,MAX_UNPACKED2=32*1024*1024,END_OF_DIRECTORY=101010256,DIRECTORY_ENTRY=33639248,LOCAL_ENTRY=67324752;function unzip(zip2,wanted){let end=-1;for(let at4=zip2.length-22;at4>=Math.max(0,zip2.length-22-65535);at4-=1)if(zip2.readUInt32LE(at4)===END_OF_DIRECTORY){end=at4;break}if(end<0)throw new Error("not a zip");let count3=zip2.readUInt16LE(end+10);if(count3>MAX_ENTRIES3)throw new Error("too many files");let at3=zip2.readUInt32LE(end+16),files=new Map;for(let i=0;i<count3;i+=1){if(zip2.readUInt32LE(at3)!==DIRECTORY_ENTRY)throw new Error("a broken directory");let method=zip2.readUInt16LE(at3+10),packed=zip2.readUInt32LE(at3+20),nameLength=zip2.readUInt16LE(at3+28),extra=zip2.readUInt16LE(at3+30),comment=zip2.readUInt16LE(at3+32),local=zip2.readUInt32LE(at3+42),name=zip2.toString("utf8",at3+46,at3+46+nameLength);if(at3+=46+nameLength+extra+comment,!wanted(name))continue;if(zip2.readUInt32LE(local)!==LOCAL_ENTRY)throw new Error("a broken entry");let start=local+30+zip2.readUInt16LE(local+26)+zip2.readUInt16LE(local+28),body10=zip2.subarray(start,start+packed),data=method===8?zlib2.inflateRawSync(body10,{maxOutputLength:MAX_UNPACKED2}):method===0?body10:null;data&&files.set(name,data.toString("utf8").replace(/<(\/?)[\w.-]+:/g,"<$1"))}return files}var WORDS={lt:"<",gt:">",amp:"&",quot:'"',apos:"'"};function decode2(text3){return text3.replace(/&(lt|gt|amp|quot|apos|#\d+|#x[0-9a-f]+);/gi,(_,e)=>{if(e[0]!=="#")return WORDS[e.toLowerCase()]??"";let code=e[1]==="x"||e[1]==="X"?parseInt(e.slice(2),16):parseInt(e.slice(1),10);return code>=0&&code<=1114111?String.fromCodePoint(code):""})}var textIn=xml=>[...xml.matchAll(/<t(?:\s[^>]*)?>([\s\S]*?)<\/t>/g)].map(m=>decode2(m[1])).join("");function attributes(tag){let out20={};for(let m of tag.matchAll(/([\w:]+)\s*=\s*"([^"]*)"/g))out20[m[1]]=decode2(m[2]);return out20}function columnOf(ref2,next){let letters=/^[A-Z]+/i.exec(ref2??"")?.[0];if(!letters)return next;let n=0;for(let ch of letters.toUpperCase())n=n*26+ch.charCodeAt(0)-64;return n-1}function sheetRows(xml,shared){let rows2=[],total=0;for(let row3 of xml.matchAll(/<row\b[^>]*?(?:\/>|>([\s\S]*?)<\/row>)/g)){if(total+=1,rows2.length>=MAX_ROWS)continue;let cells=[],next=0;for(let c of(row3[1]??"").matchAll(/<c\b([^>]*?)(?:\/>|>([\s\S]*?)<\/c>)/g)){let{r,t}=attributes(c[1]),column=columnOf(r,next);if(next=column+1,column>=MAX_COLUMNS)continue;let inner=c[2]??"",value=/<v(?:\s[^>]*)?>([\s\S]*?)<\/v>/.exec(inner)?.[1],text3=t==="s"?shared[Number(value)]??"":t==="inlineStr"?textIn(inner):decode2(value??"");for(;cells.length<column;)cells.push("");cells[column]=text3}rows2.push(cells)}return{rows:rows2,total}}function readWorkbook(file3){try{let parts2=unzip(file3,n=>n==="xl/workbook.xml"||n==="xl/_rels/workbook.xml.rels"||n==="xl/sharedStrings.xml"||/^xl\/worksheets\/[^/]+\.xml$/.test(n)),book=parts2.get("xl/workbook.xml"),links=parts2.get("xl/_rels/workbook.xml.rels");if(!book||!links)return null;let shared=[...(parts2.get("xl/sharedStrings.xml")??"").matchAll(/<si(?:\s[^>]*)?>([\s\S]*?)<\/si>/g)].map(m=>textIn(m[1])),target=new Map([...links.matchAll(/<Relationship\b[^>]*>/g)].map(m=>{let a=attributes(m[0]);return[a.Id??"",a.Target??""]})),sheets=[];for(let m of[...book.matchAll(/<sheet\b[^>]*>/g)].slice(0,MAX_SHEETS)){let a=attributes(m[0]),to=target.get(a["r:id"]??"")?.replace(/^\/?(xl\/)?/,""),xml=to?parts2.get(`xl/${to}`):void 0;xml&&sheets.push({name:a.name??"",...sheetRows(xml,shared)})}return sheets.length?sheets:null}catch{return null}}function sheetsText(sheets){return sheets.map(s=>[`Sheet "${s.name}", ${s.total} ${s.total===1?"row":"rows"}${s.total>s.rows.length?`, the first ${s.rows.length} shown`:""}:`,...s.rows.map(r=>r.join(" | "))].join(`
1570
1570
  `)).join(`
1571
1571
 
1572
1572
  `)}var APP_TOOLS=["take_screenshots","look_at_page","start_app","stop_app","app_status"],ALL_TOOLS=[...APP_TOOLS,"run_checks","i_am_stuck"];var MAX_SHOTS_PER_CALL=10,REFUSALS_TO_STOP=3,MISTAKES_TO_STOP=15,MAX_STUCK=5,MAX_IMAGE_BYTES=45e5,MAX_IMAGE_BYTES_SENT=16e6;function isPng(b){return b.length>8&&b.subarray(0,8).equals(Buffer.from([137,80,78,71,13,10,26,10]))}var STUCK_KEEP=2e3,HUGE=2e4;function tooManyWords(who4){return`The ${who4} kept asking for things it may not do, so squad-kit stopped it.`}function tooManyMistakesWords(who4){return`The ${who4} kept using squad-kit's tools the wrong way, so squad-kit ended this step. Press Try again to try once more.`}var CHECK_NAMES=["test","lint","build"],CHECK_WORDS={test:"the tests",lint:"the code check",build:"the build"},STR2={type:"string"},EXAMPLES={target:'{ "role": "button", "name": "Book" }',step:'{ "action": "fill", "target": { "label": "Date" }, "value": "2026-12-03" }',look:'{ "page": "/book" }',shot:'{ "page": "/book", "steps": [{ "action": "click", "target": { "role": "button", "name": "Book" } }], "size": "phone" }',shots:'{ "shots": [{ "page": "/book", "steps": [{ "action": "fill", "target": { "label": "Date" }, "value": "2026-12-03" }, { "action": "click", "target": { "role": "button", "name": "Book" } }, { "action": "wait", "target": { "text": "Booked" } }], "size": "phone" }] }',stuck:'{ "what": "The app does not start", "why": "start_app said the address is already in use" }',checks:'{ "which": "test" }',only:'{ "only": "tests/e2e/booking.spec.ts:42" }'},TARGET={type:"object",description:`The part of the page, named one way inside this object, never on the step itself: role and name (best), label, text, placeholder, testId or css. Example: ${EXAMPLES.target}.`,properties:{role:STR2,name:STR2,label:STR2,text:STR2,placeholder:STR2,testId:STR2,css:STR2},additionalProperties:!1},STEPS3={type:"array",maxItems:MAX_STEPS,description:`What to do on the page first, in order. Each step has an "action" and names its part of the page inside "target". goto (path), click, fill (value), select (value), check, uncheck, press (key, like "Enter"), hover, wait (target, or ms up to 5000). Example: [${EXAMPLES.step}, { "action": "click", "target": ${EXAMPLES.target} }].`,items:{type:"object",properties:{action:{type:"string",enum:["goto","click","fill","select","check","uncheck","press","hover","wait"]},target:TARGET,value:{type:"string",maxLength:MAX_VALUE},path:{type:"string",maxLength:MAX_PAGE,description:'For goto only: a path in the app, like "/rooms".'},key:STR2,ms:{type:"integer",minimum:0,maximum:5e3}},required:["action"],additionalProperties:!1}},PAGE={type:"string",maxLength:MAX_PAGE,description:'The page to open: a path in the app, starting with "/", like "/book". The field is "page" (not "path" or "url"), never a full address.'},SIZE={type:"string",enum:["desktop","phone"],description:"desktop is 1280x800 (the default), phone is 390x844."},NONE={type:"object",properties:{},additionalProperties:!1},API_METHODS=["GET","POST","PUT","PATCH","DELETE"],MAX_API_CALLS=20,MAX_API_ANSWER=100*1024,MAX_API_FILE=50*1024*1024,MAX_API_BODY=100*1024,API_WAIT_MS=6e4,API_TEXT=/^(text\/|application\/(json|xml|problem\+json|[\w.+-]*\+json|[\w.+-]*\+xml|javascript|x-www-form-urlencoded))/i,API_FILE_TYPES={"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":"xlsx","application/vnd.ms-excel":"xls","application/pdf":"pdf","application/zip":"zip","application/vnd.openxmlformats-officedocument.wordprocessingml.document":"docx"},DEFS={take_screenshots:{description:`squad-kit opens its own browser on this story's app (it starts the app if needed), does the steps, and takes one picture per shot, after its steps. The pictures come back to you, with each file name and the page it ended on, and are saved in the proof folder. Limits: at most ${MAX_SHOTS_PER_CALL} shots in one call, and at most ${40} pictures in all in this step, counting each one you take again, so plan one picture per part or case of each "done when". Asking for more is refused, and refused requests can stop the step. Look at each picture and take a wrong one again. Each answer lists every picture you took in this step: name those files in your final answer, no others. Example: ${EXAMPLES.shots}`,inputSchema:{type:"object",properties:{shots:{type:"array",minItems:1,maxItems:MAX_SHOTS_PER_CALL,items:{type:"object",properties:{page:PAGE,steps:STEPS3,size:SIZE},required:["page"],additionalProperties:!1}}},required:["shots"],additionalProperties:!1}},look_at_page:{description:`squad-kit opens a page of this story's app in its own browser, does the steps, and gives back the address it ended at and the page's accessibility tree (its roles, names and text), cut short. Use it to find the names of fields and buttons. Example: ${EXAMPLES.look}, or with steps: { "page": "/", "steps": [{ "action": "click", "target": ${EXAMPLES.target} }] }`,inputSchema:{type:"object",properties:{page:PAGE,steps:STEPS3,size:SIZE},required:["page"],additionalProperties:!1}},start_app:{description:"squad-kit starts this story's own copy of the app with the project's own start command, on a free port when the usual one is taken, and gives back the address and the last lines it printed. It takes no fields. Example: {}",inputSchema:NONE},stop_app:{description:"squad-kit stops the app it started for this step. It stops only what squad-kit started. It takes no fields. Example: {}",inputSchema:NONE},app_status:{description:"Whether the app squad-kit started for this step runs and answers, its address, and the last lines it printed. It takes no fields. Example: {}",inputSchema:NONE},run_checks:{description:`squad-kit runs the project's own saved checks in this story's copy of the code, inside its own guard, where a browser can start: browser tests work here, not in your shell. "which" picks one (test, lint or build); left out, all of them run, one after another. "only" runs one test file on its own with the project's own test tool, much faster than all the tests: use it while you fix a test, then squad-kit runs all the checks again at the end. It gives back pass or fail for each, with the last lines it printed, and runs each once: a failure is not run again. It runs only the saved commands and takes no command of yours. A long run goes on in the background, and calling this again gives its result. Example: ${EXAMPLES.checks}, or ${EXAMPLES.only}`,inputSchema:{type:"object",properties:{which:{type:"string",enum:CHECK_NAMES,description:"Which check to run. Left out, all of them."},only:{type:"string",description:"One test file in this copy to run on its own, as the test output names it, with its line for a browser test (tests/e2e/booking.spec.ts:42). Only the tests run then."}},additionalProperties:!1}},ask_api:{description:`squad-kit asks the running product's API for you, on this computer, and saves exactly what was asked and answered. Use it for a "done when" no page of the app shows. It has no field for a host: it only reaches the parts of this product. Name the saved file in your answer like a picture. At most ${MAX_API_CALLS} calls in a step.`,inputSchema:{type:"object",properties:{part:{type:"string",description:"The id of the part to ask."},method:{type:"string",enum:API_METHODS,description:"GET unless said otherwise."},path:{type:"string",description:"The path on that part, with any query, like /price?item=3."},body:{description:"What to send, for POST, PUT or PATCH: text, or an object sent as JSON."},headers:{type:"object",description:"More headers, when the part needs them.",additionalProperties:{type:"string"}}},required:["part","path"],additionalProperties:!1}},add_part:{description:"Call this when the story needs a part of the product it has no copy of yet, to read it or change it. squad-kit adds that part's copy to this story's folder, on the same branch name, and says where. The person is not asked.",inputSchema:{type:"object",properties:{part:{type:"string",description:"The id of the part."}},required:["part"],additionalProperties:!1}},i_am_stuck:{description:`Call this when something technical is in your way: the app does not start, a port is busy, the browser does not work, a command or a file is refused. squad-kit tries its known fixes (its browser, a free port, starting the app again) and says what it did. Never ask the person about it. Example: ${EXAMPLES.stuck}`,inputSchema:{type:"object",properties:{what:{type:"string",description:"What does not work."},why:{type:"string",description:"What you saw: the error or the output."}},required:["what","why"],additionalProperties:!1}}};function phoneWords(labels){let sizes=Object.keys(labels).map(k=>`${labels[k]} (${k})`);return sizes.length?`a real ${sizes.join(" or ")}`:"a real phone"}var PHONE_SHOTS='{ "shots": [{ "page": "/", "steps": [{ "action": "fill", "target": { "label": "Name" }, "value": "Sara" }, { "action": "click", "target": { "role": "button", "name": "Sign up" } }, { "action": "wait", "target": { "text": "Welcome Sara" } }], "size": "phone", "language": "ar" }] }';function phoneDefs(sizes,labels,links){let size2={type:"string",enum:sizes,description:sizes.map(k=>`${k} is the ${labels[k]??k}`).join(", ")+(sizes.length>1?" (phone is the default).":".")},page={type:"string",maxLength:MAX_PAGE,description:links.length?`The screen to open: "/" is the app's start screen; another path opens by the app's own link (${links[0]}://\u2026), like "/book".`:`"/", the app's start screen. This app has no links of its own, so every shot starts there: use steps to reach the screen you need.`},language={type:"string",pattern:"^[a-z]{2}$",description:'The language the app opens in: "en" (the default) or "ar", or another two-letter code.'},steps={...STEPS3,description:`What to do on the screen first, in order, by the words on it. Each step has an "action" and names its part of the screen inside "target", by its words: role and name (like a button's words), label, text or placeholder; or testId, its id. A phone screen has no css. click (taps), fill (value; it replaces the field's text), select (value: the choice to tap after it), press (key: Enter, Backspace, Escape to put the keyboard away, a letter or digit), hover (scrolls down until the part shows), wait (target, or ms up to 5000). Example: [{ "action": "fill", "target": { "label": "Name" }, "value": "Sara" }, { "action": "click", "target": { "role": "button", "name": "Sign up" } }].`},where=phoneWords(labels);return{take_screenshots:{description:`squad-kit opens this story's app on ${where}: an iPhone simulator it runs itself, hidden, on this computer. Each shot starts the app fresh in its language, does the steps by tapping through the app by the words on the screen, and takes one picture after its steps. The pictures come back to you, with each file name, the screen and the device, and are saved in the proof folder. Limits: at most ${MAX_SHOTS_PER_CALL} shots in one call, and at most ${40} pictures in all in this step, counting each one you take again, so plan one picture per part or case of each "done when". Asking for more is refused, and refused requests can stop the step. Look at each picture and take a wrong one again. Each answer lists every picture you took in this step: name those files in your final answer, no others. Example: ${PHONE_SHOTS}`,inputSchema:{type:"object",properties:{shots:{type:"array",minItems:1,maxItems:MAX_SHOTS_PER_CALL,items:{type:"object",properties:{page,steps,size:size2,language},required:["page"],additionalProperties:!1}}},required:["shots"],additionalProperties:!1}},look_at_page:{description:`squad-kit opens this story's app on ${where}, does the steps, and gives back what the screen shows as text: the words of each part, one a line, with what a field holds. Use it to find the words on fields and buttons. It saves no picture. Example: { "page": "/" }, or with steps: { "page": "/", "steps": [{ "action": "click", "target": { "text": "Sign up" } }], "language": "ar" }`,inputSchema:{type:"object",properties:{page,steps,size:size2,language},required:["page"],additionalProperties:!1}}}}var RUN_CHECKS_BY_PART={...DEFS.run_checks,inputSchema:{type:"object",properties:{which:{type:"string",enum:CHECK_NAMES,description:"Which check to run. Left out, all of them."},part:{type:"string",description:"The id of the part to run them in. There is no other way to choose a folder."}},required:["part"],additionalProperties:!1}},MAX_ONLY=300,text=t=>({type:"text",text:t}),ok=t=>({content:[text(t)]}),error=t=>({content:[text(t)],isError:!0});function extraFields(args,allowed){return Object.keys(args).filter(k=>!allowed.includes(k))}function hugeValue(v,max,depth=0){return typeof v=="string"?v.length>max:depth>6||!v||typeof v!="object"?!1:Object.values(v).some(x=>hugeValue(x,max,depth+1))}function outsideApp(p){if(typeof p!="string")return null;let s=p.trim();if(/^[a-z][a-z0-9+.-]*:/i.test(s)||s.startsWith("//")||s.includes("\\"))return`page "${s.slice(0,80)}" is outside the app`;let decoded=s;try{decoded=decodeURIComponent(s)}catch{return`page "${s.slice(0,80)}" is not a valid path`}return decoded.startsWith("//")||decoded.includes("\\")||/^[a-z][a-z0-9+.-]*:/i.test(decoded)||/(^|[/\\])\.\.([/\\?#]|$)/.test(decoded)?`page "${s.slice(0,80)}" goes outside the app`:null}function gotoPaths(steps){return Array.isArray(steps)?steps.filter(x=>x&&typeof x=="object"&&x.action==="goto").map(x=>x.path):[]}var TARGET_KEYS=["role","name","label","text","placeholder","testId","css"],STEP_KEYS=["action","target","value","path","key","ms"],ACTION_KEYS=["goto","click","fill","select","check","uncheck","press","hover","wait"],COMMAND_FIELD=/^(commands?|cmd|scripts?|exec|execute|shell|bash|sh|eval|javascript|program)$/i;function commandField(v,depth=0){if(depth>6||!v||typeof v!="object")return null;for(let[k,x]of Object.entries(v)){if(COMMAND_FIELD.test(k))return k;let inner=commandField(x,depth+1);if(inner)return inner}return null}function pageCall(raw,tool5,more3=[]){let ask2={...raw},example2=tool5==="look_at_page"?EXAMPLES.look:EXAMPLES.shot;for(let alias of["path","url"])if(ask2[alias]!==void 0){if(ask2.page!==void 0)return{wrong:`it got both "page" and "${alias}"`,help:`Give the page once, as "page": ${EXAMPLES.look}.`};ask2.page=ask2[alias],delete ask2[alias]}let extra=Object.keys(ask2).find(k=>!["page","steps","size",...more3].includes(k));if(extra)return{wrong:`${tool5==="look_at_page"?"look_at_page":"a shot"} takes page, steps${more3.length?", size and language":" and size"}, and got "${short4(extra,40)}"`,help:`Use: ${example2}.`};if(ask2.steps===void 0||ask2.steps===null)return{ask:ask2};if(!Array.isArray(ask2.steps))return{wrong:"steps is not a list",help:`Use: "steps": [${EXAMPLES.step}].`};let steps=[];for(let[i,s]of ask2.steps.entries()){let fixed=stepCall(s,i+1);if("wrong"in fixed)return fixed;steps.push(fixed.step)}return{ask:{...ask2,steps}}}function stepCall(raw,n){if(!raw||typeof raw!="object"||Array.isArray(raw))return{wrong:`step ${n} is not an object`,help:`A step looks like ${EXAMPLES.step}.`};let s={...raw},named=s.action===void 0?Object.keys(s).find(k=>ACTION_KEYS.includes(k)):void 0;if(named){let value=named==="fill"||named==="select"?', "value": "Sprint review"':"";return{wrong:`step ${n} names its action as a field ("${named}")`,help:`Give it as "action", with the part of the page inside "target": { "action": "${named}", "target": { "role": "textbox", "name": "Title" }${value} }.`}}let loose=TARGET_KEYS.filter(k=>s[k]!==void 0);if(loose.length){if(s.target!==void 0)return{wrong:`step ${n} has a target and also ${loose.map(k=>`"${k}"`).join(", ")} on the step itself`,help:`Put them all inside "target": ${EXAMPLES.step}.`};s.target=Object.fromEntries(loose.map(k=>[k,s[k]]));for(let k of loose)delete s[k]}let extra=Object.keys(s).find(k=>!STEP_KEYS.includes(k));if(extra){let help=["for","until","selector","element"].includes(extra)?'Name what to wait for or act on inside "target": { "action": "wait", "target": { "text": "Booked" } }.':`A step takes only these fields. Use: ${EXAMPLES.step}.`;return{wrong:`step ${n} takes action, target, value, path, key and ms, and got "${short4(extra,40)}"`,help}}let t=s.target;if(t&&typeof t=="object"&&!Array.isArray(t)){let bad2=Object.keys(t).find(k=>!TARGET_KEYS.includes(k));if(bad2)return{wrong:`the target of step ${n} takes role and name, label, text, placeholder, testId or css, and got "${short4(bad2,40)}"`,help:`Use: "target": ${EXAMPLES.target}.`}}return{step:s}}var ABOUT_BROWSER=/\b(browser|chrom(e|ium)|playwright|puppeteer|headless|screenshots?|sandbox|seatbelt|mach|bootstrap_check_in|sigtrap)\b/i,ABOUT_PORT=/\b(ports?|eaddrinuse|address (already )?in use|already in use|listen(ing)?)\b/i,ABOUT_APP=/\b(app|server|start(ed|ing)?|not (answer|respond)ing|connection refused|econnrefused|refused to connect|crash(ed)?|down)\b/i,ABOUT_SITE=/not_allowlisted|on the deny list|deny network-outbound|connect tunnel failed|network allowlist|not allowed for this project|\b(site|host|domain|website|download)s? (is |was |were )?(not allowed|blocked|closed)\b/i,SITE_CLOSED="That site is closed for this project on purpose, and squad-kit does not open it: nothing here is broken. Do not try to reach it another way, and never ask the person to open it. Get what you need from an official package registry or from what is already in this copy, or go on without it.",STOPPED8="You stopped the squad.",STOPPED_CALL="You were stopped. squad-kit takes no more calls in this run.",STILL_STARTING="The app is still starting. Call app_status in a moment.",STOPPED_FOR_CHECKS="squad-kit stopped the app it started for you, so the tests could start their own. Call start_app again when you need it.",STOPPED_FOR_CHECKS_STATUS="The app is not running: squad-kit stopped it before the checks ran, so the tests could start their own. Call start_app when you need it.",BROWSER_STARTING="squad-kit is still setting up its screenshot browser. Call this again in a moment; do not start a browser yourself.",PHONE_STARTING="squad-kit is still getting its phone ready (it builds the app and starts the phone). Call this again in a moment; do not start a simulator yourself.",WAIT_SHARE=.7,SHOOT_SHARE=.9,START_SHARE=.95,by=(c,share2)=>c.at+Math.round(c.ms*share2);function settleBy(p,at3,signal2){return new Promise(resolve2=>{let timer,done2=v=>{clearTimeout(timer),signal2.removeEventListener("abort",onStop),resolve2(v)},onStop=()=>done2("late");timer=setTimeout(()=>done2("late"),Math.min(2**31-1,Math.max(0,at3-Date.now()))),signal2.addEventListener("abort",onStop,{once:!0}),signal2.aborted&&onStop(),p.then(ok2=>done2({ok:ok2}),err2=>done2({err:err2}))})}var AgentWindow=class{constructor(s){this.s=s;this.app=null,this.hold(s.running??null),this.signal=s.signal?AbortSignal.any([s.signal,this.ctl.signal]):this.ctl.signal,this.session=new BrowserSession(this.sessionOptions())}s;key=randomBytes8(24).toString("hex");refusals=[];mistakes=[];stuck=[];pictures=new Map;apiCalls=0;imageBytesSent=0;app;starting=null;startFailed=null;waitsFor=null;leaveCopy=null;stoppedForChecks=!1;session;closed=!1;ctl=new AbortController;signal;checksJob=null;queue=Promise.resolve();reserved=0;stopped=!1;onTooMany=[];sessionOptions(){return{signal:this.signal,onLine:t=>this.s.say(t)}}get ticket(){return this.s.ticket}get step(){return this.s.step}get tools(){return this.s.tools}whenTooMany(fn){this.onTooMany.push(fn)}get tooMany(){return this.refusals.length>=REFUSALS_TO_STOP}get tooManyMistakes(){return this.mistakes.length>MISTAKES_TO_STOP}get isClosed(){return this.closed}get over(){return this.closed||this.stopped||this.signal.aborted}pictureList(){return this.pictures.size?`The pictures you took in this step. In your final answer, name only these files:
@@ -1589,7 +1589,7 @@ The last lines it printed:
1589
1589
  ${this.app.lines(15)}`:""}`)}async checksInTurn(args,clock4){let dir=this.checksJob?null:this.checksDir(args);if(!dir)return this.runChecks(args,clock4);let turn2;try{turn2=await takeTurn(dir,"checks",{signal:this.signal,waitMs:Math.max(0,by(clock4,WAIT_SHARE)-Date.now())})}catch(err2){if(this.over)return error(STOPPED_CALL);if(err2 instanceof TurnTimeout)return ok(`${waitWords(err2.holder,"checks")} Call run_checks again in a moment.`);throw err2}try{let mine2=this.app&&!this.app.ended?.()?this.app:null;this.hasChecks(dir)&&await stopAppsIn(dir);let told=!!(mine2&&this.app!==mine2&&this.stoppedForChecks),r=await this.runChecks(args,clock4),job=this.checksJob;return job?job.done.then(turn2.release,turn2.release):turn2.release(),told?{...r,content:[text(STOPPED_FOR_CHECKS),...r.content]}:r}catch(err2){throw turn2.release(),err2}}hasChecks(dir){let whole=this.s.checks,parts2=whole?.parts?.(),commands=((parts2?Object.values(parts2).find(p=>p.cwd===dir):void 0)??whole)?.commands()??{};return CHECK_NAMES.some(n=>commands[n]?.trim())}checksDir(args){let whole=this.s.checks;if(!whole)return null;let parts2=whole.parts?.(),part=typeof args.part=="string"?args.part.trim():"";return(parts2?parts2[part]??Object.values(parts2)[0]??whole:whole).cwd}async runChecks(args,clock4){let byPart=!!this.s.checks?.parts,extra=extraFields(args,byPart?["which","only","part"]:["which","only"]);if(extra.length)return this.mistake("run_checks",`run_checks takes only ${byPart?'"which", "only" and "part"':'"which" and "only"'}, and got "${short4(extra[0],40)}"`,`Use: ${EXAMPLES.checks}, ${EXAMPLES.only}, or {} for all of them.`);let which=args.which;if(which!==void 0&&!CHECK_NAMES.includes(which))return this.mistake("run_checks",'which is "test", "lint" or "build"',`Use: ${EXAMPLES.checks}, or {} for all of them.`);if(args.only!==void 0&&(typeof args.only!="string"||!args.only.trim()||args.only.length>MAX_ONLY||/[\n\r\0]/.test(args.only)))return this.mistake("run_checks","only is one test file of this copy, as text",`Use: ${EXAMPLES.only}.`);let only=typeof args.only=="string"?args.only.trim():void 0;if(only&&which&&which!=="test")return this.mistake("run_checks",'only names a test file, so which is "test" or left out',`Use: ${EXAMPLES.only}.`);let whole=this.s.checks;if(!whole)return ok("This step has no checks squad-kit can run.");let parts2=whole.parts?.(),part=typeof args.part=="string"?args.part.trim():"";if(parts2&&Object.keys(parts2).length>1&&!part)return this.mistake("run_checks","part is missing, and this story has several",`Say which: ${Object.keys(parts2).join(", ")}.`);if(parts2&&part&&!parts2[part])return this.mistake("run_checks",`this story has no copy of the part "${short4(part,40)}"`,`Its parts: ${Object.keys(parts2).join(", ")}. Call add_part for another.`);let c=parts2?parts2[part]??Object.values(parts2)[0]??whole:whole,file3=only?path116.normalize(only.replace(/:\d+$/,"")):"";if(only&&!this.checksJob&&(path116.isAbsolute(file3)||file3.startsWith("..")||!fs126.existsSync(path116.join(c.cwd,file3))))return this.mistake("run_checks",`this story's copy has no file "${short4(only,80)}"`,`Name a test file inside this copy, as the test output names it. Use: ${EXAMPLES.only}.`);let job=this.checksJob??this.startChecks(c,only?"test":which,only);if(!job)return ok(`The project has no saved ${which||only?`${which??"test"} command`:"test, lint or build command"} yet. Add the script to the project's own files (package.json and the like): squad-kit finds it there, then call this again.`);let r=await settleBy(job.done,by(clock4,START_SHARE),this.signal);if(this.over)return error(STOPPED_CALL);if(r==="late")return ok("The checks are still running. Call run_checks again in a moment for the result; it does not start them twice.");if(this.checksJob=null,"err"in r)return error(`squad-kit could not run the checks: ${r.err?.message??String(r.err)}. Call i_am_stuck if it keeps happening.`);let asked=only?`only ${only}`:which,earlier=job.asked!==asked?`This is the result of the run you started before it (${job.asked??"all the checks"}). Call again for the one you asked for now.
1590
1590
  `:"";return ok(`${earlier}${r.ok.map(checkAnswer).join(`
1591
1591
 
1592
- `)}`)}healed(detail){this.s.log?.(detail),this.s.say(HEALED_LINE)}startChecks(c,which,only){let commands=c.commands(),names2=(which?[which]:CHECK_NAMES).filter(n=>commands[n]?.trim());if(!names2.length)return null;let done2=(async()=>{await this.s.beforeStart?.().catch(()=>{});let noted=await c.onRun?.({names:names2,...only?{only}:{}}).catch(()=>{}),out20=[];for(let name of names2){if(this.over)break;let saved=commands[name].trim(),alone=only?fileAlone(c.cwd,saved,only):null,command=alone?.command??saved,note2=only&&!alone?"squad-kit cannot run one test file on its own with this project's tests, so it ran all of them.":void 0;c.onStart?.(name),this.s.log?.(`squad-kit runs ${alone?`${only} on its own`:name} for the ${this.s.who} on ${this.s.ticket}: ${command}`);let r=await runCommand(c.cwd,command,void 0,{silentMs:CHECK_SILENT_MS,signal:this.signal,onOutput:()=>this.s.beat?.(),onHealed:detail=>this.healed(detail),...c.guard?{guard:c.guard}:{}});if(this.over)break;out20.push({name,command,...r,...alone?{only}:{},...note2?{note:note2}:{}}),this.s.say(`squad-kit ran ${alone?"one test file":CHECK_WORDS[name]} for the ${this.s.who}: ${r.ok?"passed":"did not pass"}`)}return noted&&!this.over&&out20.length===names2.length&&await noted(out20).catch(()=>{}),out20})();done2.catch(()=>{});let asked=only?`only ${only}`:which,job={...asked?{asked}:{},done:done2};return this.checksJob=job,job}async askApi(args,clock4){let example2='{ "part": "api", "method": "GET", "path": "/price?item=3" }',extra=extraFields(args,["part","method","path","body","headers"]);if(extra.length)return this.refuse("ask_api",`ask_api has no field "${short4(extra[0],40)}"`,`The ${this.s.who} asked the API in a way squad-kit does not offer. squad-kit refused.`);let parts2=this.s.api?.()??{};if(!Object.keys(parts2).length)return ok("This step has no API squad-kit can ask.");let id2=typeof args.part=="string"?args.part.trim():"",part=parts2[id2];if(!part)return this.mistake("ask_api",id2?`this product has no part "${short4(id2,40)}"`:"part is missing",`Its parts: ${Object.keys(parts2).join(", ")}. Use: ${example2}.`);let method=typeof args.method=="string"?args.method.toUpperCase():"GET";if(!API_METHODS.includes(method))return this.mistake("ask_api",`method is one of ${API_METHODS.join(", ")}`,`Use: ${example2}.`);let asked=typeof args.path=="string"?args.path.trim():"",origin,target;try{origin=new URL(part.url).origin,target=new URL(asked,origin)}catch{return this.mistake("ask_api","path is not a path on this part",`Use: ${example2}.`)}if(!asked.startsWith("/")||asked.startsWith("//")||target.origin!==origin)return this.refuse("ask_api",`the path "${short4(asked,80)}" leaves ${origin}`,`The ${this.s.who} asked to reach another address with the API tool. squad-kit refused.`);if(this.apiCalls>=MAX_API_CALLS)return this.mistake("ask_api",`this step has made its ${MAX_API_CALLS} calls`,"Hand in what you have, and put the rest in offScreen with why.");let body10=args.body===void 0||args.body===null?void 0:typeof args.body=="string"?args.body:JSON.stringify(args.body);if(body10&&Buffer.byteLength(body10)>MAX_API_BODY)return this.mistake("ask_api","body is too long","Send less.");if(body10&&method==="GET")return this.mistake("ask_api","a GET sends no body",`Use another method, or put it in the path. ${example2}.`);let headers={};for(let[k,v]of Object.entries(args.headers??{})){if(typeof v!="string"||/^(host|connection|content-length|transfer-encoding)$/i.test(k)||/[\r\n]/.test(k+v))return this.mistake("ask_api",`the header "${short4(k,40)}" cannot be sent`,"Leave it out.");headers[k]=v}body10&&!Object.keys(headers).some(k=>/^content-type$/i.test(k))&&(headers["content-type"]=typeof args.body=="string"?"text/plain":"application/json");let up=await this.ensureApp(by(clock4,START_SHARE));if("fail"in up)return up.fail;this.apiCalls+=1;let n=this.apiCalls,res;try{res=await fetch(target,{method,headers,redirect:"manual",...body10?{body:body10}:{},signal:AbortSignal.any([this.signal,AbortSignal.timeout(API_WAIT_MS)])})}catch(err2){return this.over?error(STOPPED_CALL):error(`${part.name} did not answer ${method} ${asked}: ${err2.message}. Is that the right address on it? Call i_am_stuck if it keeps happening.`)}let contentType=res.headers.get("content-type")??"",type=contentType.split(";")[0].trim().toLowerCase(),bytes=Buffer.from(await res.arrayBuffer()),text3=!type||API_TEXT.test(type),stem2=`${this.s.prefix??this.s.ticket.toLowerCase()}-api-${n}`,record={part:part.name,method,path:asked,...body10?{body:body10.slice(0,MAX_API_ANSWER)}:{},status:res.status,contentType};if(text3){let answer2=bytes.subarray(0,MAX_API_ANSWER).toString("utf8");record.answer=answer2,bytes.length>MAX_API_ANSWER&&(record.cut=!0)}else{let own2=API_FILE_TYPES[type]??"bin",name=`${stem2}-answer.${own2}`;if(bytes.length>MAX_API_FILE)return error(`${part.name} answered with a file of ${bytes.length} bytes, too big to keep. Say so in your answer.`);fs126.writeFileSync(path116.join(this.s.proofDir,name),bytes),record.file=name,record.bytes=bytes.length;let sheets2=own2==="xlsx"?readWorkbook(bytes):null;sheets2&&(record.sheets=sheets2)}let file3=`${stem2}.api.json`;fs126.writeFileSync(path116.join(this.s.proofDir,file3),JSON.stringify(record,null,2)),this.pictures.set(file3,{file:file3,page:"",size:"answer",call:`${method} ${asked} on ${part.name}`}),this.s.say(`squad-kit asked ${part.name} for the ${this.s.who}, and kept the answer`);let sheets=record.sheets,kept2=`A file of ${bytes.length} bytes (${type}), saved as ${record.file}. squad-kit keeps it for the person to open.`,shown2=text3?String(record.answer).slice(0,8e3):sheets?`${kept2}
1592
+ `)}`)}healed(detail){this.s.log?.(detail),this.s.say(HEALED_LINE)}startChecks(c,which,only){let commands=c.commands(),names2=(which?[which]:CHECK_NAMES).filter(n=>commands[n]?.trim());if(!names2.length)return null;let done2=(async()=>{await this.s.beforeStart?.().catch(()=>{});let noted=await c.onRun?.({names:names2,...only?{only}:{}}).catch(()=>{}),out20=[];for(let name of names2){if(this.over)break;let saved=commands[name].trim(),alone=only?fileAlone(c.cwd,saved,only):null,command=alone?.command??saved,note2=only&&!alone?"squad-kit cannot run one test file on its own with this project's tests, so it ran all of them.":void 0;c.onStart?.(name),this.s.log?.(`squad-kit runs ${alone?`${only} on its own`:name} for the ${this.s.who} on ${this.s.ticket}: ${command}`);let r=await runCommand(c.cwd,command,void 0,{silentMs:CHECK_SILENT_MS,signal:this.signal,onOutput:()=>this.s.beat?.(),onHealed:detail=>this.healed(detail),...c.guard?{guard:c.guard}:{}});if(this.over)break;out20.push({name,command,...r,...alone?{only}:{},...note2?{note:note2}:{}}),this.s.say(`squad-kit ran ${alone?"one test file":CHECK_WORDS[name]} for the ${this.s.who}: ${r.ok?"passed":"did not pass"}`),c.onEnd?.(name,r.ok,!!alone)}return noted&&!this.over&&out20.length===names2.length&&await noted(out20).catch(()=>{}),out20})();done2.catch(()=>{});let asked=only?`only ${only}`:which,job={...asked?{asked}:{},done:done2};return this.checksJob=job,job}async askApi(args,clock4){let example2='{ "part": "api", "method": "GET", "path": "/price?item=3" }',extra=extraFields(args,["part","method","path","body","headers"]);if(extra.length)return this.refuse("ask_api",`ask_api has no field "${short4(extra[0],40)}"`,`The ${this.s.who} asked the API in a way squad-kit does not offer. squad-kit refused.`);let parts2=this.s.api?.()??{};if(!Object.keys(parts2).length)return ok("This step has no API squad-kit can ask.");let id2=typeof args.part=="string"?args.part.trim():"",part=parts2[id2];if(!part)return this.mistake("ask_api",id2?`this product has no part "${short4(id2,40)}"`:"part is missing",`Its parts: ${Object.keys(parts2).join(", ")}. Use: ${example2}.`);let method=typeof args.method=="string"?args.method.toUpperCase():"GET";if(!API_METHODS.includes(method))return this.mistake("ask_api",`method is one of ${API_METHODS.join(", ")}`,`Use: ${example2}.`);let asked=typeof args.path=="string"?args.path.trim():"",origin,target;try{origin=new URL(part.url).origin,target=new URL(asked,origin)}catch{return this.mistake("ask_api","path is not a path on this part",`Use: ${example2}.`)}if(!asked.startsWith("/")||asked.startsWith("//")||target.origin!==origin)return this.refuse("ask_api",`the path "${short4(asked,80)}" leaves ${origin}`,`The ${this.s.who} asked to reach another address with the API tool. squad-kit refused.`);if(this.apiCalls>=MAX_API_CALLS)return this.mistake("ask_api",`this step has made its ${MAX_API_CALLS} calls`,"Hand in what you have, and put the rest in offScreen with why.");let body10=args.body===void 0||args.body===null?void 0:typeof args.body=="string"?args.body:JSON.stringify(args.body);if(body10&&Buffer.byteLength(body10)>MAX_API_BODY)return this.mistake("ask_api","body is too long","Send less.");if(body10&&method==="GET")return this.mistake("ask_api","a GET sends no body",`Use another method, or put it in the path. ${example2}.`);let headers={};for(let[k,v]of Object.entries(args.headers??{})){if(typeof v!="string"||/^(host|connection|content-length|transfer-encoding)$/i.test(k)||/[\r\n]/.test(k+v))return this.mistake("ask_api",`the header "${short4(k,40)}" cannot be sent`,"Leave it out.");headers[k]=v}body10&&!Object.keys(headers).some(k=>/^content-type$/i.test(k))&&(headers["content-type"]=typeof args.body=="string"?"text/plain":"application/json");let up=await this.ensureApp(by(clock4,START_SHARE));if("fail"in up)return up.fail;this.apiCalls+=1;let n=this.apiCalls,res;try{res=await fetch(target,{method,headers,redirect:"manual",...body10?{body:body10}:{},signal:AbortSignal.any([this.signal,AbortSignal.timeout(API_WAIT_MS)])})}catch(err2){return this.over?error(STOPPED_CALL):error(`${part.name} did not answer ${method} ${asked}: ${err2.message}. Is that the right address on it? Call i_am_stuck if it keeps happening.`)}let contentType=res.headers.get("content-type")??"",type=contentType.split(";")[0].trim().toLowerCase(),bytes=Buffer.from(await res.arrayBuffer()),text3=!type||API_TEXT.test(type),stem2=`${this.s.prefix??this.s.ticket.toLowerCase()}-api-${n}`,record={part:part.name,method,path:asked,...body10?{body:body10.slice(0,MAX_API_ANSWER)}:{},status:res.status,contentType};if(text3){let answer2=bytes.subarray(0,MAX_API_ANSWER).toString("utf8");record.answer=answer2,bytes.length>MAX_API_ANSWER&&(record.cut=!0)}else{let own2=API_FILE_TYPES[type]??"bin",name=`${stem2}-answer.${own2}`;if(bytes.length>MAX_API_FILE)return error(`${part.name} answered with a file of ${bytes.length} bytes, too big to keep. Say so in your answer.`);fs126.writeFileSync(path116.join(this.s.proofDir,name),bytes),record.file=name,record.bytes=bytes.length;let sheets2=own2==="xlsx"?readWorkbook(bytes):null;sheets2&&(record.sheets=sheets2)}let file3=`${stem2}.api.json`;fs126.writeFileSync(path116.join(this.s.proofDir,file3),JSON.stringify(record,null,2)),this.pictures.set(file3,{file:file3,page:"",size:"answer",call:`${method} ${asked} on ${part.name}`}),this.s.say(`squad-kit asked ${part.name} for the ${this.s.who}, and kept the answer`);let sheets=record.sheets,kept2=`A file of ${bytes.length} bytes (${type}), saved as ${record.file}. squad-kit keeps it for the person to open.`,shown2=text3?String(record.answer).slice(0,8e3):sheets?`${kept2}
1593
1593
 
1594
1594
  What it holds:
1595
1595
 
@@ -1607,7 +1607,7 @@ The last lines it printed:
1607
1607
  ${tail(r.output,30)}`}function short4(s,n=MAX_NAME2){return s.length>n?`${s.slice(0,n)}\u2026`:s}import http from"http";import path117 from"path";import{timingSafeEqual}from"crypto";import{fileURLToPath as fileURLToPath2}from"url";var WINDOW_NAME="squad",PROTOCOLS=["2025-11-25","2025-06-18","2025-03-26","2024-11-05"],MAX_BODY=1024*1024,KEEP_IDLE_MS=12e4,windows=new Map,board=null,own=null,ownServer=null;function windowAt(base2){board=base2?base2.replace(/\/$/,""):null}function openWindow(w){windows.set(w.key,w)}async function closeWindow(w){windows.delete(w.key),await w.close()}function cliEntry(){let here=fileURLToPath2(import.meta.url);return/[\\/]cli\.js$/.test(here)?here:path117.resolve(path117.dirname(here),"../../dist/cli.js")}async function windowAccess(w){let base2=board??await ownBase();return{name:WINDOW_NAME,url:`${base2}/mcp`,key:w.key,tools:[...w.tools],bridge:{command:process.execPath,args:[cliEntry(),"mcp-bridge"]}}}function ownBase(){return own??=new Promise((resolve2,reject)=>{let srv=http.createServer((req,res)=>{serveNode(req,res)});srv.keepAliveTimeout=KEEP_IDLE_MS,srv.once("error",reject),srv.listen(0,"127.0.0.1",()=>{let a=srv.address();srv.unref(),ownServer=srv,resolve2(`http://127.0.0.1:${typeof a=="object"&&a?a.port:0}`)})}),own}function closeServer(s,done2){let sweep=setInterval(()=>s.closeIdleConnections(),100);s.close(err2=>{clearInterval(sweep),done2(err2)})}async function serveNode(req,res){let chunks=[],size2=0;for await(let c of req){if(size2+=c.length,size2>MAX_BODY)break;chunks.push(c)}let headers=new Headers;for(let[k,v]of Object.entries(req.headers))typeof v=="string"&&headers.set(k,v);let url=`http://${req.headers.host??"127.0.0.1"}${req.url??"/"}`,r=await answerWindow(new Request(url,{method:req.method??"GET",headers,...req.method==="POST"?{body:Buffer.concat(chunks)}:{}}));res.writeHead(r.status,Object.fromEntries(r.headers.entries())),res.end(Buffer.from(await r.arrayBuffer()))}var json=(body10,status=200)=>new Response(JSON.stringify(body10),{status,headers:{"content-type":"application/json","cache-control":"no-store"}}),rpcError=(id2,code,message)=>({jsonrpc:"2.0",id:id2??null,error:{code,message}});function windowOf(req){let auth2=req.headers.get("authorization")??"",key=auth2.toLowerCase().startsWith("bearer ")?auth2.slice(7).trim():"";if(!key)return null;let given=Buffer.from(key);for(let[k,w]of windows){let known=Buffer.from(k);if(given.length===known.length&&timingSafeEqual(given,known))return w}return null}async function answerWindow(req){if(req.method!=="POST")return new Response(null,{status:405,headers:{allow:"POST"}});if(req.headers.get("origin"))return json(rpcError(null,-32e3,"squad-kit takes these calls only from programs on this computer."),403);let host3=(req.headers.get("host")??"").replace(/:\d+$/,"").toLowerCase();if(!["127.0.0.1","localhost","[::1]"].includes(host3))return json(rpcError(null,-32e3,"squad-kit takes these calls only on this computer."),403);let w=windowOf(req);if(!w||w.isClosed)return json(rpcError(null,-32001,"squad-kit does not know this key, or its run has ended."),401);if(Number(req.headers.get("content-length")??0)>MAX_BODY)return json(rpcError(null,-32600,"The request is too large."),413);let body10;try{let raw=await req.text();if(raw.length>MAX_BODY)return json(rpcError(null,-32600,"The request is too large."),413);body10=JSON.parse(raw)}catch{return json(rpcError(null,-32700,"The request is not JSON."),400)}let batch=Array.isArray(body10),messages=batch?body10:[body10];if(!messages.length||messages.length>20)return json(rpcError(null,-32600,"Send one message, or a few."),400);let answers2=[];for(let m of messages){if(!m||typeof m!="object"){answers2.push(rpcError(null,-32600,"Not a JSON-RPC message."));continue}m.id===void 0||m.id===null||!m.method||answers2.push(await dispatch(w,m))}return answers2.length?json(batch?answers2:answers2[0]):new Response(null,{status:202})}async function dispatch(w,m){let p=m.params??{};switch(m.method){case"initialize":{let asked=typeof p.protocolVersion=="string"?p.protocolVersion:"";return{jsonrpc:"2.0",id:m.id,result:{protocolVersion:PROTOCOLS.includes(asked)?asked:PROTOCOLS[1],capabilities:{tools:{listChanged:!1}},serverInfo:{name:"squad-kit",version:readInstalledPackage().version},instructions:"squad-kit's own tools for this step. squad-kit checks every field and does the work itself, on this story's own app on this computer."}}}case"ping":return{jsonrpc:"2.0",id:m.id,result:{}};case"tools/list":return{jsonrpc:"2.0",id:m.id,result:{tools:w.listTools()}};case"tools/call":{let name=typeof p.name=="string"?p.name:"",result=await w.call(name,p.arguments??{});return{jsonrpc:"2.0",id:m.id,result}}default:return rpcError(m.id,-32601,`squad-kit does not offer ${String(m.method).slice(0,60)}.`)}}var TECHNICAL_BACK="This is technical. Decide it yourself, or call i_am_stuck.";function refusedAskLine(who4,what){return what==="settings"?`The ${who4} asked to change squad-kit's settings. squad-kit refused, and did not bother you with it.`:what==="secret"?`The ${who4} asked for a password or a key. squad-kit refused, and did not bother you with it.`:`The ${who4} asked to change the history of the code. squad-kit refused, and did not bother you with it.`}function refusedAskBack(what){return`squad-kit refused your question. ${what==="settings"?"Nobody may ask the person to change squad-kit's settings, its .squad folder or the files agents load as instructions.":what==="secret"?"Nobody may ask the person for a password, a key, a token or any other secret.":"Nobody may ask the person to change git history: squad-kit does all the git."} Decide it yourself, or call i_am_stuck if something technical is in your way.`}var LOOK_ALIKES={\u0430:"a",\u0432:"b",\u0435:"e",\u0451:"e",\u0437:"3",\u043A:"k",\u043C:"m",\u043D:"h",\u043E:"o",\u0440:"p",\u0441:"c",\u0442:"t",\u0443:"y",\u0445:"x",\u0455:"s",\u0456:"i",\u0457:"i",\u0458:"j","\u0501":"d","\u051B":"q","\u051D":"w",\u04BB:"h","\u04CF":"l",\u0261:"g",\u04AF:"y",\u0410:"A",\u0412:"B",\u0415:"E",\u041A:"K",\u041C:"M",\u041D:"H",\u041E:"O",\u0420:"P",\u0421:"C",\u0422:"T",\u0423:"Y",\u0425:"X",\u0405:"S",\u0406:"I",\u0408:"J","\u0500":"D",\u04AE:"Y",\u03B1:"a",\u03B2:"b",\u03B5:"e",\u03B9:"i",\u03BA:"k",\u03BD:"v",\u03BF:"o",\u03C1:"p",\u03C4:"t",\u03C5:"u",\u03C7:"x",\u03B3:"y",\u03C9:"w",\u0391:"A",\u0392:"B",\u0395:"E",\u0396:"Z",\u0397:"H",\u0399:"I",\u039A:"K",\u039C:"M",\u039D:"N",\u039F:"O",\u03A1:"P",\u03A4:"T",\u03A5:"Y",\u03A7:"X","\u2018":"'","\u2019":"'",\u02BC:"'","\u201C":'"',"\u201D":'"',"\u2010":"-","\u2011":"-","\u2012":"-","\u2013":"-","\u2212":"-"},LOOK_ALIKE=new RegExp(`[${Object.keys(LOOK_ALIKES).join("")}]`,"g"),INVISIBLE=/[\p{Cf}\p{Mn}\p{Me}ᅟᅠㅤᅠ⠀]/gu;function readable(text3){return text3.normalize("NFKC").replace(INVISIBLE,"").replace(LOOK_ALIKE,c=>LOOK_ALIKES[c]??c).replace(/[^\S\n]+/g," ").trim()}function sentences(text3){return text3.split(/(?<=[.!?؟])\s+|\n+/).map(s=>s.trim()).filter(Boolean)}var CHANGE=/\b(edit(s|ed|ing)?|chang(e|es|ed|ing)|set|sets|updat(e|es|ed|ing)|modif(y|ies|ied|ying)|add(s|ed|ing)?|remov(e|es|ed|ing)|delet(e|es|ed|ing)|replac(e|es|ed|ing)|turn(s|ed|ing)? (it |them |this |that )?(on|off)|switch(es|ed|ing)? (it |them |this |that )?(on|off)|enabl(e|es|ed|ing)|disabl(e|es|ed|ing)|adjust(s|ed|ing)?|(re)?configur(e|es|ed|ing)|tweak(s|ed|ing)?|allow(s|ed|ing)?|white-?list|allow-?list|loosen|relax|lift|bypass|skip|unlock|open up|grant|raise|increase|extend|widen|put|write|rewrite)\b/i,SQUAD_THINGS=[/(^|[\s"'`(/\\])\.squad\b/i,/\b(CLAUDE(\.local)?|AGENTS|GEMINI)\.md\b|(^|[\s"'`(/\\])\.(claude|cursor|codex|gemini)\b|\.cursorrules\b|\bcopilot-instructions\b|\bsecrets\.ya?ml\b/i,/\bsquad-?kit'?s?\s+(own\s+)?(settings?|config(uration)?|rules|checks?|guard|sandbox|hosts?|permissions?|commands?|files?|folder|limits?|options?)\b/i,/\b(allowed|permitted|allow-?listed|white-?listed)\s+(hosts?|sites?|websites?|domains?)\b|\b(hosts?|domains?)\s+list\b|\b(hosts?|sites?|websites?|domains?|addresses)\s+(that\s+)?(the\s+)?(squad|agents?)\s+(may|can|is allowed to|are allowed to)\s+(reach|use|open|visit|access|call)\b/i,/\b(the|squad-?kit'?s|its|this|that|your|my)\s+(guard|sandbox)\b(?!\s+(role|roles|desk|station|shifts?|team|users?|staff|booth|post|rota|schedule|on duty))|\b(sandboxed|seatbelt|bubblewrap)\b/i,/\b(start|test|tests|check|checks|build|setup|lint)\s+commands?\b/i,/\b(allow|let|permit)\s+(the\s+)?(squad|agents?|me|us)\s+(to\s+)?(reach|open|run|use|access|write|read|install|call|launch|start)\b/i],SETTINGS_PLACE=/\b(in|into|from|under|via|through|on|open|opens|opening|to)\s+(the\s+)?(squad-?kit'?s?\s+)?Settings\b/,ASKED=/\b(please|kindly|can you|could you|would you|will you|you (can|could|should|need to|must|have to|'ll need to|will need to|may want to|might want to)|i need you to|we need you to|(do|would) you mind)\b|^(please,?\s+)?(edit|change|set|update|modify|add|remove|delete|replace|turn|switch|enable|disable|adjust|configure|tweak|allow|grant|open|go|run|type|paste|force|rebase|squash|amend|revert|reset|push|drop|cherry-pick|rewrite|undo|give|send|share|tell|enter|provide)\b/i,HISTORY2=[/\bforce[- ]?push(es|ed|ing)?\b|\bpush\s+(--force(-with-lease)?|-f)\b|\bgit\s+(push|reset|rebase|commit|merge|checkout|switch|stash|clean|tag|branch|revert|cherry-pick|filter-branch|filter-repo|reflog|gc|prune)\b/i,/\brebas(e|es|ed|ing)\b[^.?!\n]{0,40}\b(onto|branch(es)?|commits?|main|master|origin|upstream|history|git|interactive)\b|\b(branch(es)?|commits?)\b[^.?!\n]{0,40}\brebas(e|ed|ing)\b/i,/\breset\s+--hard\b|\bhard reset\b[^.?!\n]{0,30}\b(branch|commits?|main|master|repo)\b/i,/\b(amend|squash|drop|revert|cherry-?pick|remove|delete|rewrite|undo|edit|reorder|split)\s+((the|that|this|those|these|my|your|its|last|latest|previous|old|a|some|all|two|three|\d+)\s+)*commits?\b/i,/\b(rewrite|rewriting|change|changing|edit|alter|clean( up)?|squash)\s+((the|its|this|our|your)\s+)?(git|commit|code'?s?|branch'?s?|repo(sitory)?'?s?)\s+history\b|\bhistory of (the|this|its) (code|branch|commits|repo(sitory)?)\b/i,/\b(delete|remove|drop|rename|reset|force|overwrite)\s+((the|this|that|a|my|your|its)\s+)?((git|remote|feature|ticket'?s?|story'?s?|work|main|master|base)\s+)?branch(es)?\b(?!\s+(offices?|managers?|staff|locations?|names?|list|address(es)?|codes?|levels?|of|in|at|for|from|near|that|which|where|hours|opening|accounts?|network|teams?|directors?|employees?))/i],SECRET_WORD=/^(passwords?|passcodes?|passphrases?|pins?|keys?|api-?keys?|apikeys?|tokens?|secrets?|credentials?|creds|logins?|log-ins?|sign-ins?|signins?)$/i,SIGN_IN_WORD=/^(logins?|log-ins?|sign-ins?|signins?|account|access|admin|credential|password)$/i,ENDS_THING=new Set("for of about on in at with to from by that which who whose and or so if when because here there now please first again than as into is are was were be via over under using after before while where how why then but".split(" ")),GIVE=/(?:^|\b(?:please|kindly|just)\s*,?\s+|\b(?:can|could|would|will) you\s+(?:please\s+|also\s+|just\s+|kindly\s+)?|\byou (?:can|could|should|need to|must|have to)\s+(?:also\s+|just\s+)?|\b(?:i|we) need you to\s+)(?:paste|send|share|give|provide|tell|enter|type|supply|post|reveal|hand over|copy|put|drop|forward|email|dm|text|add|input|fill in)\s+/gi,HAVE=/\b(?:do|can|could|would) you\s+(?:please\s+)?(?:still\s+)?(?:have|know|remember)\s+|\b(?:i|we)(?:'d| would| will|'ll)?\s+(?:need|want|require)\s+|\b(?:can|could|may) (?:i|we)\s+(?:please\s+)?(?:get|have|borrow|use|see)\s+/gi,SECRET_TO_HAND=/\b(passwords?|pins?|keys?|tokens?|logins?|credentials?|secrets?)\s+(?:(?:for (?:me|us)\s+)?to (?:paste|send|share|copy)|(?:that\s+)?(?:i|we) (?:can|could|should|may) (?:paste|use|copy|try))\b/i,WHAT=/\b(?:what(?:'s|s| is| are| was| were)|where(?:'s| is| are| can i find| do i find| can i get| do i get)|which is)\s+/gi,SECRET_PLACE=/(^|[\s"'`(/])\.env\b|\bsecrets?\.ya?ml\b|\bkeychain\b|\bpassword manager\b/i;function thingAsked(rest){let words2=rest.split(/\s+/).filter(Boolean),i=0;/^(me|us|it|over|in)$/i.test(words2[i]??"")&&(i+=1),/^(to|with)$/i.test(words2[i]??"")&&/^(me|us)$/i.test(words2[i+1]??"")&&(i+=2),/^what$/i.test(words2[i]??"")&&(i+=1);let thing=[];for(;i<words2.length&&thing.length<6;i+=1){let raw=words2[i],word2=raw.replace(/^["'`(]+|["'`).,;:!?]+$/g,"");if(!word2||ENDS_THING.has(word2.toLowerCase())||(thing.push(word2),/[.,;:!?)]$/.test(raw)))break}return thing}function isSecretThing(thing){let last=thing[thing.length-1]??"",before=thing[thing.length-2]??"";return SECRET_WORD.test(last)||/^(details|info|information)$/i.test(last)&&SIGN_IN_WORD.test(before)?!0:/^connection$/i.test(before)&&/^strings?$/i.test(last)}function asksForSecret(s){if(SECRET_TO_HAND.test(s)&&(ASKED.test(s)||/\b(i|we|me|us)\b/i.test(s)))return!0;for(let re of[GIVE,HAVE,WHAT]){re.lastIndex=0;for(let m=re.exec(s);m;m=re.exec(s)){if(isSecretThing(thingAsked(s.slice(m.index+m[0].length)))||re===GIVE&&SECRET_PLACE.test(s))return!0;m[0].length===0&&(re.lastIndex+=1)}}return!1}var AR_SECRET="(?:\u0627\u0644)?(?:\u0643\u0644\u0645(?:\u0629|\u0627\u062A) (?:\u0627\u0644)?(?:\u0645\u0631\u0648\u0631|\u0633\u0631)|\u0631\u0642\u0645 (?:\u0627\u0644)?\u0633\u0631\u064A|\u0631\u0645\u0632 (?:\u0627\u0644)?(?:\u062F\u062E\u0648\u0644|\u0633\u0631\u064A|\u0648\u0635\u0648\u0644)|\u0645\u0641\u062A\u0627\u062D (?:\u0627\u0644)?(?:API|\u0648\u0627\u062C\u0647\u0629|\u0648\u0635\u0648\u0644|\u062F\u062E\u0648\u0644|\u0633\u0631\u064A)|\u062A\u0648\u0643\u0646|\u0628\u064A\u0627\u0646\u0627\u062A (?:\u0627\u0644)?(?:\u062F\u062E\u0648\u0644|\u0627\u0639\u062A\u0645\u0627\u062F)|API[- ]?key|token|password)",AR_GIVE="(?:^|\u0645\u0646 \u0641\u0636\u0644\u0643\\s+|\u0631\u062C\u0627\u0621\\s+|\u064A\u0631\u062C\u0649\\s+|\u0647\u0644 \u064A\u0645\u0643\u0646\u0643\\s+|\u0647\u0644 \u062A\u0633\u062A\u0637\u064A\u0639\\s+|\u0623\u0631\u062C\u0648\\s+)(?:\u0623\u0639\u0637\u0646\u064A|\u0623\u0639\u0637\u0646\u0627|\u0623\u0631\u0633\u0644|\u0632\u0648\u062F\u0646\u064A|\u0632\u0648\u062F\u0646\u0627|\u0634\u0627\u0631\u0643|\u0627\u0644\u0635\u0642|\u0623\u062F\u062E\u0644|\u0627\u0643\u062A\u0628|\u0642\u062F\u0645|\u0627\u0646\u0633\u062E|\u0623\u062E\u0628\u0631\u0646\u064A|\u0625\u0639\u0637\u0627\u0626\u064A|\u0625\u0631\u0633\u0627\u0644|\u062A\u0632\u0648\u064A\u062F\u064A|\u062A\u0632\u0648\u064A\u062F\u0646\u0627|\u0645\u0634\u0627\u0631\u0643\u0629)(?:\\s+(?:\u0644\u064A|\u0644\u0646\u0627|\u0625\u0644\u064A|\u0625\u0644\u064A\u0646\u0627|\u0645\u0639\u064A|\u0645\u0639\u0646\u0627|\u0628))?\\s*",AR_HAVE="(?:\u0647\u0644 \u0644\u062F\u064A\u0643|\u0647\u0644 \u0639\u0646\u062F\u0643|\u0647\u0644 \u062A\u0639\u0631\u0641|\u0647\u0644 \u062A\u0645\u0644\u0643|\u0623\u062D\u062A\u0627\u062C(?:\\s+\u0625\u0644\u0649)?|\u0646\u062D\u062A\u0627\u062C(?:\\s+\u0625\u0644\u0649)?|\u0623\u0631\u064A\u062F|\u0646\u0631\u064A\u062F)\\s+",AR_WHAT="(?:\u0645\u0627 \u0647\u064A|\u0645\u0627 \u0647\u0648|\u0645\u0627|\u0623\u064A\u0646 \u0647\u064A|\u0623\u064A\u0646 \u0647\u0648|\u0623\u064A\u0646)\\s+",AR_ASKS_SECRET=new RegExp(`(?:${AR_GIVE}|${AR_HAVE}|${AR_WHAT})${AR_SECRET}`),AR_CHANGE=/(?:غير|عدل|حدث|أضف|احذف|أزل|بدل|فعل|عطل|اسمح|ارفع|افتح|اكتب في|تغيير|تعديل|تحديث|إضافة|حذف|تفعيل|تعطيل)/,AR_SQUAD_THINGS=/(?:إعدادات|ملفات|مجلد|قواعد|فحوصات|حدود|صلاحيات|حارس)\s+squad-?kit/i;function notAllowedIn(text3,reply2){for(let s of sentences(text3)){let asked=ASKED.test(s);if(asksForSecret(s)||AR_ASKS_SECRET.test(s))return{what:"secret",why:"asks the person for a secret"};if(AR_CHANGE.test(s)&&AR_SQUAD_THINGS.test(s))return{what:"settings",why:"asks the person to change squad-kit's settings or protected files"};let changes=CHANGE.test(s)&&(!reply2||asked);if(changes&&SQUAD_THINGS.some(re=>re.test(s)))return{what:"settings",why:"asks the person to change squad-kit's settings or protected files"};if(changes&&asked&&SETTINGS_PLACE.test(s))return{what:"settings",why:"asks the person to change squad-kit's settings"};if((!reply2||asked)&&HISTORY2.some(re=>re.test(s)))return{what:"history",why:"asks the person to change git history"}}return null}var TECHNICAL=[/\b(config(uration)? files?|settings files?|package\.json|tsconfig|dockerfile|docker-compose|makefile|\.env\b|\.ya?ml\b|\.toml\b|\.ini\b|\.json\b)/i,...SQUAD_THINGS,/\bports? \d{2,5}\b|\bport (number|is (in use|taken|busy|blocked)|in use)\b|\b(busy|free|open|another|a different) port\b|\bon port\b|\blisten(s|ing)? on\b|\blocalhost\b|\b127\.0\.0\.1\b|\b0\.0\.0\.0\b|:\d{4,5}\b/i,/\b(pids?|process id|kill( the)? (process|server|app)|running process(es)?|background process(es)?|dev server|web server)\b/i,/\b(permission denied|file permissions?|eacces|eperm|sudo|chmod|chown|root access|admin rights)\b/i,/\bpermissions? (to|for) (run|write|read|execute|install|access|use)\b[^.?!\n]{0,30}\b(files?|folders?|director(y|ies)|commands?|terminal|shell|scripts?|browser|tools?|ports?|network|internet|disk)\b/i,/\b(sandbox(ed)?|seatbelt|bubblewrap|mach port|bootstrap_check_in|sigtrap)\b/i,/\b(run|type|enter|paste|execute|start|launch|use)\b[^.?!\n]{0,40}\b(in|into|from|inside) (a |the |your |my )?terminal\b|\b(open|launch) (a |the |your |my )?(new )?terminal\b|\bterminal (window|app|command|session|tab|emulator)s?\b/i,/\b(command line|command prompt|shell (command|script)s?|bash|zsh|powershell|cli)\b/i,/\b(run|execute|type|enter|paste) ((the|this|a|that|these|those|following|my|your) )?((shell|terminal|cli) )?(commands?|scripts?)\b|\b(shell|terminal|cli|bash|git|npm|pnpm|yarn) commands?\b|\b(npm|pnpm|yarn|npx|pip|pip3|brew|apt|apt-get|dotnet|mvn|gradle|cargo|go get|composer)\b/i,/\b(environment variables?|env vars?)\b/i,/\b[A-Z][A-Z0-9]*_[A-Z0-9_]{2,}\b/,/\b(install(ed|ing|s)?|reinstall|upgrade|downgrade) (the |a |an )?(browser|chromium|chrome|playwright|package|packages|dependenc(y|ies)|node|python|library|libraries|module|sdk|driver|plugin|tool)\b/i,/\b(playwright|chromium|headless|webdriver|puppeteer|selenium)\b/i,/(^|[\s"'`(])(~\/|\.\.?\/|\/(Users|home|private|tmp|var|etc|opt|usr)\/|[A-Za-z]:\\)/,/\b[\w.-]+\/[\w./-]*\.(m?[jt]sx?|json|py|ya?ml|toml|sh|md|lock|cfg|ini|cs|java|rb|go|php|html|css)\b/i,/\bmcp\b|\bi_am_stuck\b/i,/\bgit\b|\bpull request\b|\bgit stash\b|\bstash(ed|ing)? ((the|my|your|these|those|its|all|local|uncommitted) )?(changes|work|edits)\b|\bmerge conflict\b|\bcommit (the|these|my|this) changes\b/i,/مل(?:ف|فات) (?:ال)?(?:إعدادات|تكوين)|متغير(?:ات)? (?:ال)?بيئة/,/(?:رقم )?(?:ال)?منفذ \d{2,5}|رقم (?:ال)?منفذ|خادم (?:ال)?(?:تطوير|ويب)|(?:ال)?عملية الجارية/,/صلاحيات (?:ال)?مل(?:ف|فات)|صلاحية (?:ال)?ملف|تم رفض الإذن/,/(?:نافذة|سطر|موجه) (?:ال)?أوامر|(?:في |إلى |من )(?:ال)?طرفية/,/(?:شغل|نفذ|اكتب|الصق|تشغيل|تنفيذ)\s+(?:هذا\s+|هذه\s+)?(?:ال)?(?:أمر|أوامر|سكربت|برنامج النصي)/,/(?:ثبت|تثبيت|حدث|تحديث)\s+(?:ال)?(?:متصفح|حزم(?:ة)?|مكتب(?:ة|ات)|إضافة|أداة|حزمة)/,/طلب(?:ات)? (?:ال)?سحب|تعارض (?:ال)?دمج|مستودع (?:ال)?كود|(?:ال)?فرع (?:الرئيسي )?في (?:ال)?مستودع/];function askText(q5){let named=x=>Array.isArray(x)?x.flatMap(s=>s&&typeof s=="object"?[s.text,s.title,s.section]:[s]):[];return[q5.text,...Array.isArray(q5.options)?q5.options:[],q5.why,...named(q5.sources),...named(q5.refs),q5.impact].filter(x=>typeof x=="string"&&x.trim()!=="").join(`
1608
1608
  `)}function judgeQuestion(q5){let text3=readable(askText(q5));if(!text3)return{pass:!0};let no=notAllowedIn(text3,!1);if(no)return{pass:!1,kind:"not-allowed",...no};let hit=TECHNICAL.find(re=>re.test(text3));return hit?{pass:!1,kind:"technical",why:`technical: ${(hit.exec(text3)?.[0]??"").trim().slice(0,60)}`}:{pass:!0}}function judgeReply(text3){let no=notAllowedIn(readable(text3??""),!0);return no?{pass:!1,kind:"not-allowed",...no}:{pass:!0}}function refusedReplyLine(who4,what){return`The ${who4}'s answer asked you ${what==="settings"?"to change squad-kit's settings":what==="secret"?"for a password or a key":"to change the history of the code"}, so squad-kit left it out. Ask again, or ask in other words.`}function questionsIn(answer2){let a=answer2;if(!a||typeof a!="object")return[];let one2=(q5,at3)=>{let x=q5;return!x||typeof x.text!="string"||!x.text.trim()?[]:[{at:at3,text:x.text,...Array.isArray(x.options)?{options:x.options.filter(o=>typeof o=="string")}:{},...typeof x.why=="string"?{why:x.why}:{},...Array.isArray(x.sources)?{sources:x.sources}:{},...Array.isArray(x.refs)?{refs:x.refs}:{},...typeof x.impact=="string"?{impact:x.impact}:{}}]};return[...one2(a.question,"question"),...Array.isArray(a.questions)?a.questions.flatMap((q5,i)=>one2(q5,`questions[${i}]`)):[]]}function stoppedQuestion(answer2,settles){for(let q5 of questionsIn(answer2)){let verdict=judgeQuestion(q5);if(!(!verdict.pass&&verdict.kind==="technical"&&settles?.(q5))&&!verdict.pass)return{q:q5,verdict}}return null}function questionBackPrompt(found2){let said=`Your ${found2.q.at==="question"?"question":`question (${found2.q.at})`}: "${found2.q.text.slice(0,300)}"`,back=found2.verdict.kind==="technical"?TECHNICAL_BACK:refusedAskBack(found2.verdict.what);return`${said}
1609
1609
  ${back}
1610
- Then carry on and hand in your answer again${found2.q.at==="question"?" without that question":" without that question in it"}. Ask the person only about the business: a gap (something missing that changes what gets built) or a clash (two requirements that disagree).`}var STOPPED9="You stopped the squad.",TECHNICAL_STOP="The step stopped on something technical, so squad-kit could not finish it. Press Try again.",RUN_WORDS=[/^You stopped the squad\.$/,/^The step reached the most it may do in one run\.$/,/^It used the \d+ turns this step may take\.$/,/^It reached the \$[\d.]+ this step may spend\.$/,/^Stopped after \d+ minutes without finishing\.$/,/^It took longer than \d+ minutes, so squad-kit stopped it\.$/,/^The answer was cut off because it got too long\.$/,/^The agent declined to do this step\.$/,/^The step was cancelled\.$/,/^The agent finished without giving squad-kit its answer\.$/,/^squad-kit could not save what the agent was doing, so it stopped the run\. The work so far is kept\.$/,/^[\w .-]{1,40} did not (start|open its session|open a session) in \d+ seconds?\. Check that it opens in a terminal and is logged in, then press Try again\.$/];function ownRunWords(said,tool5){let s=said.trim();return RUN_WORDS.some(re=>re.test(s))||!!tool5?.ownWords?.(s)}function ownWords2(err2){return err2 instanceof StepError||err2 instanceof NoRoomForCopy?err2.message:err2 instanceof Error&&err2.message===STOPPED9?STOPPED9:null}var OUTSIDE_LOOK_MS=5*6e4,FIRST_LOOK_MS=5e3;function onClashCard(t){return t.waiting?.kind==="stuck"&&t.stuck?.gate==="merge"}function still(t){return!t.live&&!t.squad&&!t.deleting&&!t.archived&&!isMerged(t)}function openPr(t){return still(t)?t.stage==="release"&&t.pr?t.pr:onClashCard(t)?t.clashedPr??null:null:null}function mayHaveClashedPr(bp,t){return onClashCard(t)&&still(t)&&!t.clashedPr&&!!t.branch&&opensPullRequest(bp,t)}async function clashedByBranch(bp,t,host3){if(!mayHaveClashedPr(bp,t)||!host3.lastPullRequest)return null;let found2=await host3.lastPullRequest(t.branch);if(!found2)return null;let pr={id:found2.id,url:found2.url,repo:host3.repo,host:host3.host};return updateTicket(bp,t.id,x=>{onClashCard(x)&&!x.clashedPr&&(x.clashedPr=pr)}),pr}async function recordMerged(bp,id2,pr,host3,target){let moved=await followBase(bp,id2,target,host3),t0=requireTicket(bp,id2);if(isMerged(t0))return!1;let clash=onClashCard(t0),draft=t0.release?null:ensureDraftRelease(bp,t0.kind==="bug"?"patch":"minor"),done2=!1;if(updateTicket(bp,id2,x=>{if(isMerged(x))return;done2=!0,x.mergedAt=new Date().toISOString(),clearStaleStop(x),x.retryPhase=void 0,x.stage="release",x.pr=pr,delete x.clashedPr,delete x.prClosed,x.release=x.release??draft?.version,x.live=!1,x.squad=null,x.progress=void 0;let ls=moved?localSetupFor(bp,x):void 0;ls&&(x.localSetup=ls),x.status=`Merged into ${target.base} \xB7 goes out in ${x.release??"the next release"}`,addActivity(x,"release",`Pull request ${pr.id} was merged on ${host3.label}. squad-kit saw it and marked the story as merged`)}),!done2)return!1;if(draft){let r=listReleases(bp).find(v=>v.version===draft.version)??draft;r.tickets.includes(id2)||writeRelease(bp,{...r,tickets:[...r.tickets,id2]})}return mapAfterMerge(bp),clash&&isMine(t0,readMeta(bp))&&await trackerMerged(bp,id2,pr,target.base),!0}function closedLine(pr,host3,base2){return`Pull request ${pr.id} was closed on ${host3.label} without merging, so it is not in ${base2}. squad-kit changed nothing else: open the pull request again there to go on`}function saidClosed(bp,id2,pr,host3,base2){updateTicket(bp,id2,x=>{x.prClosed!==pr.id&&(x.prClosed=pr.id,addActivity(x,"release",closedLine(pr,host3,base2)))})}async function clashCardNow(bp,id2){await fetchBase(bp,pullRequestClashed(bp,id2)),afterPullRequest(bp,id2,!0)}async function checkOwn(bp,t){let wait=mergeWait(t);if(wait)return checkWait(bp,t,wait);if(!openPr(t)&&!mayHaveClashedPr(bp,t))return null;let target=repoTarget(bp,t),host3=target&&repoHostFor(bp,target.url);if(!target||!host3)return{found:"none",line:`${t.id}'s code host has no saved sign-in on this computer, so squad-kit cannot ask it. Add it in Settings, then check again.`,merged:[]};let pr=openPr(t)??await clashedByBranch(bp,t,host3);if(!pr)return mayHaveClashedPr(bp,t)?{found:"none",line:`squad-kit found no pull request of ${t.id} on ${host3.label}, so there is nothing to ask about. Pick a way out on the card.`,merged:[]}:null;let state=await host3.mergeState(pr.id),base2=target.base;if(state==="merged"){let merged2=await recordMerged(bp,t.id,pr,host3,target);return{found:"merged",line:`Pull request ${pr.id} was merged on ${host3.label}. squad-kit saw it and marked ${t.id} as merged.`,merged:merged2?[t.id]:[]}}return state==="closed"?(saidClosed(bp,t.id,pr,host3,base2),{found:"closed",line:`${closedLine(pr,host3,base2)}.`,merged:[]}):(t.prClosed===pr.id&&updateTicket(bp,t.id,x=>delete x.prClosed),state==="clash"?onClashCard(t)?{found:"clash",line:`Pull request ${pr.id} still clashes with ${base2} on ${host3.label}. Settle the clash there and merge it, or pick a way out on the card.`,merged:[]}:(await clashCardNow(bp,t.id),{found:"clash",line:`Pull request ${pr.id} clashes with newer work in ${base2}, so ${t.id} waits for you with ways out.`,merged:[]}):{found:"open",line:`Pull request ${pr.id} is still open on ${host3.label}, not merged yet. Nothing changed.`,merged:[]})}async function checkWait(bp,t,wait){let base2=repoTarget(bp,t)?.base??"main",r=await recheckMerge(bp,t.id);if(r==="merged")return{found:"merged",line:`${readTicket(bp,t.id)?.activity[0]?.text??`${t.id} is merged`}.`,merged:[t.id]};if(r==="clash")return await clashCardNow(bp,t.id),{found:"clash",line:`${t.pr?`Pull request ${t.pr.id}`:t.id} clashes with newer work in ${base2}, so ${t.id} waits for you with ways out.`,merged:[]};let target=repoTarget(bp,t),host3=target&&repoHostFor(bp,target.url);return r==="closed"&&t.pr&&host3?(saidClosed(bp,t.id,t.pr,host3,base2),{found:"closed",line:`${closedLine(t.pr,host3,base2)}.`,merged:[]}):wait==="pr"?{found:"open",line:`Pull request ${t.pr?.id??""} is still open${host3?` on ${host3.label}`:""}, waiting for someone to approve it. Nothing changed.`,merged:[]}:{found:"open",line:`Your own copy of ${base2} still has changes in the same files. Save or undo them there, and ${t.id} goes in by itself.`,merged:[]}}function goOn(bp,id2){let t=readTicket(bp,id2);return!t||t.deleting||unmergedDeps(bp,t).length||!isMine(t,readMeta(bp))?!1:t.waiting?.kind==="depends"?(chooseBuild(bp,id2,"wait"),!0):wakeIfReady(bp,t)}async function checkMerge(bp,id2){let t=requireTicket(bp,id2);if(readMeta(bp).engine!=="live")throw new BoardActionError("The sample board has no code host, so there is nothing to check.");if(isMerged(t))return{found:"merged",line:`${t.id} is merged already.`,merged:[],ticket:t};let own2=await checkOwn(bp,t);if(own2)return{...own2,ticket:requireTicket(bp,id2)};let deps=unmergedDeps(bp,t);if(!deps.length)throw new BoardActionError(`${t.id} has no pull request waiting and waits for no other story, so there is nothing to check.`);let lines3=[],merged2=[],found2="merged";for(let d of deps){let r=await checkOwn(bp,d)??{found:"none",line:`${d.id} is not merged yet, and it has no pull request on the code host to ask about.`,merged:[]};lines3.push(r.line),merged2.push(...r.merged),r.found!=="merged"&&found2==="merged"&&(found2=r.found)}return goOn(bp,id2)&&lines3.push(`${t.id} starts now, on the newest code.`),{found:found2,line:lines3.join(" "),merged:merged2,ticket:requireTicket(bp,id2)}}var looks=new Map,notFound=new Set;function lookOutsideSoon(bp,at3=Date.now()){let l=looks.get(bp.squadDir);l?l.at=Math.min(l.at,at3+FIRST_LOOK_MS):looks.set(bp.squadDir,{at:at3+FIRST_LOOK_MS,busy:!1})}function lookOutsideBeside(bp,at3=Date.now()){looks.has(bp.squadDir)||lookOutsideSoon(bp,at3);let l=looks.get(bp.squadDir);l.busy||at3<l.at||(l.busy=!0,lookOutside(bp).catch(()=>{}).finally(()=>{l.busy=!1,l.at=Date.now()+OUTSIDE_LOOK_MS}))}async function lookOutside(bp){let meta=readMeta(bp),out20={asked:0,merged:[]};if(meta.engine!=="live")return out20;for(let t of listTickets(bp)){if(!isMine(t,meta)||mergeWait(t))continue;let known=openPr(t),key=`${bp.squadDir}\0${t.id}`;if(known?t.prClosed===known.id:notFound.has(key)||!mayHaveClashedPr(bp,t))continue;let target=repoTarget(bp,t),host3=target&&repoHostFor(bp,target.url);if(!(!target||!host3))try{let pr=known??await clashedByBranch(bp,t,host3);if(!pr){notFound.add(key);continue}if(pr.id===t.prClosed)continue;out20.asked+=1;let state=await host3.mergeState(pr.id);state==="merged"&&await recordMerged(bp,t.id,pr,host3,target)?out20.merged.push(t.id):state==="closed"&&saidClosed(bp,t.id,pr,host3,target.base)}catch{}}return out20}import fs127 from"fs";import os20 from"os";import path118 from"path";var CONVERSATIONS=[{key:"intake",label:"Analyst and Architect"},{key:"build",label:"Builder"},{key:"review",label:"Reviewer"},{key:"tester",label:"Tester"}];function blockedMeta(output){let found2=blockedIn(output);return found2.length?{squadKit:{blocked:found2.map(b=>blockedLine(b))}}:null}function withBlocked(updates){return updates.map(e=>{let u=e.update,meta=u.sessionUpdate==="tool_call_update"?blockedMeta(outputOf(u)):null;return meta?{...e,update:{...u,_meta:{...u._meta??{},...meta}}}:e})}function sessionFile2(id2,base2=path118.join(os20.homedir(),".claude","projects")){if(!/^[\w-]{8,64}$/.test(id2))return null;try{for(let d of fs127.readdirSync(base2)){let file3=path118.join(base2,d,`${id2}.jsonl`);if(fs127.existsSync(file3))return file3}}catch{}return null}var SYSTEM_PROMPT=/^\s*(<task-notification|<system-reminder|\[structured-output-enforce\]|<command-|Caveat:)/,MAX_TEXT=2e4,MAX_RESULT=4e3,MAX_IMAGE=3e6,MEDIA={png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",gif:"image/gif",webp:"image/webp",svg:"image/svg+xml",mp4:"video/mp4",webm:"video/webm",mov:"video/quicktime",mp3:"audio/mpeg",wav:"audio/wav",ogg:"audio/ogg",weba:"audio/webm",opus:"audio/ogg",oga:"audio/ogg",m4a:"audio/mp4",aac:"audio/aac",pdf:"application/pdf",csv:"text/csv",json:"application/json",md:"text/markdown",txt:"text/plain",html:"text/html",zip:"application/zip",xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",docx:"application/vnd.openxmlformats-officedocument.wordprocessingml.document",har:"application/json",log:"text/plain"};function dispositionOf(kind,file3){let name=path118.basename(file3);return/^[\x20-\x7e]+$/.test(name)?`${kind}; filename="${name.replace(/"/g,"")}"`:`${kind}; filename*=UTF-8''${encodeURIComponent(name)}`}function mimeOf(file3){return MEDIA[path118.extname(file3).slice(1).toLowerCase()]??"application/octet-stream"}var SHOWN=/\.(png|jpe?g|gif|webp|svg|mp4|webm|mov|mp3|wav|ogg|pdf|csv|xlsx|docx|zip|har)$/i;function clip(s,max){let t=s.trim();return t.length>max?`${t.slice(0,max)}
1610
+ Then carry on and hand in your answer again${found2.q.at==="question"?" without that question":" without that question in it"}. Ask the person only about the business: a gap (something missing that changes what gets built) or a clash (two requirements that disagree).`}var STOPPED9="You stopped the squad.",TECHNICAL_STOP="The step stopped on something technical, so squad-kit could not finish it. Press Try again.",RUN_WORDS=[/^You stopped the squad\.$/,/^The step reached the most it may do in one run\.$/,/^It used the \d+ turns this step may take\.$/,/^It reached the \$[\d.]+ this step may spend\.$/,/^Stopped after \d+ minutes without finishing\.$/,/^It took longer than \d+ minutes, so squad-kit stopped it\.$/,/^The answer was cut off because it got too long\.$/,/^The agent declined to do this step\.$/,/^The step was cancelled\.$/,/^The agent finished without giving squad-kit its answer\.$/,/^squad-kit could not save what the agent was doing, so it stopped the run\. The work so far is kept\.$/,/^[\w .-]{1,40} did not (start|open its session|open a session) in \d+ seconds?\. Check that it opens in a terminal and is logged in, then press Try again\.$/];function ownRunWords(said,tool5){let s=said.trim();return RUN_WORDS.some(re=>re.test(s))||!!tool5?.ownWords?.(s)}function ownWords2(err2){return err2 instanceof StepError||err2 instanceof NoRoomForCopy?err2.message:err2 instanceof Error&&err2.message===STOPPED9?STOPPED9:null}var OUTSIDE_LOOK_MS=5*6e4,FIRST_LOOK_MS=5e3;function onClashCard(t){return t.waiting?.kind==="stuck"&&t.stuck?.gate==="merge"}function still(t){return!t.live&&!t.squad&&!t.deleting&&!t.archived&&!isMerged(t)}function openPr(t){return still(t)?t.stage==="release"&&t.pr?t.pr:onClashCard(t)?t.clashedPr??null:null:null}function mayHaveClashedPr(bp,t){return onClashCard(t)&&still(t)&&!t.clashedPr&&!!t.branch&&opensPullRequest(bp,t)}async function clashedByBranch(bp,t,host3){if(!mayHaveClashedPr(bp,t)||!host3.lastPullRequest)return null;let found2=await host3.lastPullRequest(t.branch);if(!found2)return null;let pr={id:found2.id,url:found2.url,repo:host3.repo,host:host3.host};return updateTicket(bp,t.id,x=>{onClashCard(x)&&!x.clashedPr&&(x.clashedPr=pr)}),pr}async function recordMerged(bp,id2,pr,host3,target){let moved=await followBase(bp,id2,target,host3),t0=requireTicket(bp,id2);if(isMerged(t0))return!1;let clash=onClashCard(t0),draft=t0.release?null:ensureDraftRelease(bp,t0.kind==="bug"?"patch":"minor"),done2=!1;if(updateTicket(bp,id2,x=>{if(isMerged(x))return;done2=!0,x.mergedAt=new Date().toISOString(),clearStaleStop(x),x.retryPhase=void 0,x.stage="release",x.pr=pr,delete x.clashedPr,delete x.prClosed,x.release=x.release??draft?.version,x.live=!1,x.squad=null,x.progress=void 0;let ls=moved?localSetupFor(bp,x):void 0;ls&&(x.localSetup=ls),x.status=`Merged into ${target.base} \xB7 goes out in ${x.release??"the next release"}`,addActivity(x,"release",`Pull request ${pr.id} was merged on ${host3.label}. squad-kit saw it and marked the story as merged`)}),!done2)return!1;if(draft){let r=listReleases(bp).find(v=>v.version===draft.version)??draft;r.tickets.includes(id2)||writeRelease(bp,{...r,tickets:[...r.tickets,id2]})}return mapAfterMerge(bp),clash&&isMine(t0,readMeta(bp))&&await trackerMerged(bp,id2,pr,target.base),!0}function closedLine(pr,host3,base2){return`Pull request ${pr.id} was closed on ${host3.label} without merging, so it is not in ${base2}. squad-kit changed nothing else: open the pull request again there to go on`}function saidClosed(bp,id2,pr,host3,base2){updateTicket(bp,id2,x=>{x.prClosed!==pr.id&&(x.prClosed=pr.id,addActivity(x,"release",closedLine(pr,host3,base2)))})}async function clashCardNow(bp,id2){await fetchBase(bp,pullRequestClashed(bp,id2)),afterPullRequest(bp,id2,!0)}async function checkOwn(bp,t){let wait=mergeWait(t);if(wait)return checkWait(bp,t,wait);if(!openPr(t)&&!mayHaveClashedPr(bp,t))return null;let target=repoTarget(bp,t),host3=target&&repoHostFor(bp,target.url);if(!target||!host3)return{found:"none",line:`${t.id}'s code host has no saved sign-in on this computer, so squad-kit cannot ask it. Add it in Settings, then check again.`,merged:[]};let pr=openPr(t)??await clashedByBranch(bp,t,host3);if(!pr)return mayHaveClashedPr(bp,t)?{found:"none",line:`squad-kit found no pull request of ${t.id} on ${host3.label}, so there is nothing to ask about. Pick a way out on the card.`,merged:[]}:null;let state=await host3.mergeState(pr.id),base2=target.base;if(state==="merged"){let merged2=await recordMerged(bp,t.id,pr,host3,target);return{found:"merged",line:`Pull request ${pr.id} was merged on ${host3.label}. squad-kit saw it and marked ${t.id} as merged.`,merged:merged2?[t.id]:[]}}return state==="closed"?(saidClosed(bp,t.id,pr,host3,base2),{found:"closed",line:`${closedLine(pr,host3,base2)}.`,merged:[]}):(t.prClosed===pr.id&&updateTicket(bp,t.id,x=>delete x.prClosed),state==="clash"?onClashCard(t)?{found:"clash",line:`Pull request ${pr.id} still clashes with ${base2} on ${host3.label}. Settle the clash there and merge it, or pick a way out on the card.`,merged:[]}:(await clashCardNow(bp,t.id),{found:"clash",line:`Pull request ${pr.id} clashes with newer work in ${base2}, so ${t.id} waits for you with ways out.`,merged:[]}):{found:"open",line:`Pull request ${pr.id} is still open on ${host3.label}, not merged yet. Nothing changed.`,merged:[]})}async function checkWait(bp,t,wait){let base2=repoTarget(bp,t)?.base??"main",r=await recheckMerge(bp,t.id);if(r==="merged")return{found:"merged",line:`${readTicket(bp,t.id)?.activity[0]?.text??`${t.id} is merged`}.`,merged:[t.id]};if(r==="clash")return await clashCardNow(bp,t.id),{found:"clash",line:`${t.pr?`Pull request ${t.pr.id}`:t.id} clashes with newer work in ${base2}, so ${t.id} waits for you with ways out.`,merged:[]};let target=repoTarget(bp,t),host3=target&&repoHostFor(bp,target.url);return r==="closed"&&t.pr&&host3?(saidClosed(bp,t.id,t.pr,host3,base2),{found:"closed",line:`${closedLine(t.pr,host3,base2)}.`,merged:[]}):wait==="pr"?{found:"open",line:`Pull request ${t.pr?.id??""} is still open${host3?` on ${host3.label}`:""}, waiting for someone to approve it. Nothing changed.`,merged:[]}:{found:"open",line:`Your own copy of ${base2} still has changes in the same files. Save or undo them there, and ${t.id} goes in by itself.`,merged:[]}}function goOn(bp,id2){let t=readTicket(bp,id2);return!t||t.deleting||unmergedDeps(bp,t).length||!isMine(t,readMeta(bp))?!1:t.waiting?.kind==="depends"?(chooseBuild(bp,id2,"wait"),!0):wakeIfReady(bp,t)}async function checkMerge(bp,id2){let t=requireTicket(bp,id2);if(readMeta(bp).engine!=="live")throw new BoardActionError("The sample board has no code host, so there is nothing to check.");if(isMerged(t))return{found:"merged",line:`${t.id} is merged already.`,merged:[],ticket:t};let own2=await checkOwn(bp,t);if(own2)return{...own2,ticket:requireTicket(bp,id2)};let deps=unmergedDeps(bp,t);if(!deps.length)throw new BoardActionError(`${t.id} has no pull request waiting and waits for no other story, so there is nothing to check.`);let lines3=[],merged2=[],found2="merged";for(let d of deps){let r=await checkOwn(bp,d)??{found:"none",line:`${d.id} is not merged yet, and it has no pull request on the code host to ask about.`,merged:[]};lines3.push(r.line),merged2.push(...r.merged),r.found!=="merged"&&found2==="merged"&&(found2=r.found)}return goOn(bp,id2)&&lines3.push(`${t.id} starts now, on the newest code.`),{found:found2,line:lines3.join(" "),merged:merged2,ticket:requireTicket(bp,id2)}}var looks=new Map,notFound=new Set;function lookOutsideSoon(bp,at3=Date.now()){let l=looks.get(bp.squadDir);l?l.at=Math.min(l.at,at3+FIRST_LOOK_MS):looks.set(bp.squadDir,{at:at3+FIRST_LOOK_MS,busy:!1})}function lookOutsideBeside(bp,at3=Date.now()){looks.has(bp.squadDir)||lookOutsideSoon(bp,at3);let l=looks.get(bp.squadDir);l.busy||at3<l.at||(l.busy=!0,lookOutside(bp).catch(()=>{}).finally(()=>{l.busy=!1,l.at=Date.now()+OUTSIDE_LOOK_MS}))}async function lookOutside(bp){let meta=readMeta(bp),out20={asked:0,merged:[]};if(meta.engine!=="live")return out20;for(let t of listTickets(bp)){if(!isMine(t,meta)||mergeWait(t))continue;let known=openPr(t),key=`${bp.squadDir}\0${t.id}`;if(known?t.prClosed===known.id:notFound.has(key)||!mayHaveClashedPr(bp,t))continue;let target=repoTarget(bp,t),host3=target&&repoHostFor(bp,target.url);if(!(!target||!host3))try{let pr=known??await clashedByBranch(bp,t,host3);if(!pr){notFound.add(key);continue}if(pr.id===t.prClosed)continue;out20.asked+=1;let state=await host3.mergeState(pr.id);state==="merged"&&await recordMerged(bp,t.id,pr,host3,target)?out20.merged.push(t.id):state==="closed"&&saidClosed(bp,t.id,pr,host3,target.base)}catch{}}return out20}import fs127 from"fs";import os21 from"os";import path118 from"path";var CONVERSATIONS=[{key:"intake",label:"Analyst and Architect"},{key:"build",label:"Builder"},{key:"review",label:"Reviewer"},{key:"tester",label:"Tester"}];function blockedMeta(output){let found2=blockedIn(output);return found2.length?{squadKit:{blocked:found2.map(b=>blockedLine(b))}}:null}function withBlocked(updates){return updates.map(e=>{let u=e.update,meta=u.sessionUpdate==="tool_call_update"?blockedMeta(outputOf(u)):null;return meta?{...e,update:{...u,_meta:{...u._meta??{},...meta}}}:e})}function sessionFile2(id2,base2=path118.join(os21.homedir(),".claude","projects")){if(!/^[\w-]{8,64}$/.test(id2))return null;try{for(let d of fs127.readdirSync(base2)){let file3=path118.join(base2,d,`${id2}.jsonl`);if(fs127.existsSync(file3))return file3}}catch{}return null}var SYSTEM_PROMPT=/^\s*(<task-notification|<system-reminder|\[structured-output-enforce\]|<command-|Caveat:)/,MAX_TEXT=2e4,MAX_RESULT=4e3,MAX_IMAGE=3e6,MEDIA={png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",gif:"image/gif",webp:"image/webp",svg:"image/svg+xml",mp4:"video/mp4",webm:"video/webm",mov:"video/quicktime",mp3:"audio/mpeg",wav:"audio/wav",ogg:"audio/ogg",weba:"audio/webm",opus:"audio/ogg",oga:"audio/ogg",m4a:"audio/mp4",aac:"audio/aac",pdf:"application/pdf",csv:"text/csv",json:"application/json",md:"text/markdown",txt:"text/plain",html:"text/html",zip:"application/zip",xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",docx:"application/vnd.openxmlformats-officedocument.wordprocessingml.document",har:"application/json",log:"text/plain"};function dispositionOf(kind,file3){let name=path118.basename(file3);return/^[\x20-\x7e]+$/.test(name)?`${kind}; filename="${name.replace(/"/g,"")}"`:`${kind}; filename*=UTF-8''${encodeURIComponent(name)}`}function mimeOf(file3){return MEDIA[path118.extname(file3).slice(1).toLowerCase()]??"application/octet-stream"}var SHOWN=/\.(png|jpe?g|gif|webp|svg|mp4|webm|mov|mp3|wav|ogg|pdf|csv|xlsx|docx|zip|har)$/i;function clip(s,max){let t=s.trim();return t.length>max?`${t.slice(0,max)}
1611
1611
  \u2026 (${t.length-max} more characters)`:t}function text2(t,meta){return{type:"text",text:t,...meta?{_meta:meta}:{}}}function kindOf3(name){switch(name){case"Read":return"read";case"Edit":case"MultiEdit":case"Write":case"NotebookEdit":return"edit";case"Glob":case"Grep":case"LS":return"search";case"Bash":return"execute";case"WebFetch":case"WebSearch":return"fetch";case"TodoWrite":return"think";default:return"other"}}function diffsOf(name,input7){let file3=typeof input7.file_path=="string"?input7.file_path:"";if(!file3)return[];let str4=v=>typeof v=="string"?clip(v,MAX_TEXT):"";return name==="Edit"?[{type:"diff",path:file3,oldText:str4(input7.old_string),newText:str4(input7.new_string)}]:name==="Write"?[{type:"diff",path:file3,oldText:null,newText:str4(input7.content)}]:name==="MultiEdit"&&Array.isArray(input7.edits)?input7.edits.map(e=>({type:"diff",path:file3,oldText:str4(e.old_string),newText:str4(e.new_string)})):[]}function filesIn(textToScan,cwd){let found2=new Set;for(let m of textToScan.matchAll(/(?:^|[\s"'=(:])((?:\/|\.{0,2}\/)?[\w@./-]+\.(?:png|jpe?g|gif|webp|svg|mp4|webm|mov|mp3|wav|ogg|pdf|csv|xlsx|docx|zip|har))\b/gi)){let p=m[1],abs=path118.isAbsolute(p)?p:cwd?path118.resolve(cwd,p):"";if(abs&&SHOWN.test(abs)&&fs127.existsSync(abs)&&found2.add(abs),found2.size>=12)break}return[...found2].map(abs=>({type:"content",content:{type:"resource_link",uri:`file://${abs}`,name:path118.basename(abs),mimeType:mimeOf(abs),size:fs127.statSync(abs).size}}))}function resultContent(content){if(typeof content=="string")return content.trim()?[{type:"content",content:text2(clip(content,MAX_RESULT))}]:[];if(!Array.isArray(content))return[];let out20=[];for(let c of content)c?.type==="text"&&c.text?.trim()&&out20.push({type:"content",content:text2(clip(c.text,MAX_RESULT))}),c?.type==="image"&&c.source?.data&&out20.push({type:"content",content:c.source.data.length<=MAX_IMAGE?{type:"image",data:c.source.data,mimeType:c.source.media_type??"image/png"}:text2("(a large image was left out)")});return out20}function planOf2(input7){return Array.isArray(input7.todos)?input7.todos.map(t=>({content:String(t.content??""),priority:"medium",status:t.status==="completed"?"completed":t.status==="in_progress"?"in_progress":"pending"})):[]}function readConversation(id2,limit=400,base2,nativeDir){let native=nativeDir&&/^[\w-]{8,64}$/.test(id2)?path118.join(nativeDir,`${id2}.jsonl`):null;if(native&&fs127.existsSync(native)){let updates2=[];for(let raw of fs127.readFileSync(native,"utf8").split(`
1612
1612
  `))try{raw.trim()&&updates2.push(JSON.parse(raw))}catch{}let cwd2=cwdOf(id2,base2);return{sessionId:id2,...cwd2?{cwd:cwd2}:{},updates:withBlocked(updates2.slice(-limit)),total:updates2.length}}let file3=sessionFile2(id2,base2);if(!file3)return null;let updates=[],toolFiles=new Map,toolContent=new Map,answers2=new Set,cwd;for(let raw of fs127.readFileSync(file3,"utf8").split(`
1613
1613
  `)){if(!raw.trim())continue;let line3;try{line3=JSON.parse(raw)}catch{continue}if(line3.isSidechain||line3.type!=="user"&&line3.type!=="assistant")continue;cwd??=line3.cwd;let at3=line3.timestamp,push=update=>updates.push({...at3?{at:at3}:{},update}),content=line3.message?.content,blocks2=typeof content=="string"?[{type:"text",text:content}]:content??[];for(let b of blocks2)if(b.type==="text"&&b.text?.trim())if(line3.type==="user"){let from=SYSTEM_PROMPT.test(b.text)?"claude-code":"squad-kit";push({sessionUpdate:"user_message_chunk",content:text2(clip(b.text,MAX_TEXT),{squadKit:{from}})})}else push({sessionUpdate:"agent_message_chunk",content:text2(clip(b.text,MAX_TEXT))});else if(b.type==="thinking"&&b.thinking?.trim())push({sessionUpdate:"agent_thought_chunk",content:text2(clip(b.thinking,MAX_RESULT))});else if(b.type==="tool_use"&&b.name&&b.id){let input7=b.input??{};if(b.name==="TodoWrite"){let entries=planOf2(input7);entries.length&&push({sessionUpdate:"plan",entries});continue}b.name==="StructuredOutput"&&answers2.add(b.id);let title2=b.name==="StructuredOutput"?"Handed in its answer":describeToolEvent({name:b.name,input:input7},cwd??"/")??`Used ${b.name}`,filePath=typeof input7.file_path=="string"?input7.file_path:typeof input7.path=="string"?input7.path:"",command=typeof input7.command=="string"?input7.command:"";command&&toolFiles.set(b.id,command);let callContent=[...diffsOf(b.name,input7),...b.name!=="Read"&&filePath&&SHOWN.test(filePath)?filesIn(filePath,cwd):[]];toolContent.set(b.id,callContent),push({sessionUpdate:"tool_call",toolCallId:b.id,title:title2,kind:kindOf3(b.name),status:"in_progress",content:callContent,...filePath?{locations:[{path:filePath}]}:{},rawInput:command?{command}:filePath?{path:filePath}:{}})}else if(b.type==="tool_result"&&b.tool_use_id){let resultText=typeof b.content=="string"?b.content:Array.isArray(b.content)?b.content.map(c=>c.text??"").join(`
@@ -1615,7 +1615,7 @@ Then carry on and hand in your answer again${found2.q.at==="question"?" without
1615
1615
  ${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?fs127.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 started2=!!sessionFile2(id2,base2)||!!(nativeDir&&fs127.existsSync(path118.join(nativeDir,`${id2}.jsonl`)));return[{key:c.key,label:c.label,sessionId:id2,started:started2}]})}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=fs127.realpathSync(file3);return folders.some(f=>{try{let root=fs127.realpathSync(f);return real2===root||real2.startsWith(root+path118.sep)}catch{return!1}})&&fs127.statSync(real2).isFile()?real2:null}catch{return null}}function chatSummaries(tickets,q5=""){let needle=q5.trim().toLowerCase(),out20=[];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()}`}out20.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 out20.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,run7){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=run7;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 fs128 from"fs";import path119 from"path";var ProductError=class extends Error{constructor(message,detail){super(message);this.detail=detail}detail};function mainCopyDir(bp,repo){return path119.join(productCopiesDir(bp),repo.replace(/[^\w.-]+/g,"-"))}var productCopiesDir=bp=>path119.join(bp.localDir,"product");function changedParts(meta,t){if(t.parts?.length)return t.parts;let repo=repoFor(meta,t).path;return partsOf2(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&&fs128.existsSync(mark)&&fs128.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}
1616
1616
  ${tail(r.output,30)}`);await fillOwnCopy(bp,repo.path,dir),fs128.writeFileSync(mark,print)}var running4=new Map,keyOf3=(bp,holder)=>`${bp.root}
1617
1617
  ${holder}`;function stopProduct(bp,holder){let key=keyOf3(bp,holder);for(let{app}of running4.get(key)??[])app.stop();running4.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),list6=running4.get(key)??[];running4.set(key,list6);try{let front;for(let place of plan.places){place.fromStory||await readyMainCopy(bp,t,place,o.signal);let was=list6.find(r=>r.repo===place.repo.path);was&&was.app.ended?.()&&list6.splice(list6.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),list6.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(hold2){if(hold2.kind==="apps")return"Waiting for Your apps: they use the same addresses. Stop them, and this goes on.";let who4=hold2.holder.replace(/^try:/,"");return hold2.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&&!fits3(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 fits3(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 list6=value?.dependsOn;if(list6==null)return null;if(!Array.isArray(list6))return"dependsOn is not a list";let known=new Set(open3.map(x=>x.toUpperCase()));for(let item of list6){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(q5){return q5.port?{fact:{label:"What is in the way",value:portWords(q5.port)},...q5.port.ours?{freePort:{port:q5.port.port,what:portHolder(q5.port)}}:{}}:{}}function slowDetail(q5){let lines3=[q5.command?`Waiting on: ${q5.command}${q5.pid?` (process ${q5.pid})`:""}`:"",q5.port?portDetail(q5.port):""].filter(Boolean);return lines3.length?lines3.join(`
1618
- `):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(q5=>q5.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),done2=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),running5=t.clock&&trackStageOf(t.stage)===stage?runSeconds(t.clock,at3):void 0;return running5!==void 0?done2+running5:runs2.length?done2: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)),about2=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=about2(t.track==="express"||t.track==="auto"?usual("architect","design"):usual("analyst","spec")+usual("architect","design")):s==="implement"?note2=about2((t.work?.steps??t.steps?.length??0)*usual("builder","build")):s==="test"&&(note2=about2(usual("squad","checks")));return{stage:s,note:note2,...wait?{wait}:{}}})}function noteWhenWords(t,now3){switch(now3.state){case"working":{let left2=now3.usual?Math.max(60,middle(now3.usual)-(now3.seconds??0)):60;return now3.role==="squad"?`squad-kit is running the checks. The next agent reads it when its step starts, ${inAbout(left2)}.`:`The ${now3.who} reads it when its next step starts, ${inAbout(left2)}.`}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 q5=t.quiet;if(!q5)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(q5),detail=slowDetail(q5);if(q5.kind==="stuck"){let quiet=Math.max(0,(at3-Date.parse(q5.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:q5.detail},...port.fact?[port.fact]:[],{label:"Your work",value:"Safe. Everything done so far is kept."}],...limit?{limit}:{},canEnd:!!q5.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:q5.detail,facts,...limit?{limit}:{},canEnd:!!q5.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),left2=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(left2)}. Nothing is needed from you now.`:`${then} You will be asked to ${go.what} ${inAbout(left2)}. Nothing is needed from you now.`,now3={...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:left2,what:go.what},noteWhen:""};now3.noteWhen=noteWhenWords(t,now3);let slow=slowCard(t,t.clock??{...c,doing:raw||void 0},part,lastAt,at3);return slow&&(now3.slow=slow),now3}import path120 from"path";var PhoneUnavailable=class extends Error{constructor(message,detail){super(message);this.detail=detail}detail},STOPPED10="You stopped the squad.",NO_LINKS="This app has no links of its own, so squad-kit opened its start screen: use steps to reach the screen you need.",DEFAULT_LANGUAGE="en",sleep5=ms2=>new Promise(r=>setTimeout(r,ms2));function within2(p,ms2,what){let timer,late2=new Promise((_,reject)=>{timer=setTimeout(()=>reject(new StepFailure("timeout",`${what} took longer than ${Math.round(ms2/1e3)} seconds`)),ms2)});return Promise.race([p,late2]).finally(()=>clearTimeout(timer))}function linkFor(scheme,page){return`${scheme}://${page.replace(/^\/+/,"")}`}var drivers=new Map,started=new Map,warmed=new Set,installed2=new Map;function dropDriver(id2){let d=drivers.get(id2);drivers.delete(id2),started.delete(id2),warmed.delete(id2),d?.then(x=>x.close()).catch(()=>{})}onDown(dropDriver);process.once("exit",()=>{for(let d of started.values())d.close()});function closeDrivers(){for(let id2 of[...drivers.keys()])dropDriver(id2)}async function driverFor(id2,tools4){let have=drivers.get(id2);if(have){let d=await have.catch(()=>null);if(d?.alive)return d;drivers.delete(id2)}let opening=phoneDriver().open({device:id2,binary:tools4.maestro,...tools4.javaHome?{javaHome:tools4.javaHome}:{},dir:path120.join(phonesHome(),"maestro")});return drivers.set(id2,opening),opening.then(d=>started.set(id2,d)).catch(()=>drivers.delete(id2)),opening}async function failed(step2,at3,err2,page,driver,stepMs){let words2=err2 instanceof StepFailure?failureWords(step2,err2,stepMs):{why:err2?.message??String(err2),plain:"The app did not react as expected on the phone."},snapshot="";try{snapshot=driver?trimSnapshot(await driver.screenText(6e3)):""}catch{snapshot=""}return{step:at3,what:at3&&step2?stepWords2(step2):`open ${page}`,...words2,page,snapshot}}var PhoneSession=class{constructor(s,phone=shotPhone()){this.s=s;this.phone=phone}s;phone;readying=null;app=null;devices={};closed=!1;get sizes(){return this.s.sizes??["phone","tablet"]}labels(){return Object.fromEntries(Object.entries(this.devices).map(([k,d])=>[k,d.model]))}links(){return this.app?.info.schemes??[]}get appId(){return this.app?.info.appId}ready(){return this.closed?Promise.reject(new Error(STOPPED10)):(this.readying||(this.readying=this.getReady(),this.readying.catch(()=>this.readying=null)),this.readying)}async getReady(){let o=this.s;try{let app=o.app;app||(app=(await buildForPhone(o.cwd,o.kind,{say:o.say,...o.log?{log:o.log}:{},...o.signal?{signal:o.signal}:{}})).app);let info2=await this.phone.appInfo(app);this.app={path:app,info:info2};for(let size2 of this.sizes)this.devices[size2]=await ownDevice(this.phone,size2,o.runtime?{runtime:o.runtime}:{});await withPhone(this.phone,this.sizes[0],async turn2=>{turn2.fresh&&dropDriver(turn2.device.id),await this.installOn(turn2.device.id,turn2.fresh),await this.warm(turn2.device.id)},this.turnOptions())}catch(err2){throw o.signal?.aborted||this.closed?new Error(STOPPED10):err2 instanceof BuildFailed||err2 instanceof PhoneError?new PhoneUnavailable(err2.message,err2.detail):new PhoneUnavailable("squad-kit could not get its iPhone ready.",err2?.stack??String(err2))}}turnOptions(){return{...this.s.runtime?{runtime:this.s.runtime}:{},say:this.s.say,...this.s.log?{log:this.s.log}:{},...this.s.signal?{signal:this.s.signal}:{}}}async installOn(id2,fresh){let app=this.app,key=`${id2}:${app.info.appId}`;if(!fresh&&installed2.get(key)===app.path)return;let t0=Date.now();this.s.say(`Putting the app on squad-kit's ${this.phone.words.phone}, ${aboutWords(stepSeconds2("install"))}`),await this.phone.install(id2,app.path,this.s.signal?{signal:this.s.signal}:{}),noteStep("install",(Date.now()-t0)/1e3),installed2.set(key,app.path),this.s.log?.(`squad-kit put ${app.path} on its phone ${id2} in ${Math.round((Date.now()-t0)/1e3)} s`)}async warm(id2){let driver=await driverFor(id2,this.s.tools);if(warmed.has(id2))return driver;this.s.say(`Getting squad-kit's ${this.phone.words.phone} ready to tap through the app, ${aboutWords(stepSeconds2("first-run"))}`);let t0=Date.now();return await driver.screenText(18e4),noteStep("first-run",(Date.now()-t0)/1e3),warmed.add(id2),driver}async shoot(ask2,c){return await this.walk(ask2,c,!0)}async look(ask2,c){let r=await this.walk(ask2,c,!1);return"step"in r?r:{page:r.page,snapshot:r.snapshot,device:r.device,...r.said?{said:r.said}:{}}}async walk(ask2,c,picture){await this.ready();let size2=ask2.size??"phone";if(!this.sizes.includes(size2))throw new PhoneUnavailable(`squad-kit has no ${size2} for this app.`,`Sizes: ${this.sizes.join(", ")}`);let app=this.app,appId=app.info.appId,language=ask2.language?.trim()||DEFAULT_LANGUAGE;return withPhone(this.phone,size2,async turn2=>{let id2=turn2.device.id,device=turn2.device.model;if(turn2.fresh&&dropDriver(id2),c.signal?.aborted||this.closed)throw new Error(STOPPED10);let page="/",said,driver=null,at3=0,step2=null,open3=async p=>{if(p==="/"||!p.trim())return;let scheme=app.info.schemes[0];if(!scheme){said=NO_LINKS;return}await this.phone.openLink(id2,linkFor(scheme,p)),page=p};try{await this.installOn(id2,turn2.fresh),driver=await this.warm(id2),await this.phone.terminate(id2,appId),await this.phone.launch(id2,appId,{language}),await open3(ask2.page),await settle(driver);let stepMs=c.stepMs,typed=!1;for(let[i,raw]of(ask2.steps??[]).entries()){if(at3=i+1,step2={...raw,...raw.target?{target:oneWay(raw.target)}:{},...raw.action==="wait"&&typeof raw.ms=="number"?{ms:Math.max(0,Math.min(raw.ms,MAX_PAUSE_MS))}:{}},Date.now()>=c.deadline)throw new StepFailure("timeout","The call ran out of time");let ms2=Math.max(1e3,Math.min(stepMs,c.deadline-Date.now()));if(step2.action==="goto"){await open3(step2.path??"/"),await settle(driver);continue}driver=await this.tapped(id2,appId,driver,step2,ms2),typed=step2.action==="fill"||step2.action==="press"&&step2.key!=="Escape"&&step2.key!=="Enter"}if(at3=0,step2=null,typed&&await driver.act(appId,{action:"press",key:"Escape"},8e3).catch(()=>{}),await settle(driver),!picture)return{page,snapshot:trimSnapshot(await driver.screenText(1e4)),device,...said?{said}:{}};let png=await within2(this.phone.screenshot(id2),3e4,"taking the picture");return png.length?{png,page,device,...said?{said}:{}}:{step:0,what:"save the picture",why:"the phone gave no picture",plain:"squad-kit could not save the picture.",page,snapshot:""}}catch(err2){if(c.signal?.aborted||this.s.signal?.aborted||this.closed)throw new Error(STOPPED10);return failed(step2,at3,err2,page,driver,c.stepMs)}},this.turnOptions())}async tapped(id2,appId,driver,step2,ms2){try{return await within2(driver.act(appId,step2,ms2),ms2+4e4,stepWords2(step2)),driver}catch(err2){if(err2 instanceof StepFailure||driver.alive)throw err2;this.s.log?.(`Maestro ended on squad-kit's phone ${id2}; squad-kit starts it again: ${err2.message}`),dropDriver(id2);let again2=await this.warm(id2);return await within2(again2.act(appId,step2,ms2),ms2+4e4,stepWords2(step2)),again2}}async close(){this.closed=!0}};async function settle(driver){let until=Date.now()+6e3,last="";for(;Date.now()<until;){let now3=await driver.screenText(5e3).catch(()=>"");if(now3&&now3===last)return;last=now3,await sleep5(500)}}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,now3=Date.now()){let key=`${bp.root}:${id2}`;if(now3-(looked2.get(key)??0)<LOOK_MS)return;looked2.set(key,now3);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 now3=requireTicket(bp,t.id);return now3.live||now3.squad?restartStep(bp,t.id):now3}import fs129 from"fs";import path121 from"path";var troubles2=new Map,checked2=new Map,SAVE_CHECK_MS=5e3,TEST_WRITE=Buffer.alloc(64*1024,32);function canSave(bp){if(!fs129.existsSync(bp.squadDir))return!1;for(let dir of[bp.squadDir,bp.localDir]){let file3=path121.join(dir,`.save-check-${process.pid}`);try{fs129.mkdirSync(dir,{recursive:!0}),fs129.writeFileSync(file3,TEST_WRITE),fs129.rmSync(file3,{force:!0})}catch{try{fs129.rmSync(file3,{force:!0})}catch{}return!1}}return!0}function isSaveTrouble(bp,err2){return fs129.existsSync(bp.squadDir)?isFileTrouble(err2)?!canSave(bp):!1:!0}function noteSaveTrouble(bp,err2){if(troubles2.has(bp.squadDir))return!1;let e=asError(err2);return troubles2.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[...troubles2.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 troubles2.has(bp.squadDir)?at3-(checked2.get(bp.squadDir)??0)<SAVE_CHECK_MS?{held:!0,fixed:!1}:(checked2.set(bp.squadDir,at3),canSave(bp)?(troubles2.delete(bp.squadDir),checked2.delete(bp.squadDir),{held:!1,fixed:!0}):{held:!0,fixed:!1}):{held:!1,fixed:!1}}function forgetSaveTrouble(bp){troubles2.delete(bp.squadDir),checked2.delete(bp.squadDir),turnTroubles.delete(bp.squadDir)}import fs132 from"fs";import path124 from"path";import fs130 from"fs";import path122 from"path";var SETUP_STEPS=["files","tool","read","commands","tests","app","overview"];function talkFile(bp){return path122.join(bp.localDir,"overview-talk.json")}function readTalk(bp){try{let t=JSON.parse(fs130.readFileSync(talkFile(bp),"utf8"));return Array.isArray(t.messages)?t:null}catch{return null}}function writeTalk(bp,t){fs130.mkdirSync(path122.dirname(talkFile(bp)),{recursive:!0});let tmp=`${talkFile(bp)}.${process.pid}.tmp`;fs130.writeFileSync(tmp,`${JSON.stringify(t,null,1)}
1618
+ `):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(q5=>q5.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),done2=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),running5=t.clock&&trackStageOf(t.stage)===stage?runSeconds(t.clock,at3):void 0;return running5!==void 0?done2+running5:runs2.length?done2: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)),about2=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=about2(t.track==="express"||t.track==="auto"?usual("architect","design"):usual("analyst","spec")+usual("architect","design")):s==="implement"?note2=about2((t.work?.steps??t.steps?.length??0)*usual("builder","build")):s==="test"&&(note2=about2(usual("squad","checks")));return{stage:s,note:note2,...wait?{wait}:{}}})}function noteWhenWords(t,now3){switch(now3.state){case"working":{let left2=now3.usual?Math.max(60,middle(now3.usual)-(now3.seconds??0)):60;return now3.role==="squad"?`squad-kit is running the checks. The next agent reads it when its step starts, ${inAbout(left2)}.`:`The ${now3.who} reads it when its next step starts, ${inAbout(left2)}.`}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."}}var CHECK_SAID={test:{name:"The tests",it:"them",passes:"they pass"},lint:{name:"The code check",it:"it",passes:"it passes"},build:{name:"The build",it:"it",passes:"it passes"}},timesWords=n=>n===1?"once":`${n} times`;function slowWhy(t,c=t.clock,q5=t.quiet){let f=c?.failed;if(f){let w=CHECK_SAID[f.check];return f.fixed?{kind:"own-fixed",short:lowerFirst2(FIXING[f.check]).replace(/^fixing/,"fixed"),sentence:`${w.name} failed ${timesWords(f.times)} on the way. The Builder fixed ${w.it}, and ${w.passes} now.`}:{kind:"own-fail",short:lowerFirst2(FIXING[f.check]),sentence:`${w.name} failed ${timesWords(f.times)}, so the Builder is fixing ${w.it}.`}}let tries=t.tries??0;return c?.who==="builder"&&tries>0?{kind:"checks",short:`fixing the checks (try ${tries+1} of 3)`,sentence:`The checks failed ${timesWords(tries)}, so the Builder is fixing them (try ${tries+1} of 3).`}:c?.who==="reviewer"&&(t.reviewRound??0)>1?{kind:"review",short:`review round ${t.reviewRound} of 2`,sentence:`The Reviewer asked for changes, so this is its second look (round ${t.reviewRound} of 2).`}:c&&(c.running==="setup"||c.doing===DOING.setup)?{kind:"setup",short:lowerFirst2(DOING.setup),sentence:"This story's copy is being set up (packages and the like), which can take a few minutes."}:q5?.busy?{kind:"busy",short:"your computer is busy",sentence:"Your computer is busy, so steps run slower."}:{kind:"bigger",short:"a bigger step than usual",sentence:"Nothing failed on the way, so it is most likely a bigger step than usual."}}var STILL_WORKING_S=180;function stillWorking(c,lastAt,at3=Date.now()){return c.who==="squad"||c.running?!0:!!lastAt&&(at3-Date.parse(lastAt))/1e3<STILL_WORKING_S}function limitWords2(c,part,at3=Date.now()){if(!c.limit)return;if(c.who==="squad")return`squad-kit gives each check up to ${c.limit} min. One that runs longer is stopped and counts as not passed.`;let asks="If it isn't done by then, squad-kit stops it and asks you what to do.";if(c.who==="builder"&&c.step==="build"&&part){let used=Math.floor(runSeconds({...c,since:c.since??new Date(at3).toISOString()},at3)/60);return`squad-kit gives the Builder up to ${c.limit} min for all its steps together${used>=1?` (${used} min used so far)`:""}. ${asks}`}return`squad-kit gives this step up to ${c.limit} min. ${asks}`}function slowActivity(t,q5,at3=Date.now()){let c=t.clock;if(!c)return`Taking longer than usual: ${q5.detail}`;let why=slowWhy(t,c,q5);return`Taking longer than usual: ${lowerFirst2(why.sentence)} ${stillWorking(c,c.doingAt??c.since,at3)?"Still working.":q5.detail}`}var RUNNING_SAID={test:"the tests",lint:"the code check",build:"the build",setup:"the copy's setup"};function slowCard(t,c,part,lastAt,at3,seconds2){let q5=t.quiet;if(!q5)return;let who4=WHO[c.who],where=`${t.id} \xB7 ${part?`step ${part.n} of ${part.of}`:who4}`,limit=limitWords2(c,part,at3),port=portParts(q5),detail=slowDetail(q5);if(q5.kind==="stuck"){let quiet=Math.max(0,(at3-Date.parse(q5.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:q5.detail},...port.fact?[port.fact]:[],{label:"Your work",value:"Safe. Everything done so far is kept."}],...limit?{limit}:{},canEnd:!!q5.pid,...port.freePort?{freePort:port.freePort}:{},...detail?{detail}:{}}}let why=slowWhy(t,c,q5),calm=stillWorking(c,lastAt,at3),ago3=lastAt?Math.max(0,(at3-Date.parse(lastAt))/1e3):void 0,facts=[{label:"How long",value:q5.detail}];if(c.doing&&facts.push({label:"Doing now",value:upperFirst(c.doing)}),ago3!==void 0){let still2=ago3<STILL_WORKING_S?", so it is still working":c.running?`, and squad-kit is still running ${RUNNING_SAID[c.running]} for it`:"";facts.push({label:"Last progress",value:`${minutesWords(ago3)} ago${still2}`})}limit&&facts.push({label:"Time limit",value:limit}),facts.push({label:"Your part",value:"You don't need to do anything. If it gets stuck, it will show in Waiting for you."}),port.fact&&facts.push(port.fact);let line3=["Longer than usual",seconds2!==void 0?durationWords(seconds2):"",calm?"still working":ago3!==void 0?`no update for ${durationWords(ago3)}`:""].filter(Boolean).join(" \xB7 "),note2=why.kind!=="bigger"&&why.short!==(c.doing?shortDoing(c.doing):"")?upperFirst(why.short):void 0;return{kind:"long",title:"Taking longer than usual",where,text:why.sentence,facts,...limit?{limit}:{},why,calm,line:line3,...note2?{note:note2}:{},canEnd:!!q5.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),left2=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(left2)}. Nothing is needed from you now.`:`${then} You will be asked to ${go.what} ${inAbout(left2)}. Nothing is needed from you now.`,now3={...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:left2,what:go.what},noteWhen:""};now3.noteWhen=noteWhenWords(t,now3);let slow=slowCard(t,t.clock??{...c,doing:raw||void 0},part,lastAt,at3,seconds2);return slow&&(now3.slow=slow),now3}import path120 from"path";var PhoneUnavailable=class extends Error{constructor(message,detail){super(message);this.detail=detail}detail},STOPPED10="You stopped the squad.",NO_LINKS="This app has no links of its own, so squad-kit opened its start screen: use steps to reach the screen you need.",DEFAULT_LANGUAGE="en",sleep5=ms2=>new Promise(r=>setTimeout(r,ms2));function within2(p,ms2,what){let timer,late2=new Promise((_,reject)=>{timer=setTimeout(()=>reject(new StepFailure("timeout",`${what} took longer than ${Math.round(ms2/1e3)} seconds`)),ms2)});return Promise.race([p,late2]).finally(()=>clearTimeout(timer))}function linkFor(scheme,page){return`${scheme}://${page.replace(/^\/+/,"")}`}var drivers=new Map,started=new Map,warmed=new Set,installed2=new Map;function dropDriver(id2){let d=drivers.get(id2);drivers.delete(id2),started.delete(id2),warmed.delete(id2),d?.then(x=>x.close()).catch(()=>{})}onDown(dropDriver);process.once("exit",()=>{for(let d of started.values())d.close()});function closeDrivers(){for(let id2 of[...drivers.keys()])dropDriver(id2)}async function driverFor(id2,tools4){let have=drivers.get(id2);if(have){let d=await have.catch(()=>null);if(d?.alive)return d;drivers.delete(id2)}let opening=phoneDriver().open({device:id2,binary:tools4.maestro,...tools4.javaHome?{javaHome:tools4.javaHome}:{},dir:path120.join(phonesHome(),"maestro")});return drivers.set(id2,opening),opening.then(d=>started.set(id2,d)).catch(()=>drivers.delete(id2)),opening}async function failed(step2,at3,err2,page,driver,stepMs){let words2=err2 instanceof StepFailure?failureWords(step2,err2,stepMs):{why:err2?.message??String(err2),plain:"The app did not react as expected on the phone."},snapshot="";try{snapshot=driver?trimSnapshot(await driver.screenText(6e3)):""}catch{snapshot=""}return{step:at3,what:at3&&step2?stepWords3(step2):`open ${page}`,...words2,page,snapshot}}var PhoneSession=class{constructor(s,phone=shotPhone()){this.s=s;this.phone=phone}s;phone;readying=null;app=null;devices={};closed=!1;get sizes(){return this.s.sizes??["phone","tablet"]}labels(){return Object.fromEntries(Object.entries(this.devices).map(([k,d])=>[k,d.model]))}links(){return this.app?.info.schemes??[]}get appId(){return this.app?.info.appId}ready(){return this.closed?Promise.reject(new Error(STOPPED10)):(this.readying||(this.readying=this.getReady(),this.readying.catch(()=>this.readying=null)),this.readying)}async getReady(){let o=this.s;try{let app=o.app;app||(app=(await buildForPhone(o.cwd,o.kind,{say:o.say,...o.log?{log:o.log}:{},...o.signal?{signal:o.signal}:{}})).app);let info2=await this.phone.appInfo(app);this.app={path:app,info:info2};for(let size2 of this.sizes)this.devices[size2]=await ownDevice(this.phone,size2,o.runtime?{runtime:o.runtime}:{});await withPhone(this.phone,this.sizes[0],async turn2=>{turn2.fresh&&dropDriver(turn2.device.id),await this.installOn(turn2.device.id,turn2.fresh),await this.warm(turn2.device.id)},this.turnOptions())}catch(err2){throw o.signal?.aborted||this.closed?new Error(STOPPED10):err2 instanceof BuildFailed||err2 instanceof PhoneError?new PhoneUnavailable(err2.message,err2.detail):new PhoneUnavailable("squad-kit could not get its iPhone ready.",err2?.stack??String(err2))}}turnOptions(){return{...this.s.runtime?{runtime:this.s.runtime}:{},say:this.s.say,...this.s.log?{log:this.s.log}:{},...this.s.signal?{signal:this.s.signal}:{}}}async installOn(id2,fresh){let app=this.app,key=`${id2}:${app.info.appId}`;if(!fresh&&installed2.get(key)===app.path)return;let t0=Date.now();this.s.say(`Putting the app on squad-kit's ${this.phone.words.phone}, ${aboutWords(stepSeconds2("install"))}`),await this.phone.install(id2,app.path,this.s.signal?{signal:this.s.signal}:{}),noteStep("install",(Date.now()-t0)/1e3),installed2.set(key,app.path),this.s.log?.(`squad-kit put ${app.path} on its phone ${id2} in ${Math.round((Date.now()-t0)/1e3)} s`)}async warm(id2){let driver=await driverFor(id2,this.s.tools);if(warmed.has(id2))return driver;this.s.say(`Getting squad-kit's ${this.phone.words.phone} ready to tap through the app, ${aboutWords(stepSeconds2("first-run"))}`);let t0=Date.now();return await driver.screenText(18e4),noteStep("first-run",(Date.now()-t0)/1e3),warmed.add(id2),driver}async shoot(ask2,c){return await this.walk(ask2,c,!0)}async look(ask2,c){let r=await this.walk(ask2,c,!1);return"step"in r?r:{page:r.page,snapshot:r.snapshot,device:r.device,...r.said?{said:r.said}:{}}}async walk(ask2,c,picture){await this.ready();let size2=ask2.size??"phone";if(!this.sizes.includes(size2))throw new PhoneUnavailable(`squad-kit has no ${size2} for this app.`,`Sizes: ${this.sizes.join(", ")}`);let app=this.app,appId=app.info.appId,language=ask2.language?.trim()||DEFAULT_LANGUAGE;return withPhone(this.phone,size2,async turn2=>{let id2=turn2.device.id,device=turn2.device.model;if(turn2.fresh&&dropDriver(id2),c.signal?.aborted||this.closed)throw new Error(STOPPED10);let page="/",said,driver=null,at3=0,step2=null,open3=async p=>{if(p==="/"||!p.trim())return;let scheme=app.info.schemes[0];if(!scheme){said=NO_LINKS;return}await this.phone.openLink(id2,linkFor(scheme,p)),page=p};try{await this.installOn(id2,turn2.fresh),driver=await this.warm(id2),await this.phone.terminate(id2,appId),await this.phone.launch(id2,appId,{language}),await open3(ask2.page),await settle(driver);let stepMs=c.stepMs,typed=!1;for(let[i,raw]of(ask2.steps??[]).entries()){if(at3=i+1,step2={...raw,...raw.target?{target:oneWay(raw.target)}:{},...raw.action==="wait"&&typeof raw.ms=="number"?{ms:Math.max(0,Math.min(raw.ms,MAX_PAUSE_MS))}:{}},Date.now()>=c.deadline)throw new StepFailure("timeout","The call ran out of time");let ms2=Math.max(1e3,Math.min(stepMs,c.deadline-Date.now()));if(step2.action==="goto"){await open3(step2.path??"/"),await settle(driver);continue}driver=await this.tapped(id2,appId,driver,step2,ms2),typed=step2.action==="fill"||step2.action==="press"&&step2.key!=="Escape"&&step2.key!=="Enter"}if(at3=0,step2=null,typed&&await driver.act(appId,{action:"press",key:"Escape"},8e3).catch(()=>{}),await settle(driver),!picture)return{page,snapshot:trimSnapshot(await driver.screenText(1e4)),device,...said?{said}:{}};let png=await within2(this.phone.screenshot(id2),3e4,"taking the picture");return png.length?{png,page,device,...said?{said}:{}}:{step:0,what:"save the picture",why:"the phone gave no picture",plain:"squad-kit could not save the picture.",page,snapshot:""}}catch(err2){if(c.signal?.aborted||this.s.signal?.aborted||this.closed)throw new Error(STOPPED10);return failed(step2,at3,err2,page,driver,c.stepMs)}},this.turnOptions())}async tapped(id2,appId,driver,step2,ms2){try{return await within2(driver.act(appId,step2,ms2),ms2+4e4,stepWords3(step2)),driver}catch(err2){if(err2 instanceof StepFailure||driver.alive)throw err2;this.s.log?.(`Maestro ended on squad-kit's phone ${id2}; squad-kit starts it again: ${err2.message}`),dropDriver(id2);let again2=await this.warm(id2);return await within2(again2.act(appId,step2,ms2),ms2+4e4,stepWords3(step2)),again2}}async close(){this.closed=!0}};async function settle(driver){let until=Date.now()+6e3,last="";for(;Date.now()<until;){let now3=await driver.screenText(5e3).catch(()=>"");if(now3&&now3===last)return;last=now3,await sleep5(500)}}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,now3=Date.now()){let key=`${bp.root}:${id2}`;if(now3-(looked2.get(key)??0)<LOOK_MS)return;looked2.set(key,now3);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 now3=requireTicket(bp,t.id);return now3.live||now3.squad?restartStep(bp,t.id):now3}import fs129 from"fs";import path121 from"path";var troubles2=new Map,checked2=new Map,SAVE_CHECK_MS=5e3,TEST_WRITE=Buffer.alloc(64*1024,32);function canSave(bp){if(!fs129.existsSync(bp.squadDir))return!1;for(let dir of[bp.squadDir,bp.localDir]){let file3=path121.join(dir,`.save-check-${process.pid}`);try{fs129.mkdirSync(dir,{recursive:!0}),fs129.writeFileSync(file3,TEST_WRITE),fs129.rmSync(file3,{force:!0})}catch{try{fs129.rmSync(file3,{force:!0})}catch{}return!1}}return!0}function isSaveTrouble(bp,err2){return fs129.existsSync(bp.squadDir)?isFileTrouble(err2)?!canSave(bp):!1:!0}function noteSaveTrouble(bp,err2){if(troubles2.has(bp.squadDir))return!1;let e=asError(err2);return troubles2.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[...troubles2.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 troubles2.has(bp.squadDir)?at3-(checked2.get(bp.squadDir)??0)<SAVE_CHECK_MS?{held:!0,fixed:!1}:(checked2.set(bp.squadDir,at3),canSave(bp)?(troubles2.delete(bp.squadDir),checked2.delete(bp.squadDir),{held:!1,fixed:!0}):{held:!0,fixed:!1}):{held:!1,fixed:!1}}function forgetSaveTrouble(bp){troubles2.delete(bp.squadDir),checked2.delete(bp.squadDir),turnTroubles.delete(bp.squadDir)}import fs132 from"fs";import path124 from"path";import fs130 from"fs";import path122 from"path";var SETUP_STEPS=["files","tool","read","commands","tests","app","overview"];function talkFile(bp){return path122.join(bp.localDir,"overview-talk.json")}function readTalk(bp){try{let t=JSON.parse(fs130.readFileSync(talkFile(bp),"utf8"));return Array.isArray(t.messages)?t:null}catch{return null}}function writeTalk(bp,t){fs130.mkdirSync(path122.dirname(talkFile(bp)),{recursive:!0});let tmp=`${talkFile(bp)}.${process.pid}.tmp`;fs130.writeFileSync(tmp,`${JSON.stringify(t,null,1)}
1619
1619
  `),fs130.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 running5=s.steps.find(x=>x.state==="now");if(running5)return running5;let read7=s.steps.find(x=>x.id==="read");return read7?.state==="failed"?read7: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),q5=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:q5(.25),high:q5(.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 fs131 from"fs";import path123 from"path";var STOPPED11="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(out20,idea){let ideas=idea?(out20.ideas??[]).filter(i=>i?.title&&!TECHNICAL_IDEA.test(`${i.title} ${i.why??""}`)):[];return{empty:!0,summary:out20.summary,map:[],decisions:[],commands:{},users:"",can:[],worth:[],ideas,parts:[],links:[]}}function withAllTests(dir,c){let safe2=fromTheRead(dir,c);for(let line3 of safe2.changed)writeLog({level:"info",text:line3});let test=withBrowserTests(dir,safe2.commands.test);return test?{...safe2.commands,test}:safe2.commands}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,read7=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)!==read7&&(s.reads=[...s.reads??[],read7].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===STOPPED11;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),out20=answer2.empty||fresh.setup?.kind==="empty"&&nextToNothing(bp.root)?noCodeAnswer(answer2,idea):answer2;fresh.map=out20.map??[],fresh.commands={...withAllTests(bp.root,clean2(out20.commands)),...clean2(fresh.commands??{})},nested.length&&(fresh.repos=nested.map(r=>{let found3=out20.repos?.find(x=>x.path.replace(/\/$/,"")===r.path),kept2=fresh.repos?.find(x=>x.path===r.path);return{...r,summary:found3?.summary??kept2?.summary,commands:{...withAllTests(path123.join(bp.root,r.path),clean2(found3?.commands??{})),...clean2(kept2?.commands??{})}}}));let parts2=partsAfterRead(cleanParts(out20.runs,fresh,bp.root),fresh.parts);if(parts2.length?fresh.parts=parts2:delete fresh.parts,fresh.scan={status:"done",at:at3,summary:out20.summary,empty:out20.empty},fresh.setup){let s=fresh.setup;if(finish(s,"read","done",{note:readWords(s.proof?.files)}),learnSetupTime(bp,stepOf(s,"read")),out20.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),out20.empty||learnAfterRead(bp);let before=readOverview(bp);writeOverview(bp,{...cleanOverview(out20),readAt:at3,...before.approved&&!fresh.setup?{approved:before.approved}:{}});let found2=(out20.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)&&fs131.rmSync(path123.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 path123.join(worktreeRoot(path123.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=path123.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,path123.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 run7=(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);fs131.existsSync(dir)&&await dropSnapshot(path123.join(bp.root,repo.path),dir)}});return droppingSetup.set(bp.squadDir,run7.catch(()=>{})),run7}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(STOPPED11);r.ok||failures.push({error:dockerBlockedWords(r.output,guardFor(bp,copy2.dir))??"Installing what it needs did not work.",output:`${setup}:
1620
1620
  ${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(STOPPED11);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 failed3=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},failed3.length){let f=failed3[0];finish(s,"tests","failed",{note:runs2.length>1?`${failed3.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}:
1621
1621
  ${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"),fs131.mkdirSync(bp.proofDir,{recursive:!0});let file3=path123.join(bp.proofDir,"setup-app.png");fs131.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=fs131.existsSync(file3)?path123.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()})},STEPS4={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(STOPPED11);await STEPS4[id2](bp,deps,copies)}catch(err2){let stopped=deps.signal.aborted||err2.message===STOPPED11;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 read7=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:read7,...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 q5=parseQuality(value);if(!q5)throw new BoardActionError(`"${value}" is not a Quality setting. Use best, balanced or thrifty.`);let meta=readMeta(bp);return meta.quality=q5,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:!fs132.existsSync(path124.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}.
@@ -1625,8 +1625,8 @@ ${idea.why}`:idea.title});return writeOverview(bp,{...readOverview(bp),ideas:o.i
1625
1625
  ${items3.map(x=>`- ${x}`).join(`
1626
1626
  `)}`:"";return[meta.scan?.summary?`What it is: ${meta.scan.summary}`:"",o.users?`Who uses it: ${o.users}`:"",list6("What it can do today",o.can),list6("Worth knowing",o.worth),list6("How it fits together",o.parts.map(p=>`${p.name}: ${p.what}${p.files.length?` (${p.files.join(", ")})`:""}`)),list6("Good first tickets",o.ideas.map(i=>i.title))].filter(Boolean).join(`
1627
1627
 
1628
- `)}function talkReply(bp,c){let t=readTalk(bp);if(t?.open){if(t.stopping){c.stop(),c.running||writeTalk(bp,{...t,thinking:!1,stopping:void 0,messages:t.thinking?[...t.messages,{who:"squad",text:"You stopped this answer. Ask again, or carry on.",at:now(),note:"stopped"}]:t.messages});return}!t.thinking||c.running||c.launch(async()=>{let prompt=overviewTalkPrompt(readMeta(bp),listDecisions(bp),understood(bp),t.messages);try{let r=await c.run({prompt}),out20=r.structured??{},options=(out20.options??[]).map(o=>String(o).trim()).filter(Boolean).slice(0,4),fresh=readTalk(bp)??t;writeTalk(bp,{...fresh,thinking:!1,messages:[...fresh.messages,{who:"architect",text:(out20.reply??r.text??"").trim()||"(no answer)",at:now(),...options.length?{options}:{}}]})}catch(err2){let fresh=readTalk(bp)??t,stopped=!!fresh.stopping||err2.message===STOPPED12,own2=ownWords2(err2);stopped||writeLog({level:"warn",text:`The Architect could not answer in the talk: ${err2?.message??String(err2)}`}),writeTalk(bp,{...fresh,thinking:!1,stopping:void 0,messages:[...fresh.messages,stopped?{who:"squad",text:"You stopped this answer. Ask again, or carry on.",at:now(),note:"stopped"}:{who:"squad",text:own2?`The Architect could not answer: ${own2}`:"The Architect could not answer this time. Press Try again.",at:now(),note:"failed"}]})}})}}function readSaid(bp){let t=readTalk(bp);return t?.reread?corrections(t):""}function readIdea(bp){let t=readTalk(bp);return t?.reread?t.idea??"":""}function readAgainDone(bp){let t=readTalk(bp);t&&writeTalk(bp,{...t,reread:!1})}function saveFirstWords(bp,users){!hasOverview(bp)||typeof users!="string"||!users.trim()||writeOverview(bp,{...readOverview(bp),users:users.trim()})}function saveBlueprintParts(bp,answer2){if(!hasOverview(bp)||!answer2||typeof answer2!="object")return;let a=answer2,fresh=cleanOverview({parts:a.parts,links:a.links});fresh.parts.length&&writeOverview(bp,{...readOverview(bp),parts:fresh.parts,links:fresh.links,refreshedAt:now()})}import fs133 from"fs";import os21 from"os";import path125 from"path";var MAX_FILES3=20;async function openTicketsFor(bp,t){let meta=readMeta(bp),open3=[];for(let o of openOnBoard(bp,t.id))open3.push({id:o.id,title:o.title,stage:o.stage,files:await branchFiles(bp,meta,o)});return open3}async function branchFiles(bp,meta,o){try{let repo=repoFor(meta,o),root=path125.join(bp.root,repo.path),files=[];if(o.worktree&&fs133.existsSync(o.worktree)&&o.baseCommit)files=await changedFiles(o.worktree,o.baseCommit);else{let ref2=branchRef(root,o),fork=ref2?git(root,["merge-base",repo.base,ref2],{allowFail:!0}):"";ref2&&fork&&(files=git(root,["diff","--name-only",fork,ref2],{allowFail:!0}).split(`
1629
- `).filter(Boolean))}let squad=squadIn(bp,root);return files=files.filter(f=>!(squad&&(f===squad||f.startsWith(`${squad}/`)))),meta.repos?.length&&(files=files.map(f=>`${repo.path}/${f}`)),files.length>MAX_FILES3?[...files.slice(0,MAX_FILES3),`and ${files.length-MAX_FILES3} more`]:files}catch{return[]}}function squadIn(bp,root){let rel=path125.relative(root,bp.squadDir).split(path125.sep).join("/");return rel&&!rel.startsWith("..")&&!path125.isAbsolute(rel)?rel:""}function branchRef(root,t){let branch=t.branch??`squad/${t.id.toLowerCase()}`;return git(root,["rev-parse","--verify","-q",`refs/heads/${branch}`],{allowFail:!0})?`refs/heads/${branch}`:pushedBranch(root,branch)}function dependencyToStartFrom(bp,t){return t.dependency?.way!=="branch"||t.worktree&&fs133.existsSync(t.worktree)?null:unmergedDeps(bp,t).find(d=>t.dependency.on.includes(d.id))??null}async function addWorktreeOn(bp,t,dep,at3,how2={}){let meta=readMeta(bp),repo=repoFor(meta,t);if(repoFor(meta,dep).path!==repo.path)throw new Error(`${dep.id} changes another repo (${repoFor(meta,dep).path}), so ${t.id} cannot start from its branch. Build anyway, or wait for it.`);let root=path125.join(bp.root,repo.path),start=await workOf(bp,root,dep,repo.base);if(!start)throw new Error(`${dep.id} has no code yet to build on. Wait for it, or build anyway.`);let dir=at3??path125.join(worktreeRoot(root),t.id.toLowerCase()),branch=`squad/${t.id.toLowerCase()}`,from=`${dep.id}'s work so far`;return await copyTurn(dir,async()=>{if(await clearHalfCopy(root,dir,how2.signal),copyIsWhole(dir))return!0;if(!git(root,["rev-parse","--verify","-q",`refs/heads/${branch}`],{allowFail:!0}))return await makeCopy(root,dir,["-b",branch],start,how2,{branch,start}),!1;if(await makeCopy(root,dir,[],branch,how2),isAncestor(dir,start,"HEAD"))return!1;try{await gitLater(dir,[...commitArgsFor(dir),"merge","--no-ff","-m",`Start from ${from}`,start])}catch(err2){throw await gitLater(dir,["merge","--abort"],{allowFail:!0}),trackTask(removeWorktree(root,dir)),writeLog({level:"warn",ticket:t.id,text:`Starting from ${from}: ${err2.message}`}),new Error(`${t.id}'s work clashes with ${from}, so it cannot start from it.`)}return!1})?{dir,branch,baseCommit:start,from}:(updateTicket(bp,t.id,x=>{x.dependency&&(x.dependency.from=start)}),{dir,branch,baseCommit:start,from})}async function workOf(bp,root,dep,base2){let copy2=dep.worktree&&fs133.existsSync(dep.worktree)?dep.worktree:null,tip=copy2?"HEAD":branchRef(root,dep);if(!tip)return null;let cwd=copy2??root,fork=git(cwd,["merge-base",base2,tip],{allowFail:!0});if(!fork)return null;let index=path125.join(os21.tmpdir(),`squad-start-${process.pid}-${Date.now()}`),env2={GIT_INDEX_FILE:index};try{git(cwd,["read-tree",tip],{env:env2}),copy2&&await stageAllLater(cwd,env2);let squad=squadIn(bp,root);squad&&git(cwd,["reset","-q",fork,"--",squad],{env:env2,allowFail:!0});let tree=git(cwd,["write-tree"],{env:env2});return git(cwd,[...commitArgsFor(cwd),"commit-tree",tree,"-p",fork,"-m",`${dep.id}: work so far, for the tickets that build on it`])}finally{fs133.rmSync(index,{force:!0})}}var CATCH_UP_BEFORE=new Set(["test","review","proof"]);function catchUp2(bp,t,planned){if(!CATCH_UP_BEFORE.has(planned)||!t.worktree||!fs133.existsSync(t.worktree))return planned;let c=t.dependency,due=c?.way==="branch"&&!c.caughtUp&&depsReady(bp,c.on);if(!t.baseWanted&&!due)return planned;let base2=repoFor(readMeta(bp),t).base;return updateTicket(bp,t.id,x=>{due&&x.dependency&&(x.dependency.caughtUp=now(),addActivity(x,"squad",`${c.on.join(" and ")} ${c.on.length===1?"is":"are"} merged, so squad-kit brings the newest code into this ticket before the checks`)),delete x.baseWanted,x.status="Bringing in the newest code",x.squad&&(x.squad.phase="update-base")}),"update-base"}import{execFileSync as execFileSync13}from"child_process";import fs134 from"fs";import path126 from"path";import{performance as performance2}from"perf_hooks";var NO_TTY=new Set(["?","??","-",""]),sleep6=ms2=>new Promise(r=>setTimeout(r,ms2)),clock=()=>performance2.now(),LOOK2={encoding:"utf8",stdio:["ignore","pipe","ignore"],maxBuffer:16*1024*1024,timeout:5e3};function processes(){if(process.platform==="win32")return[];try{return execFileSync13("ps",["-A","-o","pid=,ppid=,pgid=,tty=,command="],LOOK2).split(`
1628
+ `)}function talkReply(bp,c){let t=readTalk(bp);if(t?.open){if(t.stopping){c.stop(),c.running||writeTalk(bp,{...t,thinking:!1,stopping:void 0,messages:t.thinking?[...t.messages,{who:"squad",text:"You stopped this answer. Ask again, or carry on.",at:now(),note:"stopped"}]:t.messages});return}!t.thinking||c.running||c.launch(async()=>{let prompt=overviewTalkPrompt(readMeta(bp),listDecisions(bp),understood(bp),t.messages);try{let r=await c.run({prompt}),out20=r.structured??{},options=(out20.options??[]).map(o=>String(o).trim()).filter(Boolean).slice(0,4),fresh=readTalk(bp)??t;writeTalk(bp,{...fresh,thinking:!1,messages:[...fresh.messages,{who:"architect",text:(out20.reply??r.text??"").trim()||"(no answer)",at:now(),...options.length?{options}:{}}]})}catch(err2){let fresh=readTalk(bp)??t,stopped=!!fresh.stopping||err2.message===STOPPED12,own2=ownWords2(err2);stopped||writeLog({level:"warn",text:`The Architect could not answer in the talk: ${err2?.message??String(err2)}`}),writeTalk(bp,{...fresh,thinking:!1,stopping:void 0,messages:[...fresh.messages,stopped?{who:"squad",text:"You stopped this answer. Ask again, or carry on.",at:now(),note:"stopped"}:{who:"squad",text:own2?`The Architect could not answer: ${own2}`:"The Architect could not answer this time. Press Try again.",at:now(),note:"failed"}]})}})}}function readSaid(bp){let t=readTalk(bp);return t?.reread?corrections(t):""}function readIdea(bp){let t=readTalk(bp);return t?.reread?t.idea??"":""}function readAgainDone(bp){let t=readTalk(bp);t&&writeTalk(bp,{...t,reread:!1})}function saveFirstWords(bp,users){!hasOverview(bp)||typeof users!="string"||!users.trim()||writeOverview(bp,{...readOverview(bp),users:users.trim()})}function saveBlueprintParts(bp,answer2){if(!hasOverview(bp)||!answer2||typeof answer2!="object")return;let a=answer2,fresh=cleanOverview({parts:a.parts,links:a.links});fresh.parts.length&&writeOverview(bp,{...readOverview(bp),parts:fresh.parts,links:fresh.links,refreshedAt:now()})}import fs133 from"fs";import os22 from"os";import path125 from"path";var MAX_FILES3=20;async function openTicketsFor(bp,t){let meta=readMeta(bp),open3=[];for(let o of openOnBoard(bp,t.id))open3.push({id:o.id,title:o.title,stage:o.stage,files:await branchFiles(bp,meta,o)});return open3}async function branchFiles(bp,meta,o){try{let repo=repoFor(meta,o),root=path125.join(bp.root,repo.path),files=[];if(o.worktree&&fs133.existsSync(o.worktree)&&o.baseCommit)files=await changedFiles(o.worktree,o.baseCommit);else{let ref2=branchRef(root,o),fork=ref2?git(root,["merge-base",repo.base,ref2],{allowFail:!0}):"";ref2&&fork&&(files=git(root,["diff","--name-only",fork,ref2],{allowFail:!0}).split(`
1629
+ `).filter(Boolean))}let squad=squadIn(bp,root);return files=files.filter(f=>!(squad&&(f===squad||f.startsWith(`${squad}/`)))),meta.repos?.length&&(files=files.map(f=>`${repo.path}/${f}`)),files.length>MAX_FILES3?[...files.slice(0,MAX_FILES3),`and ${files.length-MAX_FILES3} more`]:files}catch{return[]}}function squadIn(bp,root){let rel=path125.relative(root,bp.squadDir).split(path125.sep).join("/");return rel&&!rel.startsWith("..")&&!path125.isAbsolute(rel)?rel:""}function branchRef(root,t){let branch=t.branch??`squad/${t.id.toLowerCase()}`;return git(root,["rev-parse","--verify","-q",`refs/heads/${branch}`],{allowFail:!0})?`refs/heads/${branch}`:pushedBranch(root,branch)}function dependencyToStartFrom(bp,t){return t.dependency?.way!=="branch"||t.worktree&&fs133.existsSync(t.worktree)?null:unmergedDeps(bp,t).find(d=>t.dependency.on.includes(d.id))??null}async function addWorktreeOn(bp,t,dep,at3,how2={}){let meta=readMeta(bp),repo=repoFor(meta,t);if(repoFor(meta,dep).path!==repo.path)throw new Error(`${dep.id} changes another repo (${repoFor(meta,dep).path}), so ${t.id} cannot start from its branch. Build anyway, or wait for it.`);let root=path125.join(bp.root,repo.path),start=await workOf(bp,root,dep,repo.base);if(!start)throw new Error(`${dep.id} has no code yet to build on. Wait for it, or build anyway.`);let dir=at3??path125.join(worktreeRoot(root),t.id.toLowerCase()),branch=`squad/${t.id.toLowerCase()}`,from=`${dep.id}'s work so far`;return await copyTurn(dir,async()=>{if(await clearHalfCopy(root,dir,how2.signal),copyIsWhole(dir))return!0;if(!git(root,["rev-parse","--verify","-q",`refs/heads/${branch}`],{allowFail:!0}))return await makeCopy(root,dir,["-b",branch],start,how2,{branch,start}),!1;if(await makeCopy(root,dir,[],branch,how2),isAncestor(dir,start,"HEAD"))return!1;try{await gitLater(dir,[...commitArgsFor(dir),"merge","--no-ff","-m",`Start from ${from}`,start])}catch(err2){throw await gitLater(dir,["merge","--abort"],{allowFail:!0}),trackTask(removeWorktree(root,dir)),writeLog({level:"warn",ticket:t.id,text:`Starting from ${from}: ${err2.message}`}),new Error(`${t.id}'s work clashes with ${from}, so it cannot start from it.`)}return!1})?{dir,branch,baseCommit:start,from}:(updateTicket(bp,t.id,x=>{x.dependency&&(x.dependency.from=start)}),{dir,branch,baseCommit:start,from})}async function workOf(bp,root,dep,base2){let copy2=dep.worktree&&fs133.existsSync(dep.worktree)?dep.worktree:null,tip=copy2?"HEAD":branchRef(root,dep);if(!tip)return null;let cwd=copy2??root,fork=git(cwd,["merge-base",base2,tip],{allowFail:!0});if(!fork)return null;let index=path125.join(os22.tmpdir(),`squad-start-${process.pid}-${Date.now()}`),env2={GIT_INDEX_FILE:index};try{git(cwd,["read-tree",tip],{env:env2}),copy2&&await stageAllLater(cwd,env2);let squad=squadIn(bp,root);squad&&git(cwd,["reset","-q",fork,"--",squad],{env:env2,allowFail:!0});let tree=git(cwd,["write-tree"],{env:env2});return git(cwd,[...commitArgsFor(cwd),"commit-tree",tree,"-p",fork,"-m",`${dep.id}: work so far, for the tickets that build on it`])}finally{fs133.rmSync(index,{force:!0})}}var CATCH_UP_BEFORE=new Set(["test","review","proof"]);function catchUp2(bp,t,planned){if(!CATCH_UP_BEFORE.has(planned)||!t.worktree||!fs133.existsSync(t.worktree))return planned;let c=t.dependency,due=c?.way==="branch"&&!c.caughtUp&&depsReady(bp,c.on);if(!t.baseWanted&&!due)return planned;let base2=repoFor(readMeta(bp),t).base;return updateTicket(bp,t.id,x=>{due&&x.dependency&&(x.dependency.caughtUp=now(),addActivity(x,"squad",`${c.on.join(" and ")} ${c.on.length===1?"is":"are"} merged, so squad-kit brings the newest code into this ticket before the checks`)),delete x.baseWanted,x.status="Bringing in the newest code",x.squad&&(x.squad.phase="update-base")}),"update-base"}import{execFileSync as execFileSync13}from"child_process";import fs134 from"fs";import path126 from"path";import{performance as performance2}from"perf_hooks";var NO_TTY=new Set(["?","??","-",""]),sleep6=ms2=>new Promise(r=>setTimeout(r,ms2)),clock=()=>performance2.now(),LOOK2={encoding:"utf8",stdio:["ignore","pipe","ignore"],maxBuffer:16*1024*1024,timeout:5e3};function processes(){if(process.platform==="win32")return[];try{return execFileSync13("ps",["-A","-o","pid=,ppid=,pgid=,tty=,command="],LOOK2).split(`
1630
1630
  `).map(l=>l.trim().match(/^(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/)).filter(m=>!!m).map(m=>({pid:Number(m[1]),ppid:Number(m[2]),pgid:Number(m[3]),tty:NO_TTY.has(m[4])?"":m[4],command:m[5]}))}catch{return[]}}function lookAt2(cmd15,args){try{return execFileSync13(cmd15,args,LOOK2)}catch(err2){return String(err2.stdout??"")}}var hasProc=()=>process.platform==="linux"&&fs134.existsSync("/proc/self/cwd");function workingFolders(pids){let out20=new Map;if(!pids.length||process.platform==="win32")return out20;if(hasProc()){for(let pid of pids)try{out20.set(pid,fs134.readlinkSync(`/proc/${pid}/cwd`))}catch{}return out20}for(let i=0;i<pids.length;i+=400){let pid=0;for(let line3 of lookAt2("lsof",["-a","-d","cwd","-p",pids.slice(i,i+400).join(","),"-Fpn"]).split(`
1631
1631
  `))line3.startsWith("p")?pid=Number(line3.slice(1)):line3.startsWith("n")&&pid&&out20.set(pid,line3.slice(1))}return out20}function listenInodes(){let out20=new Map;for(let f of["/proc/net/tcp","/proc/net/tcp6"]){let text3="";try{text3=fs134.readFileSync(f,"utf8")}catch{continue}for(let line3 of text3.split(`
1632
1632
  `).slice(1)){let cols=line3.trim().split(/\s+/);cols[3]!=="0A"||!cols[1]||!cols[9]||out20.set(cols[9],parseInt(cols[1].split(":")[1]??"0",16))}}return out20}function listeningPorts(pids){let out20=new Map;if(!pids.length||process.platform==="win32")return out20;let add2=(pid2,port)=>{if(!port)return;let list6=out20.get(pid2)??[];list6.includes(port)||list6.push(port),out20.set(pid2,list6)};if(hasProc()){let inodes=listenInodes();for(let pid2 of pids)try{for(let fd of fs134.readdirSync(`/proc/${pid2}/fd`)){let m=/^socket:\[(\d+)\]$/.exec(fs134.readlinkSync(`/proc/${pid2}/fd/${fd}`)),port=m?inodes.get(m[1]):void 0;port&&add2(pid2,port)}}catch{}return out20}let pid=0;for(let line3 of lookAt2("lsof",["-a","-nP","-iTCP","-sTCP:LISTEN","-p",pids.join(","),"-Fpn"]).split(`
@@ -1772,7 +1772,7 @@ ${PLAIN2}`}function allStoryLines(b){return b.epics.flatMap(e=>[...e.features.fl
1772
1772
 
1773
1773
  ${documentText(b,documentFiles(ctx.bp,b))}`,r=await ask(ctx,{step:"questions",prompt:replyPrompt2(q5,b.questions.indexOf(q5)+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===q5.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)}),done2(),!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 out20=r.structured,b2=updateBreakdown(ctx.bp,ctx.id,x=>{out20.summary?.trim()&&(x.summary=out20.summary.trim()),x.epics=(out20.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=(out20.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,out20.decided,new Map,by2);for(let q5 of x.questions){if(q5.answer?.choice!==SKIP_CHOICE)continue;(out20.decided??[]).some(d=>String(d.question??"").trim().toLowerCase()===q5.id)||x.decided.push({id:nextDecidedId(x.decided),text:`${q5.text} The Analyst chose: ${q5.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 out20=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,list6=[];for(let o of out20.stories??[]){let key=nextKey(x,"story");o.key&&map.set(String(o.key).trim().toUpperCase(),key),list6.push({s:story(o,key),needs:o.needs??[],feature:o.feature?String(o.feature).trim().toUpperCase():void 0})}for(let{s,feature}of list6){let f=e.features.find(y=>y.key===feature);(f?f.stories:e.stories).push(s)}resolveNeeds(x,list6,map),cutLoops(x),addDecided2(x,out20.decided,map,byFor(x)),made=list6.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,started2){return(outs??[]).map(o=>{let k=keyFor(m,o.key,"story"),kept2=started2.get(k);if(kept2)return started2.delete(k),kept2;let s=story(o,k);return m.made.push({s,needs:o.needs??[]}),s})}function mergeFeature(m,o,started2){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,started2)}}function mergeEpic(m,o,started2){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,started2)),stories:mergeStories(m,o.stories,started2)}}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 out20=(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,kept2=[],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:[]},started2=startedIn(ctx.bp,t?storiesIn(t):allStories(x).map(p=>p.story));if(kept2=[...started2.values()],whole)x.epics=(out20.epics??[]).map(e=>mergeEpic(m,e,started2)),out20.leftOut&&(x.leftOut=out20.leftOut.map(l=>({text:String(l.text??"").trim(),why:String(l.why??"").trim(),...cleanRef(l.ref)?{ref:cleanRef(l.ref)}:{}})).filter(l=>l.text)),out20.summary?.trim()&&(x.summary=out20.summary.trim()),started2.size&&x.epics[0]&&x.epics[0].stories.push(...started2.values());else if(t.level==="epic"&&out20.epic){let e=mergeEpic(m,{...out20.epic,key:t.item.key},started2);e.stories.push(...started2.values()),x.epics.splice(x.epics.indexOf(t.epic),1,e)}else if(t.level==="feature"&&out20.feature){let f=mergeFeature(m,{...out20.feature,key:t.item.key},started2);f.stories.push(...started2.values()),t.epic.features.splice(t.epic.features.indexOf(t.feature),1,f)}else if(t.level==="story"&&out20.story){let s=story({...out20.story},t.item.key);m.made.push({s,needs:out20.story.needs??[]}),t.list.splice(t.list.indexOf(t.item),1,s)}keepLinks(before,x),resolveNeeds(x,m.made,m.map);let left2=new Set(allKeys(x));for(let{story:s}of allStories(x))s.dependsOn=s.dependsOn.filter(n=>left2.has(n));let redone=t&&left2.has(t.item.key)?t.item.key:void 0;x.decided=x.decided.flatMap(d=>{if(!d.item||left2.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,out20.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`:""}`),kept2.length){let one2=kept2.length===1;addBreakdownActivity(s,"squad",`Kept ${andWords3(kept2.map(k=>k.key))} as ${one2?"it is":"they are"}: ${one2?"it":"they"} started as ${andWords3(kept2.map(k=>k.ticket))}. Change ${one2?"it":"them"} there`)}}),!1}var STEPS5={text:textStep,read:readStep,reply:replyStep,draft:draftStep,redo:redoStep};function failed2(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(`
1774
1774
  `)}`});let plain=`${own2??TECHNICAL_STOP}${job.step==="read"&&!capped&&!(err2 instanceof DocumentError)?READ_AGAIN:""}`;job.step==="reply"?updateBreakdown(bp,id2,x=>{let q5=x.questions.find(y=>y.id===job.question);q5&&(q5.thinking=!1,q5.thread=[...q5.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 STEPS5[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}failed2(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 out20=new Map;for(let{story:story2}of stories){let t=story2.ticket?readableTicket(bp,story2.ticket):null;t&&!t.deleting&&out20.set(story2.key,t)}return out20}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))),left2=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(!left2.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 left2)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=left2.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)),missing3=stories.filter(({story:story2})=>story2.ticket&&!tickets.has(story2.key)),unreadable=missing3.filter(({story:story2})=>ticketProblem(bp,story2.ticket)).map(({story:story2})=>story2.ticket),deleted=missing3.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,started2=[],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()}),started2.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:started2,...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:(done2,of,p)=>updateBreakdownState(bp,id2,s=>{s.sync&&(s.sync={...s.sync,done:before+done2,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,STOPPED13="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),list6=numbered.length?numbered:entries.length===count3?entries.map((e,i)=>({n:i,status:e.status})):[];if(!list6.length)return null;let doing=list6.find(e=>e.status==="in_progress"),open3=list6.find(e=>e.status!=="completed");return doing?doing.n:open3?open3.n:Math.min(count3-1,list6[list6.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 inTurn3(a[0],"checks",work,signal2?{signal:signal2}:{}).catch(turnStop)}function runGatesAt2(...a){let dirs=a[2].map(p=>p.dir);return inTurn3(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 createHash12("sha1").update(JSON.stringify(planSnapshot(t))).digest("hex").slice(0,12)}async function secretFiles(bp,cwd){return[bp.secretsFile,path130.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(running5,limit){return queuedLine(running5.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"||!fs139.existsSync(t.worktree))return!1;for(let c of t.copies??[])fs139.existsSync(c.dir)&&(git(c.dir,["rev-parse","--git-dir"],{allowFail:!0})?settleCopy(c.dir):repairCopyLink(path130.join(projectRoot(this.bp),c.repo),c.dir));let repo=path130.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(path130.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 q5=t.questions?.find(x=>x.thinking);if(q5){pause2?this.hold(t,limitText(pause2)):this.mayStart(limit)?this.launch(t.id,()=>this.reply(t.id,q5.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,tools4=first2||pause2?null:toolsHold(this.bp,t,this.phaseFor(t))??this.picturesWait(t,this.phaseFor(t));if(!first2&&!pause2&&!tools4&&waitsForTools(t.status)&&t.status!==TOOLS_CHECKING&&this.toolsHere(t.id),first2)this.hold(t,waitsForFirstWords(first2.id));else if(pause2)this.hold(t,limitText(pause2));else if(tools4)this.waitForTools(t,tools4);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 running5=[];for(let s of squads)running5.push(...s.ticketIds());this.hold(t,queuedText(running5,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{}}}waitForTools(t,b){let told=t.status===b.status;if(this.hold(t,b.status),!(told||b.checking)){this.log("info",`Waits for tools: ${b.text}`,t.id);try{this.update(t.id,x=>addActivity(x,"squad",`${b.text} It waits, and goes on by itself once the tool is there`))}catch{}}}picturesWait(t,phase){return phase!=="proof"?null:picturesHold(this.bp,t,this.hasWeb(t))}hasWeb(t){let rc=(this.appOf(t)?.repo??repoFor(this.meta(),t)).commands;return!!(rc.start?.trim()&&rc.url?.trim())}toolsHere(id2){try{this.update(id2,x=>addActivity(x,"squad","The tools it needs are on this computer now, so it goes on"))}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(`
1775
- `))}).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),out20=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"&&out20.push(...projectCopies(this.bp).map(r=>path130.join(r,"setup-copy"))),k.startsWith("__before:")){let t2=tickets.find(x=>x.id===k.slice(9));t2&&out20.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&&out20.push(...ticketCopies(this.bp,t))}return productHold(this.bp)&&out20.push(productCopiesDir(this.bp)),out20}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,list6)=>{let t=tickets.find(x=>x.id===id2),lines3=stoppedLines(list6,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||!fs139.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=appIn2(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 fs139.realpathSync.native(dir)}catch{return path130.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,q5){try{let t=readTicket(this.bp,id2);if(!t||!q5&&!t.quiet||q5&&t.quiet&&q5.kind===t.quiet.kind&&q5.detail===t.quiet.detail)return;this.update(id2,x=>{q5&&x.quiet?.kind!==q5.kind&&addActivity(x,"squad",q5.kind==="stuck"?`Looks stuck: ${q5.detail}`:`Taking long: ${q5.detail}`),q5?x.quiet={...q5,...x.quiet?.port&&q5.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(STOPPED13)}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=path130.join(this.bp.localDir,"rules-read"),files=rulesFiles(root).map(from=>{let to=path130.join(dir,path130.relative(root,from));return fs139.mkdirSync(path130.dirname(to),{recursive:!0}),fs139.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:run7=>this.noteRun(id2,cwd,run7)}:{}}}async noteRun(id2,cwd,run7){let notes=this.builderRuns.get(id2);for(let name of run7.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:hold2=>this.update(id2,x=>x.status=waitWords2(hold2)),log:text3=>this.log("info",text3,id2)})}apiParts(t){let meta=this.meta();if(runsAsProduct(meta)){let{places}=productPlan(this.bp,meta,t),beside=partsOf2(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(),now3=readTicket(this.bp,id2)??t,copies=copiesOf(meta,now3),out20={};for(let c of copies){let named=partsOf2(meta).filter(p=>p.repo===c.repo.path&&!p.shared);for(let key of named.length?named.map(p=>p.id):[c.repo.path])out20[key]=this.windowChecks(id2,c.dir,c.repo.path,noted)}return out20};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=>path130.resolve(x.dir)===path130.resolve(cwd)):void 0,failed3=c?(await this.setUpAgain(id2,c)).failed:void 0;failed3&&this.log("warn",`Setting up the copy again did not work, so the app may not work (${failed3.command}):
1775
+ `))}).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),out20=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"&&out20.push(...projectCopies(this.bp).map(r=>path130.join(r,"setup-copy"))),k.startsWith("__before:")){let t2=tickets.find(x=>x.id===k.slice(9));t2&&out20.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&&out20.push(...ticketCopies(this.bp,t))}return productHold(this.bp)&&out20.push(productCopiesDir(this.bp)),out20}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,list6)=>{let t=tickets.find(x=>x.id===id2),lines3=stoppedLines(list6,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||!fs139.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=appIn2(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 fs139.realpathSync.native(dir)}catch{return path130.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),busy=computerBusy();for(let id2 of keys){let t=readTicket(this.bp,id2);if(!t)continue;let q5=t.quietSnoozedUntil&&Date.parse(t.quietSnoozedUntil)>Date.now()?null:this.dog.check(id2,Date.now(),rows2)??slowStep(t,usual);this.setQuiet(id2,q5?.kind==="long"&&busy?{...q5,busy}:q5),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,q5){try{let t=readTicket(this.bp,id2);if(!t||!q5&&!t.quiet||q5&&t.quiet&&q5.kind===t.quiet.kind&&q5.detail===t.quiet.detail&&!!q5.busy==!!t.quiet.busy)return;this.update(id2,x=>{q5&&x.quiet?.kind!==q5.kind&&addActivity(x,"squad",q5.kind==="stuck"?`Looks stuck: ${q5.detail}`:slowActivity(x,q5)),q5?x.quiet={...q5,...x.quiet?.port&&q5.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(STOPPED13)}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=path130.join(this.bp.localDir,"rules-read"),files=rulesFiles(root).map(from=>{let to=path130.join(dir,path130.relative(root,from));return fs139.mkdirSync(path130.dirname(to),{recursive:!0}),fs139.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 said=plainToolLine(e,cwd)??DOING.other;this.update(id2,t=>{let plain=stepWords(said,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=OWN_CHECK_DOING[name],runsFor(x,name,x.status)}),onEnd:(name,ok2,alone)=>this.update(id2,x=>{noted?ownCheckEnded(x,name,ok2,alone):ranFor(x,name),x.clock?.doing&&(x.status=x.clock.doing)}),...noted?{onRun:run7=>this.noteRun(id2,cwd,run7)}:{}}}async noteRun(id2,cwd,run7){let notes=this.builderRuns.get(id2);for(let name of run7.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:hold2=>this.update(id2,x=>x.status=waitWords2(hold2)),log:text3=>this.log("info",text3,id2)})}apiParts(t){let meta=this.meta();if(runsAsProduct(meta)){let{places}=productPlan(this.bp,meta,t),beside=partsOf2(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(),now3=readTicket(this.bp,id2)??t,copies=copiesOf(meta,now3),out20={};for(let c of copies){let named=partsOf2(meta).filter(p=>p.repo===c.repo.path&&!p.shared);for(let key of named.length?named.map(p=>p.id):[c.repo.path])out20[key]=this.windowChecks(id2,c.dir,c.repo.path,noted)}return out20};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=>path130.resolve(x.dir)===path130.resolve(cwd)):void 0,failed3=c?(await this.setUpAgain(id2,c)).failed:void 0;failed3&&this.log("warn",`Setting up the copy again did not work, so the app may not work (${failed3.command}):
1776
1776
  ${tail(failed3.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(`
1777
1777
 
1778
1778
  `),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))))},run7=({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 run7(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,done2=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 run7(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);done2=again2}let settles=opts.role==="builder"?q5=>!!inTheWay(askText(q5)):void 0;for(let back=0;;){let found2=opts.schema?stoppedQuestion(done2.structured,settles):null;if(!found2)return ended(opts.reply||opts.schema===CHAT_SCHEMA?this.plainReply(done2,who4,opts.role,key):done2);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}
@@ -1796,8 +1796,8 @@ ${specPrompt(t0,change,!1,others)}`,permissions:["read"],schema:SPEC_SCHEMA,time
1796
1796
  ${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",out20.question);let mockup=out20.mockup?.trim()&&out20.screens>0?this.saveMockup(id2,out20.mockup):void 0,meta=this.meta(),named=meta.repos?.length?(out20.parts??[]).map(p=>String(p).trim()):[],inRepos=reposOfParts(meta,named),repo=meta.repos?.length?inRepos[0]??meta.repos.find(r2=>r2.path===out20.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(out20.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=>partsOf2(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=out20.story,t.criteria=out20.criteria,t.releaseNote=out20.releaseNote),t.agentQuestion=void 0,mockup?(t.mockup=mockup,addActivity(t,"architect","Made a mockup of how the screen will look")):out20.screens||delete t.mockup,out20.decision?t.decision={...out20.decision}:delete t.decision;let known=new Set(listDecisions(this.bp).map(d=>d.id));if(t.follows=[...new Set(out20.follows??[])].filter(x=>known.has(x)),t.dependsOn=designDepends(this.bp,t,out20.dependsOn),t.decision?.replaces&&!known.has(t.decision.replaces)&&delete t.decision.replaces,t.steps=out20.steps.map(s=>({title:s,status:"todo"})),applyPlanEdits(t),t.work={steps:out20.steps.length,tests:Math.max(0,out20.tests),screens:Math.max(0,out20.screens),...out20.asks?{asks:Math.max(0,out20.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",out20.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&&fs139.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,how2=>dep?addWorktreeOn(this.bp,t,dep,at3,how2):addWorktree(path130.join(root,repo.path),id2,repo.base,fetched,at3,how2)),named=repo.path==="."?"the project":repoLabel(meta,repo.path)??repo.path;return t=this.update(id2,x=>{x.worktree=wt.dir,at3&&(x.storyDir=path130.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,how2=>addWorktree(path130.join(projectRoot(this.bp),repo.path),id2,repo.base,fetched,path130.join(t.storyDir,repo.path),how2));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 docker2=dockerBlockedWords(r.output,this.guardFor(dir,id2));if(docker2)throw this.update(id2,x=>x.setupOwed=[...new Set([...x.setupOwed??[],repo.path])]),new StepError(docker2,`${setup}:
1797
1797
  ${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}
1798
1798
 
1799
- ${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||!fs139.existsSync(c.dir)||readTicket(this.bp,id2)?.setupFor?.[c.repo.path]===await setupPrintNow(c.dir))return{ran:!1};this.update(id2,x=>addActivity(x,"squad","New packages or database changes came into this copy, so squad-kit sets it up again")),this.log("info",`Setting up the copy again: ${setup}`,id2);let theirs=await changedTracked(c.dir),run7=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 run7();!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:
1800
- ${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 run7());let back=await putBackTracked(c.dir,{keep:theirs});if(back.length&&this.log("info",`The setup command changed ${back.join(", ")}; squad-kit put ${back.length===1?"it":"them"} back`,id2),!r.ok)return{ran:!0,failed:{name:"setup",command:setup,ok:!1,output:r.output,seconds:r.seconds,...r.ended?{ended:!0}:{}}};let print=await setupPrintNow(c.dir);return this.update(id2,x=>x.setupFor={...x.setupFor,[c.repo.path]:print}),{ran:!0}}async addPart(id2,which,lanes){let meta=this.meta(),part=partsOf2(meta).find(p=>p.id===which.trim()||p.name.toLowerCase()===which.trim().toLowerCase());if(!part)return`There is no part "${which}". The parts: ${partsOf2(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,path130.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:missing3}=placeStoryFiles(this.bp,repo,dir);if(!missing3.length)return;let line3=`An engineer has not given stories their values for ${missing3.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.
1799
+ ${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||!fs139.existsSync(c.dir)||readTicket(this.bp,id2)?.setupFor?.[c.repo.path]===await setupPrintNow(c.dir))return{ran:!1};this.update(id2,x=>{addActivity(x,"squad","New packages or database changes came into this copy, so squad-kit sets it up again"),runsFor(x,"setup",DOING.setup)}),this.log("info",`Setting up the copy again: ${setup}`,id2);let theirs=await changedTracked(c.dir),run7=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 run7();!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:
1800
+ ${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 run7()),this.update(id2,x=>ranFor(x,"setup"));let back=await putBackTracked(c.dir,{keep:theirs});if(back.length&&this.log("info",`The setup command changed ${back.join(", ")}; squad-kit put ${back.length===1?"it":"them"} back`,id2),!r.ok)return{ran:!0,failed:{name:"setup",command:setup,ok:!1,output:r.output,seconds:r.seconds,...r.ended?{ended:!0}:{}}};let print=await setupPrintNow(c.dir);return this.update(id2,x=>x.setupFor={...x.setupFor,[c.repo.path]:print}),{ran:!0}}async addPart(id2,which,lanes){let meta=this.meta(),part=partsOf2(meta).find(p=>p.id===which.trim()||p.name.toLowerCase()===which.trim().toLowerCase());if(!part)return`There is no part "${which}". The parts: ${partsOf2(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,path130.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:missing3}=placeStoryFiles(this.bp,repo,dir);if(!missing3.length)return;let line3=`An engineer has not given stories their values for ${missing3.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.
1801
1801
 
1802
1802
  `:sofar.length?`${intro}
1803
1803
 
@@ -1828,8 +1828,8 @@ ${tail(setUp.failed.output,30)}`,id2),setUp.ran&&await this.dropKept(id2);let ke
1828
1828
 
1829
1829
  ${proofAgainPrompt(again2)}`:prompt,permissions:["read"],schema:PROOF_SCHEMA,check,checkHelp:()=>win.pictureList(),timeoutMs:PROOF_STEP_MS,addDirs:this.extraDirs(t),box:{},win,...beside?{}:{onTool:this.watcher(id2,"tester",workFolder(t))}}).finally(()=>this.keepLanes(id2,"tester",lanes,[this.bp.proofDir]))).structured??{shots:[]},say=text3=>this.update(id2,x=>addActivity(x,"tester",text3));lists.length&&this.update(id2,x=>this.listNoes.set(id2,answerLists(this.bp,x,lists,out20.checklist)));let shots=[];for(let s of out20.shots??[]){let file3=path130.basename(String(s.file??"")),taken=win.pictures.get(file3);if(!taken){say(`The Tester named ${file3||"a picture"} for "done when" ${s.criterion}, which squad-kit did not take in this step, so it is left out`);continue}if(shots.filter(x=>x.criterion===s.criterion).length>=6){say(`The Tester named more than ${6} pictures for "done when" ${s.criterion}, so squad-kit kept the first ${6}`);continue}let note2=s.note?this.plainNote(win,s.note):"";shots.push({criterion:s.criterion,file:file3,page:taken.page,size:taken.size,...taken.device?{device:taken.device}:{},shows:verdictOf(s.shows)??"not-shown",...note2?{note:note2}:{}})}let off=[];for(let o of when2==="after"?out20.offScreen??[]:[]){let n=Number(o?.criterion);if(!criterionText2(t,n)||off.some(x=>x.criterion===n))continue;let refused=win.refusals.length,why=this.plainNote(win,String(o.why??""));win.refusals.length>refused||off.push({criterion:n,why})}let skipped=[...off.map(o=>o.why),this.plainNote(win,out20.skipped??"")].filter(Boolean).join(" ");if(win.tooMany)throw new StepError(tooManyWords("Tester"),win.refusalDetail());if(win.tooManyMistakes)throw new StepError(tooManyMistakesWords("Tester"),win.mistakeDetail());let named=n=>criterionText2(t,n),problems=[],rejected=new Set;for(let s of shots){let full=path130.join(this.bp.proofDir,s.file),criterion=named(s.criterion);if(!criterion||!fs139.existsSync(full))continue;let bad2=isApiProof(s.file)?null:shotProblem(full,since);bad2&&(rejected.add(s.file),problems.push({criterion,text:`The ${when2} screenshot for "${criterion}" (${s.file}): ${bad2}.`}))}return this.update(id2,x=>{let proof=x.proof??[],usable=shots.filter(s=>!rejected.has(s.file)&&named(s.criterion)&&fs139.existsSync(path130.join(this.bp.proofDir,s.file)));for(let n of new Set(usable.map(s=>s.criterion))){let criterion=named(n),own2=usable.filter(s=>s.criterion===n),row3=proof.find(p=>p.criterion===criterion);row3||(row3={criterion,before:"",after:"",beforeLabel:"Before",afterLabel:"After"},proof.push(row3)),when2==="before"?(row3.before=own2[0].file,own2[0].device?row3.beforeDevice=own2[0].device:delete row3.beforeDevice):(addPictures(row3,own2,(a,b)=>this.sameFile(a,b)),this.pairWithBefore(row3))}if(x.proof=proof.map(p=>p.before?p:{...p,before:p.after,...p.device?{beforeDevice:p.device}:{},beforeLabel:"New screen",afterLabel:"After"}),when2==="after"){let kept2=again2.length?x.offScreen??[]:[],added2=off.map(o=>({criterion:named(o.criterion),why:o.why||"The Tester says it does not show on a screen."})),all=[...kept2.filter(k=>!added2.some(a=>a.criterion===k.criterion)),...added2];all.length?x.offScreen=all:delete x.offScreen;let notOn=off.length?`. Not on a screen: ${off.map(o=>`"done when" ${o.criterion}`).join(", ")}`:skipped?`. Skipped: ${skipped}`:"",answers2=shots.filter(s=>isApiProof(s.file)).length,pictures=shots.length-answers2-rejected.size;addActivity(x,"tester",`Saved ${pictures} ${answers2&&pictures===1?"screenshot":"screenshots"}${answers2?` and ${answers2} ${answers2===1?"answer":"answers"} from the API`:""}${notOn}`)}}),{problems,planned:shots.map(s=>s.criterion),said:skipped,stuck:win.stuck,sizes:Object.fromEntries(shots.map(s=>[s.file,s.size]))}}async readyBrowser(id2,signal2,job){let beat2=setInterval(()=>this.dog.beat(job),2e4);try{await prepareShots({...signal2?{signal:signal2}:{},onLine:text3=>this.update(id2,x=>addActivity(x,"tester",text3))})}catch(err2){throw signal2?.aborted?err2:new StepError(err2.message,shotsErrorDetail(err2))}finally{clearInterval(beat2)}}plainNote(win,text3){let said=text3.trim();if(!said)return"";let verdict=judgeQuestion({text:said});if(!verdict.pass&&verdict.kind==="not-allowed")return win.refuse("note",`${verdict.why}: "${said.slice(0,200)}"`,refusedAskLine("Tester",verdict.what)),"";let{kept:kept2,left:left2}=plainParts(said,technicalWords);return left2&&this.log("info",`Left out of the Tester's note: ${left2.slice(0,600)}`,win.ticket),kept2?kept2.slice(0,600):(this.update(win.ticket,x=>addActivity(x,"tester","The Tester wrote something technical, so it stays out of the proof.")),"")}},NO_APP="squad-kit could not start the app, so it took no screenshots. Press Try again to try once more.",NO_PICTURES="squad-kit could not take the screenshots this time. Press Try again to try once more.",STUCK_PICTURES="The Tester got stuck on something technical that squad-kit could not fix, so there are no screenshots yet. Press Try again to try once more.";function technicalWords(sentence){let v=judgeQuestion({text:sentence});return!v.pass&&v.kind==="technical"}function checkNames(names2){let words2={test:"the tests",lint:"the lint check",build:"the build",start:"starting the app"},w=[...new Set(names2.map(n=>words2[n]??`the ${n} check`))];return w.length>1?`${w.slice(0,-1).join(", ")} and ${w.at(-1)}`:w[0]??"a check"}function missingShots(t,round){let count3=doneWhenCount(t),off=offScreenNumbers(t),out20=[];for(let n=1;n<=count3;n+=1)(t.proof??[]).find(p=>p.criterion===criterionText2(t,n))?.after||off.has(n)||out20.push({criterion:n,why:round.planned.includes(n)?"squad-kit could not use the picture the Tester took.":"The Tester took no picture of it."});return out20}var SAID={met:"it is met",broken:"it is broken","not-shown":"it is not shown","off-screen":"it is not on a screen"};function openStep(t){let i=(t.steps??[]).findIndex(s=>s.status!=="done");return i>=0?i+1:1}function held2(status){return!!(status&&(status.startsWith(QUEUED2)||isLimitText(status)||waitsForFirst(status)||waitsForTools(status)))}function isInside(dir,file3){return typeof file3=="string"&&!path130.relative(dir,file3).startsWith("..")}function clean3(c){return Object.fromEntries(Object.entries(c).filter(([,v])=>typeof v=="string"&&v.trim()))}function attachmentDirs(t){return[...new Set((t.attachments??[]).map(a=>path130.dirname(a)))]}var WaitingForYou=class extends Error{constructor(){super("Waiting for your answer")}};import{execSync as execSync2}from"child_process";import fs142 from"fs";import path133 from"path";import fs141 from"fs";import path132 from"path";import yaml17 from"js-yaml";import fs140 from"fs";import path131 from"path";function infoFile(bp){return bp.consoleFile}function readConsoleInfo(bp){try{return JSON.parse(fs140.readFileSync(infoFile(bp),"utf8"))}catch{return null}}function writeConsoleInfo(bp,info2){fs140.mkdirSync(bp.localDir,{recursive:!0}),fs140.writeFileSync(infoFile(bp),`${JSON.stringify(info2,null,2)}
1830
1830
  `,{encoding:"utf8",mode:384})}function appFile(){return path131.join(squadHome(),"app.json")}function readAppInfo(){try{return JSON.parse(fs140.readFileSync(appFile(),"utf8"))}catch{return null}}function writeAppInfo(info2){fs140.mkdirSync(squadHome(),{recursive:!0}),fs140.writeFileSync(appFile(),`${JSON.stringify(info2,null,2)}
1831
- `,{encoding:"utf8",mode:384})}function runningApp(){let info2=readAppInfo();return info2&&isAlive(info2.pid)&&!takenOver(info2.pid,info2.startedAt)?info2:null}function isAlive(pid){if(!pid||pid===process.pid)return!1;try{return process.kill(pid,0),!0}catch{return!1}}function runningConsole(bp){let info2=readConsoleInfo(bp);if(info2&&isAlive(info2.pid)&&!takenOver(info2.pid,info2.startedAt))return info2;for(let file3 of[bp.pidFile,path131.join(bp.legacyDir,"console.json"),path131.join(bp.legacyDir,"squad.pid")])try{let text3=fs140.readFileSync(file3,"utf8").trim(),pid=file3.endsWith(".json")?Number(JSON.parse(text3).pid):Number(text3);if(isAlive(pid))return file3.endsWith(".json")?{...JSON.parse(text3),pid}:{pid}}catch{}return null}function buildStamp(entry=process.argv[1]){try{return Math.round(fs140.statSync(fs140.realpathSync(entry??"")).mtimeMs)}catch{return 0}}async function stopConsole(pid,waitMs=2e4){let gone=async ms2=>{let until=Date.now()+ms2;for(;Date.now()<until;){if(!isAlive(pid))return!0;await new Promise(r=>setTimeout(r,250))}return!isAlive(pid)};try{process.kill(pid,"SIGINT")}catch{return!0}if(await gone(waitMs))return!0;try{process.kill(pid,"SIGTERM")}catch{return!0}return gone(5e3)}var MoveError=class extends Error{},read5=file3=>yaml17.load(fs141.readFileSync(file3,"utf8"));function listYaml(dir){return fs141.existsSync(dir)?fs141.readdirSync(dir).filter(f=>f.endsWith(".yaml")).map(f=>read5(path132.join(dir,f))):[]}function isMediaFile(name){return isMediaNote(name)||/\.transcript\.txt$/.test(name)||/\.(ogg|opus|oga|m4a|mp3|wav|aac|amr|flac|weba|mp4|mov|m4v|webm)\.screen/.test(name)}function copyDir(from,to){return fs141.existsSync(from)?(fs141.mkdirSync(to,{recursive:!0}),fs141.cpSync(from,to,{recursive:!0}),fs141.readdirSync(from).length):0}function repath(v,map){return typeof v=="string"?map.get(v)??v:Array.isArray(v)?v.map(x=>repath(x,map)):v&&typeof v=="object"?Object.fromEntries(Object.entries(v).map(([k,x])=>[k,repath(x,map)])):v}function same4(v){return typeof v=="string"?v.trim():Array.isArray(v)?v.map(same4):v&&typeof v=="object"?Object.fromEntries(Object.entries(v).filter(([k,x])=>x!==void 0&&!(k==="chosen"&&x===!1)).sort(([a],[b])=>a.localeCompare(b)).map(([k,x])=>[k,same4(x)])):v}function differences(label3,before,after){let a=same4(before),b=same4(after);return[...new Set([...Object.keys(a),...Object.keys(b)])].filter(k=>JSON.stringify(a[k])!==JSON.stringify(b[k])).map(k=>`${label3}: ${k}`)}function openBoard(squadDir){let bp=boardPaths(squadDir);if(!hasLegacyBoard(bp))return{bp,moved:null};if(runningConsole(bp))throw new MoveError("This board is open somewhere else. Close it first (or run: squad console --restart): squad-kit moves its files to the new layout once, while nothing runs.");let moved=moveBoard(bp);return{bp:boardPaths(squadDir),moved}}function moveBoard(bp,now3=new Date){if(!hasLegacyBoard(bp))throw new MoveError("There is no board to move: it is moved already, or there is none.");let legacy=bp.legacyDir,meta=read5(path132.join(legacy,"board.yaml")),backup=path132.join(squadHome(),"backups",`${path132.basename(bp.root)}-${now3.toISOString().replace(/[:.]/g,"-")}`);fs141.mkdirSync(path132.dirname(backup),{recursive:!0}),fs141.cpSync(bp.squadDir,backup,{recursive:!0});let id2=newProjectId(meta.project||path132.basename(bp.root)),configBefore=fs141.existsSync(bp.configFile)?fs141.readFileSync(bp.configFile,"utf8"):null,next=null,created=p=>[p.ticketsDir,p.decisionsDir,p.releasesDir,p.rulesDir,p.knowledgeDir,p.localDir],before=new Set([...created(bp)].filter(d=>fs141.existsSync(d)));try{let config2=configBefore!==null?loadConfig(bp.configFile):{...DEFAULT_CONFIG,project:{...DEFAULT_CONFIG.project,name:meta.project}};if(saveConfig(bp.configFile,{...config2,board:{id:id2}}),next=boardPaths(bp.squadDir),next.projectId!==id2)throw new MoveError("The new settings did not save the project id.");writeMeta(next,{...meta,id:id2,layout:LAYOUT,documentsInGit:meta.documentsInGit??!0});let files=copyDir(path132.join(legacy,"agents"),next.rulesDir);files+=copyDir(path132.join(legacy,"context","wiki"),next.wikiDir),files+=copyDir(path132.join(legacy,"proof"),next.proofDir),files+=copyDir(path132.join(legacy,"sessions"),next.chatsDir),fs141.existsSync(path132.join(legacy,"console.json"))&&fs141.copyFileSync(path132.join(legacy,"console.json"),next.consoleFile);let oldSecrets=path132.join(bp.squadDir,"secrets.yaml");fs141.existsSync(oldSecrets)&&(fs141.mkdirSync(path132.dirname(next.secretsFile),{recursive:!0}),fs141.copyFileSync(oldSecrets,next.secretsFile),fs141.chmodSync(next.secretsFile,384));let tickets=listYaml(path132.join(legacy,"tickets")),moved=[];for(let t of tickets){let map=new Map,from=path132.join(legacy,"attachments",t.id);if(fs141.existsSync(from))for(let name of fs141.readdirSync(from)){let src=path132.join(from,name);if(!fs141.statSync(src).isFile())continue;let dest=name==="mockup.html"?path132.join(ticketDir(next,t.id),name):isMediaFile(name)?path132.join(ticketMediaDir(next,t.id),name):path132.join(ticketFilesDir(next,t.id),name);fs141.mkdirSync(path132.dirname(dest),{recursive:!0}),fs141.copyFileSync(src,dest),map.set(src,dest),files+=1}let nt=repath(t,map);writeTicket(next,nt),moved.push(nt)}let decisions=listYaml(path132.join(legacy,"decisions"));for(let d of decisions)writeDecision(next,d);let releases=listYaml(path132.join(legacy,"releases"));for(let r of releases)writeRelease(next,r);let problems=[];for(let t of moved){let back=readTicket(next,t.id);back?problems.push(...differences(t.id,t,back)):problems.push(`${t.id}: missing`)}let decisionsBack=new Map(listDecisions(next).map(d=>[d.id,d]));for(let d of decisions)problems.push(...differences(d.id,d,decisionsBack.get(d.id)??{}));let releasesBack=new Map(listReleases(next).map(r=>[r.version,r]));for(let r of releases)problems.push(...differences(r.version,r,releasesBack.get(r.version)??{}));let metaBack=readMeta(next);for(let k of["project","key","nextNumber","engine","commands","git","repos","map","lessons"])JSON.stringify(same4(meta[k]))!==JSON.stringify(same4(metaBack[k]))&&problems.push(`board settings: ${k}`);if(JSON.stringify(same4(meta.blueprint?.stack??[]))!==JSON.stringify(same4(metaBack.blueprint?.stack??[]))&&problems.push("board settings: stack"),problems.length)throw new MoveError(`The new files did not match the old ones, so nothing changed: ${problems.slice(0,8).join("; ")}`);return fs141.rmSync(legacy,{recursive:!0,force:!0}),fs141.rmSync(oldSecrets,{force:!0}),{tickets:moved.length,decisions:decisions.length,releases:releases.length,files,backup,localDir:next.localDir}}catch(err2){for(let dir of[...created(bp),...next?created(next):[]])before.has(dir)||fs141.rmSync(dir,{recursive:!0,force:!0});throw configBefore===null?fs141.rmSync(bp.configFile,{force:!0}):writeConfigFile(bp.configFile,configBefore),err2 instanceof MoveError?err2:new MoveError(`Moving the board failed, so nothing changed: ${err2.message}`)}}function keyFor2(name){let words2=name.split(/[^A-Za-z0-9]+/).filter(Boolean);return(words2.length>1?words2.map(w=>w[0]):[words2[0]?.slice(0,2)??"SK"]).join("").toUpperCase().slice(0,3)||"SK"}function gitUser(dir){try{return execSync2("git config user.name",{encoding:"utf8",stdio:["ignore","pipe","ignore"],env:gitEnv(),...dir&&fs142.existsSync(dir)?{cwd:dir}:{}}).trim().split(/\s+/)[0]||"You"}catch{return"You"}}function detectTools(){return AI_TOOLS.map(t=>({name:t.label,ready:t.ready()}))}var SQUAD_README="# squad-kit\n\nThis folder is the project's memory, kept by squad-kit. People can read it, and edit it by hand.\n\n- `config.yaml`: the project's settings.\n- `tickets/<ID>/`: each ticket's spec, questions, design, notes and proof.\n- `decisions/`: what the squad decided, and why.\n- `releases/`: what each release carried.\n- `rules/`: how each role works on this project.\n- `knowledge/`: what the squad learned about the project.\n- `how-to-work-here.md`: how to run a story in a conversation in any AI tool, kept up to date by squad-kit.\n\nLive progress, agent chats, screenshots and sign-ins stay on each person's computer, in `~/.squad-kit/`.\n\nOpen the board: `squad console`\n";function setupProject(root){let squadDir=path133.join(root,SQUAD_DIR),name=path133.basename(path133.resolve(root));fs142.mkdirSync(squadDir,{recursive:!0}),saveConfig(path133.join(squadDir,"config.yaml"),{...DEFAULT_CONFIG,project:{...DEFAULT_CONFIG.project,name}});let readme=path133.join(squadDir,"README.md");return fs142.existsSync(readme)||fs142.writeFileSync(readme,SQUAD_README,"utf8"),name}function moveOldSecrets(bp){let old=path133.join(bp.squadDir,"secrets.yaml"),local=path133.join(bp.localDir,"secrets.yaml");return!fs142.existsSync(old)||fs142.existsSync(local)?!1:(fs142.mkdirSync(bp.localDir,{recursive:!0}),fs142.copyFileSync(old,local),fs142.chmodSync(local,384),fs142.rmSync(old),!0)}function keepOwnKnowledge(bp){let kept2=[];for(let name of["project.md","lessons.md","stack.md"]){let file3=path133.join(bp.knowledgeDir,name);if(!fs142.existsSync(file3))continue;let to=name.replace(/\.md$/,".before-squad-kit.md");fs142.renameSync(file3,path133.join(bp.knowledgeDir,to)),kept2.push(to)}return kept2}function thisComputer(bp){if(fs142.existsSync(bp.localMetaFile))return!1;let meta=readMeta(bp);return writeMeta(bp,{...meta,user:meta.user??{name:gitUser(projectRoot(bp)),role:"Project manager"},roles:meta.roles??{analyst:"Claude",architect:"Claude",builder:"Claude",tester:"Claude",reviewer:"Claude"},tools:detectTools(),speed:meta.speed??1,parallel:meta.parallel??2,map:meta.map??[],lessons:meta.lessons??[],...meta.engine==="live"&&!meta.scan?{scan:{status:"pending"}}:{}}),!0}function pickedUpLines(picked){return picked.map(p=>p.mine?`Picked up ${p.id} from ${p.branch}${p.from?`: ${p.from} handed it over to you`:""}. The squad carries on from its documents and code`:`${p.id} came from ${p.branch}: someone else owns it, so their squad works on it`)}function ensureLiveBoard(bp,projectName2){let root=projectRoot(bp);if(boardExists(bp)){hideSquadFromGit(bp);let fresh=thisComputer(bp),meta2=readMeta(bp),picked=meta2.engine==="live"?pickUpPushed(bp):[];meta2.engine==="live"&&(meta2.tools=detectTools(),writeMeta(bp,meta2),ensureAgents(bp));let n=listTickets(bp).length,d=listDecisions(bp).length,here=`This computer: new for this project. The board comes from .squad/: ${n} ${n===1?"ticket":"tickets"} and ${d} ${d===1?"decision":"decisions"}. Agent chats, live progress and screenshots stay on the computer they ran on`;return{bp,did:[...fresh?[here]:[],...pickedUpLines(picked)]}}let from=isRepo(root)?projectFromPushed(bp.squadDir):null;if(from&&boardExists(boardPaths(bp.squadDir))){let next2=ensureLiveBoard(boardPaths(bp.squadDir),projectName2);return{bp:next2.bp,did:[`Project files: the settings, rules and knowledge came from the pushed branch ${from}`,...next2.did]}}let did=[],started2=Date.now(),nested=workspaceRepos(root),hadRepo=isRepo(root),hadFiles=fs142.readdirSync(root).some(f=>f!==SQUAD_DIR),kind=["stories","plans"].some(d=>fs142.existsSync(path133.join(bp.squadDir,d)))?"older":nested.length?"repos":hadRepo?hadFiles?"repo":"empty":hadFiles?"files":"empty",saveLater=nested.length===0&&ensureRepo(root,projectName2)==="later";for(let r of nested)ensureRepo(path133.join(root,r.path),projectName2);did.push(nested.length?`Git: found ${nested.length} code ${nested.length===1?"repo":"repos"} inside`:hadRepo?`Git: found the repo, on ${currentBranch(root)}`:saveLater?"Git: made a repo. The files already here go in its first commit next, which takes a while on a big folder":hadFiles?"Git: made a repo, with the files already here in its first commit":"Git: made a new repo for the project");let id2=newProjectId(projectName2);saveConfig(bp.configFile,{...loadConfig(bp.configFile),board:{id:id2}});let next=boardPaths(bp.squadDir);moveOldSecrets(next)&&(next=boardPaths(bp.squadDir),did.push("Sign-ins: moved out of .squad, to this computer only")),hideSquadFromGit(next);let kept2=keepOwnKnowledge(next);kept2.length&&did.push(`Your notes in knowledge/ kept as ${kept2.join(", ")}`);let tools4=detectTools(),meta={id:id2,layout:LAYOUT,documentsInGit:!0,project:projectName2,key:keyFor2(projectName2),nextNumber:1,engine:"live",user:{name:gitUser(root),role:"Project manager"},roles:{analyst:"Claude",architect:"Claude",builder:"Claude",tester:"Claude",reviewer:"Claude"},tools:tools4,speed:1,map:[],lessons:[],commands:{},git:{base:currentBranch(root),remote:remotes(root)[0]??""},...nested.length?{repos:nested}:{},scan:{status:"pending"},parallel:2,setup:{...newSetup(kind,started2),...saveLater?{steps:newSetup(kind,started2).steps.map(s=>s.id==="files"?{id:s.id,state:"todo"}:s)}:{}}};return writeMeta(next,meta),ensureAgents(next),did.push(`Board: ready, with rules for the five roles; tickets are named ${meta.key}-1, ${meta.key}-2`),did.push(`AI tools: ${tools4.map(t=>`${t.name} ${t.ready?"ready":"not installed"}`).join(", ")}`),{bp:next,did}}function requireGit(){try{execSync2("git --version",{stdio:"ignore",env:gitEnv()})}catch{throw new Error("squad-kit needs git. On a Mac, run this once, then run squad console again: xcode-select --install")}}function prepareProject(root){let did=[];if(!isOwnFolder(root))throw new Error("squad-kit works inside a project folder. Pick the project's own folder, or a new, empty one.");fs142.existsSync(path133.join(root,SQUAD_DIR,"config.yaml"))||(setupProject(root),did.push("Project files: .squad/ with the settings and a README"));let paths=buildPaths(root),broken=settingsProblem(boardPaths(paths.squadDir));if(broken)throw new Error(broken);let config2=loadConfig(paths.configFile),opened=openBoard(paths.squadDir);if(opened.moved){let m=opened.moved;did.push(`Moved this board to the new layout: ${m.tickets} tickets, ${m.decisions} decisions and ${m.releases} releases, all checked.`),did.push(`The project's files are in .squad/; this computer's are in ${m.localDir}. A copy of the old files: ${m.backup}`)}boardExists(opened.bp)||requireGit();let setup=ensureLiveBoard(opened.bp,config2.project.name);return did.push(...setup.did),{paths:buildPaths(root),bp:setup.bp,did,fresh:setup.did.length>0}}import fs143 from"fs";import os22 from"os";import path134 from"path";var MAX_NAME3=80,isFolder=dir=>fs143.existsSync(dir)&&fs143.statSync(dir).isDirectory();function folderNameFor(name){return[...name.replace(/[/\\:*?"<>|\u0000-\u001f]/g," ").replace(/\s+/g," ").replace(/^[. ]+/,"").replace(/[. ]+$/,"")].slice(0,MAX_NAME3).join("").trim()}function defaultPlace(){let documents=path134.join(os22.homedir(),"Documents");return isFolder(documents)?documents:os22.homedir()}function whyNotHere(dir){let project=findSquadRoot(dir);return project?`This folder is inside the project ${path134.basename(project)}. Pick a place outside it.`:isRepo(dir)?"This folder is inside another project's code. Pick a place outside it.":null}function newProjectFolder(input7){let name=folderNameFor(input7.name??"");if(!name)throw new Error("Give the project a name.");let brd=!!(input7.file||input7.text?.trim());if(!brd&&!input7.idea?.trim())throw new Error("Add your BRD, or say what you want to build.");if(brd&&input7.idea?.trim())throw new Error("Give a BRD or an idea, not both.");let place=path134.resolve(input7.where?.trim()||defaultPlace());if(!isFolder(place))throw new Error(`There is no folder at ${place}.`);let why=whyNotHere(place);if(why)throw new Error(why);let dir=path134.join(place,name);if(fs143.existsSync(dir))throw new Error(`${name} is there already. Pick another name, or press Add project to open it.`);return dir}function startFrom(bp,input7){if(input7.idea?.trim())return startWithIdea(bp,input7.idea),"/setup";let b=addBreakdown(bp,{...input7.file?{file:input7.file}:{},...input7.text?.trim()?{text:input7.text}:{},with:input7.with??[]});return skipFirstRead(bp),`/requirements/${encodeURIComponent(b.id)}`}function dropNewProject(dir,bp){fs143.rmSync(dir,{recursive:!0,force:!0}),bp&&fs143.rmSync(bp.localDir,{recursive:!0,force:!0})}function makeNewProject(input7){let dir=newProjectFolder(input7);fs143.mkdirSync(dir);let bp;try{let ready=prepareProject(dir);return bp=ready.bp,{...ready,opens:startFrom(ready.bp,input7)}}catch(err2){throw dropNewProject(dir,bp),err2}}async function act(c,fn){try{return c.json(await fn()??{ok:!0})}catch(err2){if(isDiskTrouble(err2))return c.json({error:fileTroubleWords(err2)},400);let said=plainWords(err2,`${c.req.method} ${new URL(c.req.url).pathname} failed`);return c.json({error:said},wordsForPeople(err2)===null?500:400)}}var NOISE2=new Set(["node_modules","dist","build","coverage","__pycache__","Library","Applications"]),MAX_FOLDERS=500,has3=(dir,...parts2)=>fs144.existsSync(path135.join(dir,...parts2));function listFolder(asked){let home2=os23.homedir(),dir=path135.resolve(asked||home2);if(!fs144.existsSync(dir)||!fs144.statSync(dir).isDirectory())throw new Error(`There is no folder at ${dir}.`);let entries;try{entries=fs144.readdirSync(dir,{withFileTypes:!0})}catch{throw new Error(`squad-kit may not look inside ${dir}.`)}let visible=entries.filter(e=>!e.name.startsWith(".")),folders=visible.filter(e=>e.isDirectory()&&!NOISE2.has(e.name)).sort((a,b)=>a.name.localeCompare(b.name,void 0,{sensitivity:"base"})).slice(0,MAX_FOLDERS).map(e=>{let p=path135.join(dir,e.name);return{name:e.name,path:p,kind:has3(p,SQUAD_DIR,"config.yaml")?"project":has3(p,".git")?"repo":"folder"}}),repos=folders.filter(f=>f.kind!=="folder"&&has3(f.path,".git")).length,kind=has3(dir,SQUAD_DIR,"config.yaml")?"project":has3(dir,".git")?"repo":repos?"repos":visible.length?"files":"empty",parent=path135.dirname(dir)===dir?null:path135.dirname(dir),places=[{name:"Home",path:home2},...["Desktop","Documents","Workspace","Projects","Developer","code"].map(name=>({name,path:path135.join(home2,name)})).filter(p=>fs144.existsSync(p.path))],canAdd=kind==="project"||isOwnFolder(dir),notHere=whyNotHere(dir);return{path:dir,parent,home:home2,kind,repos,canAdd,folders,places,place:defaultPlace(),...notHere?{notHere}:{}}}function lookAt3(asked){let dir=path135.resolve(asked);if(!fs144.existsSync(dir)||!fs144.statSync(dir).isDirectory())throw new Error(`There is no folder at ${dir}.`);if(!has3(dir,SQUAD_DIR,"config.yaml")&&!isOwnFolder(dir))throw new Error("This folder can't be a project.");return firstLook(dir)}async function body(c){try{return await c.req.json()}catch{return{}}}function mountAppApi(app,opts){let projects=opts.projects,settings=()=>({parallel:parallelLimit(),max:MAX_PARALLEL,running:ticketsRunning(),welcomed:welcomed()});app.get("/api/v1/app/projects",c=>c.json({projects:projects.summaries(),...settings()})),app.post("/api/v1/app/projects",async c=>{let b=await body(c);return act(c,()=>{if(!b.folder?.trim())throw new Error("Pick a folder first.");let{project,did,added:added2}=projects.add(b.folder.trim(),b.choice==="above"||b.choice==="here"?b.choice:void 0);return{...projects.summaries().find(p=>p.id===project.id),did,added:added2}})}),app.get("/api/v1/app/ready",c=>act(c,()=>computerReady())),app.post("/api/v1/app/sample",c=>act(c,()=>{let{id:id2}=projects.openSample();return projects.summaries().find(p=>p.id===id2)})),app.post("/api/v1/app/projects/new",async c=>{let b=await body(c),str4=v=>typeof v=="string"?v:void 0,words2={name:str4(b.name),where:str4(b.where),idea:str4(b.idea),text:str4(b.text)};return act(c,()=>{let{project,did,opens}=projects.create({...words2,...b.file?{file:b.file}:{},...Array.isArray(b.with)?{with:b.with}:{}});return{...projects.summaries().find(p=>p.id===project.id),did,opens}})}),app.delete("/api/v1/app/projects/:id",c=>act(c,()=>{if(!projects.remove(c.req.param("id")))throw new Error("That project is not in the app.");return{ok:!0}})),app.get("/api/v1/app/waiting",c=>c.json({items:projects.waiting()})),app.get("/api/v1/app/folders",c=>act(c,()=>listFolder(c.req.query("path")??""))),app.get("/api/v1/app/folders/look",c=>act(c,()=>lookAt3(c.req.query("path")??""))),app.post("/api/v1/app/folders",async c=>{let b=await body(c);return act(c,()=>{let name=(b.name??"").trim();if(!name||name.startsWith(".")||/[/\\:]/.test(name))throw new Error("Give the folder a name without / or : in it.");let dir=path135.join(path135.resolve(b.parent||os23.homedir()),name);if(fs144.existsSync(dir))throw new Error(`${name} is there already.`);return fs144.mkdirSync(dir),listFolder(dir)})}),app.get("/api/v1/app/settings",c=>c.json(settings())),app.put("/api/v1/app/settings",async c=>{let b=await body(c);return act(c,()=>(b.parallel!==void 0&&setParallelLimit(Number(b.parallel)),b.welcomed===!0&&setWelcomed(),settings()))})}function mountUpdateApi(app,updates){app.get("/api/v1/app/update",c=>c.json(updates.status())),app.post("/api/v1/app/update/check",c=>act(c,()=>updates.check({fresh:!0}))),app.post("/api/v1/app/update",async c=>{let b=await body(c);return act(c,()=>updates.start({now:b.now===!0}))}),app.delete("/api/v1/app/update",c=>act(c,()=>updates.close())),app.put("/api/v1/app/update",async c=>{let b=await body(c);return act(c,()=>updates.prefs({daily:typeof b.daily=="boolean"?b.daily:void 0,later:typeof b.later=="string"||b.later===null?b.later:void 0}))}),app.post("/api/v1/app/update/restart",c=>act(c,()=>updates.restartNow()))}import{HTTPException}from"hono/http-exception";import{execFileSync as execFileSync14}from"child_process";import fs146 from"fs";import os25 from"os";import path137 from"path";import yaml19 from"js-yaml";import fs145 from"fs";import os24 from"os";import path136 from"path";import yaml18 from"js-yaml";function listFile(){return path136.join(squadHome(),"projects.json")}function readProjects(){try{let raw=JSON.parse(fs145.readFileSync(listFile(),"utf8"));return Array.isArray(raw.projects)?raw.projects.filter(p=>p&&typeof p.id=="string"&&typeof p.root=="string"):[]}catch{return[]}}function writeProjects(list6){fs145.mkdirSync(squadHome(),{recursive:!0});let file3=listFile(),tmp=`${file3}.${process.pid}.tmp`;fs145.writeFileSync(tmp,`${JSON.stringify({projects:list6},null,2)}
1832
- `,"utf8"),fs145.renameSync(tmp,file3)}function rememberProject(id2,root){let list6=readProjects(),same5=p=>p.root===root||p.id===id2,old=list6.find(same5),entry={id:id2,root,addedAt:old?.addedAt??new Date().toISOString()},next=list6.flatMap(p=>p===old?[entry]:same5(p)?[]:[p]);return writeProjects(old?next:[...next,entry]),entry}function forgetProject(id2){let list6=readProjects(),left2=list6.filter(p=>p.id!==id2);return left2.length===list6.length?!1:(writeProjects(left2),!0)}function projectName(root){try{let raw=yaml18.load(fs145.readFileSync(path136.join(root,SQUAD_DIR,"config.yaml"),"utf8"));return String(raw?.board?.project||raw?.project?.name||path136.basename(root))}catch{return path136.basename(root)}}var norm=s=>s.trim().toLowerCase().replace(/[^\p{L}\p{N}]+/gu,"-").replace(/^-+|-+$/g,"");function findProject(ref2){let asked=ref2.trim(),asPath=path136.resolve(asked.replace(/^~(?=$|\/)/,os24.homedir()));if(fs145.existsSync(asPath)&&fs145.statSync(asPath).isDirectory()){let root=findSquadRoot(asPath);if(root)return root}let list6=readProjects().filter(p=>fs145.existsSync(path136.join(p.root,SQUAD_DIR,"config.yaml"))),hit=list6.find(p=>p.id===asked)??list6.find(p=>norm(projectName(p.root))===norm(asked))??list6.find(p=>norm(path136.basename(p.root))===norm(asked));if(hit)return hit.root;let known=list6.map(p=>projectName(p.root));throw new Error(`No project called "${asked}".${known.length?` Your projects: ${known.join(", ")}.`:""} To add one, open a terminal in its folder and run: squad console`)}import{deflateRawSync}from"zlib";var TABLE=(()=>{let t=new Uint32Array(256);for(let n=0;n<256;n+=1){let c=n;for(let k=0;k<8;k+=1)c=c&1?3988292384^c>>>1:c>>>1;t[n]=c>>>0}return t})();function crc32(data){let c=4294967295;for(let i=0;i<data.length;i+=1)c=TABLE[(c^data[i])&255]^c>>>8;return(c^4294967295)>>>0}function dosTime(d){let year=Math.max(1980,d.getFullYear());return{time:d.getHours()<<11|d.getMinutes()<<5|Math.floor(d.getSeconds()/2),date:year-1980<<9|d.getMonth()+1<<5|d.getDate()}}function zip(entries){let parts2=[],central=[],offset=0,seen=new Set;for(let e of entries){let name=e.name.replace(/\\/g,"/").replace(/^\/+/,"");if(!name||name.split("/").includes(".."))throw new Error(`Not a safe name in a zip: ${e.name}`);if(seen.has(name))throw new Error(`Two files named ${name} in one zip.`);seen.add(name);let raw=Buffer.isBuffer(e.data)?e.data:Buffer.from(e.data,"utf8"),packed=deflateRawSync(raw,{level:9}),deflate=packed.length<raw.length,body10=deflate?packed:raw,nameBytes=Buffer.from(name,"utf8"),crc=crc32(raw),{time:time2,date}=dosTime(e.date??new Date),flags=2048,method=deflate?8:0,local=Buffer.alloc(30);local.writeUInt32LE(67324752,0),local.writeUInt16LE(20,4),local.writeUInt16LE(flags,6),local.writeUInt16LE(method,8),local.writeUInt16LE(time2,10),local.writeUInt16LE(date,12),local.writeUInt32LE(crc,14),local.writeUInt32LE(body10.length,18),local.writeUInt32LE(raw.length,22),local.writeUInt16LE(nameBytes.length,26),local.writeUInt16LE(0,28),parts2.push(local,nameBytes,body10);let dir=Buffer.alloc(46);if(dir.writeUInt32LE(33639248,0),dir.writeUInt16LE(788,4),dir.writeUInt16LE(20,6),dir.writeUInt16LE(flags,8),dir.writeUInt16LE(method,10),dir.writeUInt16LE(time2,12),dir.writeUInt16LE(date,14),dir.writeUInt32LE(crc,16),dir.writeUInt32LE(body10.length,20),dir.writeUInt32LE(raw.length,24),dir.writeUInt16LE(nameBytes.length,28),dir.writeUInt16LE(0,30),dir.writeUInt16LE(0,32),dir.writeUInt16LE(0,34),dir.writeUInt16LE(0,36),dir.writeUInt32LE(33188<<16>>>0,38),dir.writeUInt32LE(offset,42),central.push(dir,nameBytes),offset+=local.length+nameBytes.length+body10.length,offset>4294967295)throw new Error("The report is too large for a zip file.")}let dirBytes=Buffer.concat(central),end=Buffer.alloc(22);return end.writeUInt32LE(101010256,0),end.writeUInt16LE(0,4),end.writeUInt16LE(0,6),end.writeUInt16LE(entries.length,8),end.writeUInt16LE(entries.length,10),end.writeUInt32LE(dirBytes.length,12),end.writeUInt32LE(offset,16),end.writeUInt16LE(0,20),Buffer.concat([...parts2,dirBytes,end])}var DAY=864e5,NEVER="Never included: your tokens, passwords, secrets.yaml, console.json or .env files.",MAX_CONVERSATION=3*1024*1024;function run5(cmd15,args){try{return execFileSync14(cmd15,args,{encoding:"utf8",stdio:["ignore","pipe","ignore"],timeout:8e3,...cmd15==="git"?{env:gitEnv()}:{}}).trim()||null}catch{return null}}function versions(){let mac=process.platform==="darwin"?run5("sw_vers",["-productVersion"]):null;return{squadKit:readInstalledPackage().version,node:process.version,git:run5("git",["--version"]),system:`${mac?`macOS ${mac}`:os25.type()} (${os25.platform()} ${os25.release()}, ${os25.arch()})`,tools:AI_TOOLS.filter(t=>t.canRun).map(t=>({name:t.label,version:t.version?.()??null}))}}function reportFolder(){if(process.env.SQUAD_KIT_HOME?.trim())return path137.join(squadHome(),"reports");let downloads=path137.join(os25.homedir(),"Downloads");return fs146.existsSync(downloads)?downloads:path137.join(squadHome(),"reports")}function slug(s){return s.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"project"}function projectOf(bp){if(bp)try{return boardExists(bp)?readMeta(bp).project||path137.basename(bp.root):path137.basename(bp.root)}catch{return path137.basename(bp.root)}}function reportName(project,ticket){return project?`${slug(project)}${ticket?`-${ticket.toUpperCase()}`:""}-report.zip`:"squad-kit-report.zip"}function planReport(input7){let project=projectOf(input7.bp),ticket=input7.ticket?.toUpperCase(),tools4=AI_TOOLS.filter(t=>t.canRun).map(t=>t.label),holds=[...ticket?[`${ticket}'s timeline, status and each step's time`,"The agent conversations for this ticket"]:[],...project&&!ticket?[`How ${project}'s tickets stand, and its settings on the board`]:[],...project?[]:["The projects in squad-kit and how each one stands"],"squad-kit's own log for the last 24 hours",`Versions: squad-kit, ${tools4.join(", ")}${tools4.length?", ":""}Node, git, the system`];return{name:reportName(project,ticket),folder:input7.dir??reportFolder(),holds,never:NEVER,command:`squad report${ticket?` ${ticket}`:""}`}}function conversationLines(updates){let lines3=updates.map(u=>JSON.stringify(u,(key,value)=>key==="data"&&typeof value=="string"&&value.length>200?`[${value.length} characters of a picture or file left out]`:value)),bytes=0,from=lines3.length;for(;from>0&&bytes+lines3[from-1].length+1<=MAX_CONVERSATION;)bytes+=lines3[--from].length+1;return`${from>0?`${JSON.stringify({note:`The first ${from} updates were left out to keep the file small.`})}
1831
+ `,{encoding:"utf8",mode:384})}function runningApp(){let info2=readAppInfo();return info2&&isAlive(info2.pid)&&!takenOver(info2.pid,info2.startedAt)?info2:null}function isAlive(pid){if(!pid||pid===process.pid)return!1;try{return process.kill(pid,0),!0}catch{return!1}}function runningConsole(bp){let info2=readConsoleInfo(bp);if(info2&&isAlive(info2.pid)&&!takenOver(info2.pid,info2.startedAt))return info2;for(let file3 of[bp.pidFile,path131.join(bp.legacyDir,"console.json"),path131.join(bp.legacyDir,"squad.pid")])try{let text3=fs140.readFileSync(file3,"utf8").trim(),pid=file3.endsWith(".json")?Number(JSON.parse(text3).pid):Number(text3);if(isAlive(pid))return file3.endsWith(".json")?{...JSON.parse(text3),pid}:{pid}}catch{}return null}function buildStamp(entry=process.argv[1]){try{return Math.round(fs140.statSync(fs140.realpathSync(entry??"")).mtimeMs)}catch{return 0}}async function stopConsole(pid,waitMs=2e4){let gone=async ms2=>{let until=Date.now()+ms2;for(;Date.now()<until;){if(!isAlive(pid))return!0;await new Promise(r=>setTimeout(r,250))}return!isAlive(pid)};try{process.kill(pid,"SIGINT")}catch{return!0}if(await gone(waitMs))return!0;try{process.kill(pid,"SIGTERM")}catch{return!0}return gone(5e3)}var MoveError=class extends Error{},read5=file3=>yaml17.load(fs141.readFileSync(file3,"utf8"));function listYaml(dir){return fs141.existsSync(dir)?fs141.readdirSync(dir).filter(f=>f.endsWith(".yaml")).map(f=>read5(path132.join(dir,f))):[]}function isMediaFile(name){return isMediaNote(name)||/\.transcript\.txt$/.test(name)||/\.(ogg|opus|oga|m4a|mp3|wav|aac|amr|flac|weba|mp4|mov|m4v|webm)\.screen/.test(name)}function copyDir(from,to){return fs141.existsSync(from)?(fs141.mkdirSync(to,{recursive:!0}),fs141.cpSync(from,to,{recursive:!0}),fs141.readdirSync(from).length):0}function repath(v,map){return typeof v=="string"?map.get(v)??v:Array.isArray(v)?v.map(x=>repath(x,map)):v&&typeof v=="object"?Object.fromEntries(Object.entries(v).map(([k,x])=>[k,repath(x,map)])):v}function same4(v){return typeof v=="string"?v.trim():Array.isArray(v)?v.map(same4):v&&typeof v=="object"?Object.fromEntries(Object.entries(v).filter(([k,x])=>x!==void 0&&!(k==="chosen"&&x===!1)).sort(([a],[b])=>a.localeCompare(b)).map(([k,x])=>[k,same4(x)])):v}function differences(label3,before,after){let a=same4(before),b=same4(after);return[...new Set([...Object.keys(a),...Object.keys(b)])].filter(k=>JSON.stringify(a[k])!==JSON.stringify(b[k])).map(k=>`${label3}: ${k}`)}function openBoard(squadDir){let bp=boardPaths(squadDir);if(!hasLegacyBoard(bp))return{bp,moved:null};if(runningConsole(bp))throw new MoveError("This board is open somewhere else. Close it first (or run: squad console --restart): squad-kit moves its files to the new layout once, while nothing runs.");let moved=moveBoard(bp);return{bp:boardPaths(squadDir),moved}}function moveBoard(bp,now3=new Date){if(!hasLegacyBoard(bp))throw new MoveError("There is no board to move: it is moved already, or there is none.");let legacy=bp.legacyDir,meta=read5(path132.join(legacy,"board.yaml")),backup=path132.join(squadHome(),"backups",`${path132.basename(bp.root)}-${now3.toISOString().replace(/[:.]/g,"-")}`);fs141.mkdirSync(path132.dirname(backup),{recursive:!0}),fs141.cpSync(bp.squadDir,backup,{recursive:!0});let id2=newProjectId(meta.project||path132.basename(bp.root)),configBefore=fs141.existsSync(bp.configFile)?fs141.readFileSync(bp.configFile,"utf8"):null,next=null,created=p=>[p.ticketsDir,p.decisionsDir,p.releasesDir,p.rulesDir,p.knowledgeDir,p.localDir],before=new Set([...created(bp)].filter(d=>fs141.existsSync(d)));try{let config2=configBefore!==null?loadConfig(bp.configFile):{...DEFAULT_CONFIG,project:{...DEFAULT_CONFIG.project,name:meta.project}};if(saveConfig(bp.configFile,{...config2,board:{id:id2}}),next=boardPaths(bp.squadDir),next.projectId!==id2)throw new MoveError("The new settings did not save the project id.");writeMeta(next,{...meta,id:id2,layout:LAYOUT,documentsInGit:meta.documentsInGit??!0});let files=copyDir(path132.join(legacy,"agents"),next.rulesDir);files+=copyDir(path132.join(legacy,"context","wiki"),next.wikiDir),files+=copyDir(path132.join(legacy,"proof"),next.proofDir),files+=copyDir(path132.join(legacy,"sessions"),next.chatsDir),fs141.existsSync(path132.join(legacy,"console.json"))&&fs141.copyFileSync(path132.join(legacy,"console.json"),next.consoleFile);let oldSecrets=path132.join(bp.squadDir,"secrets.yaml");fs141.existsSync(oldSecrets)&&(fs141.mkdirSync(path132.dirname(next.secretsFile),{recursive:!0}),fs141.copyFileSync(oldSecrets,next.secretsFile),fs141.chmodSync(next.secretsFile,384));let tickets=listYaml(path132.join(legacy,"tickets")),moved=[];for(let t of tickets){let map=new Map,from=path132.join(legacy,"attachments",t.id);if(fs141.existsSync(from))for(let name of fs141.readdirSync(from)){let src=path132.join(from,name);if(!fs141.statSync(src).isFile())continue;let dest=name==="mockup.html"?path132.join(ticketDir(next,t.id),name):isMediaFile(name)?path132.join(ticketMediaDir(next,t.id),name):path132.join(ticketFilesDir(next,t.id),name);fs141.mkdirSync(path132.dirname(dest),{recursive:!0}),fs141.copyFileSync(src,dest),map.set(src,dest),files+=1}let nt=repath(t,map);writeTicket(next,nt),moved.push(nt)}let decisions=listYaml(path132.join(legacy,"decisions"));for(let d of decisions)writeDecision(next,d);let releases=listYaml(path132.join(legacy,"releases"));for(let r of releases)writeRelease(next,r);let problems=[];for(let t of moved){let back=readTicket(next,t.id);back?problems.push(...differences(t.id,t,back)):problems.push(`${t.id}: missing`)}let decisionsBack=new Map(listDecisions(next).map(d=>[d.id,d]));for(let d of decisions)problems.push(...differences(d.id,d,decisionsBack.get(d.id)??{}));let releasesBack=new Map(listReleases(next).map(r=>[r.version,r]));for(let r of releases)problems.push(...differences(r.version,r,releasesBack.get(r.version)??{}));let metaBack=readMeta(next);for(let k of["project","key","nextNumber","engine","commands","git","repos","map","lessons"])JSON.stringify(same4(meta[k]))!==JSON.stringify(same4(metaBack[k]))&&problems.push(`board settings: ${k}`);if(JSON.stringify(same4(meta.blueprint?.stack??[]))!==JSON.stringify(same4(metaBack.blueprint?.stack??[]))&&problems.push("board settings: stack"),problems.length)throw new MoveError(`The new files did not match the old ones, so nothing changed: ${problems.slice(0,8).join("; ")}`);return fs141.rmSync(legacy,{recursive:!0,force:!0}),fs141.rmSync(oldSecrets,{force:!0}),{tickets:moved.length,decisions:decisions.length,releases:releases.length,files,backup,localDir:next.localDir}}catch(err2){for(let dir of[...created(bp),...next?created(next):[]])before.has(dir)||fs141.rmSync(dir,{recursive:!0,force:!0});throw configBefore===null?fs141.rmSync(bp.configFile,{force:!0}):writeConfigFile(bp.configFile,configBefore),err2 instanceof MoveError?err2:new MoveError(`Moving the board failed, so nothing changed: ${err2.message}`)}}function keyFor2(name){let words2=name.split(/[^A-Za-z0-9]+/).filter(Boolean);return(words2.length>1?words2.map(w=>w[0]):[words2[0]?.slice(0,2)??"SK"]).join("").toUpperCase().slice(0,3)||"SK"}function gitUser(dir){try{return execSync2("git config user.name",{encoding:"utf8",stdio:["ignore","pipe","ignore"],env:gitEnv(),...dir&&fs142.existsSync(dir)?{cwd:dir}:{}}).trim().split(/\s+/)[0]||"You"}catch{return"You"}}function detectTools(){return AI_TOOLS.map(t=>({name:t.label,ready:t.ready()}))}var SQUAD_README="# squad-kit\n\nThis folder is the project's memory, kept by squad-kit. People can read it, and edit it by hand.\n\n- `config.yaml`: the project's settings.\n- `tickets/<ID>/`: each ticket's spec, questions, design, notes and proof.\n- `decisions/`: what the squad decided, and why.\n- `releases/`: what each release carried.\n- `rules/`: how each role works on this project.\n- `knowledge/`: what the squad learned about the project.\n- `how-to-work-here.md`: how to run a story in a conversation in any AI tool, kept up to date by squad-kit.\n\nLive progress, agent chats, screenshots and sign-ins stay on each person's computer, in `~/.squad-kit/`.\n\nOpen the board: `squad console`\n";function setupProject(root){let squadDir=path133.join(root,SQUAD_DIR),name=path133.basename(path133.resolve(root));fs142.mkdirSync(squadDir,{recursive:!0}),saveConfig(path133.join(squadDir,"config.yaml"),{...DEFAULT_CONFIG,project:{...DEFAULT_CONFIG.project,name}});let readme=path133.join(squadDir,"README.md");return fs142.existsSync(readme)||fs142.writeFileSync(readme,SQUAD_README,"utf8"),name}function moveOldSecrets(bp){let old=path133.join(bp.squadDir,"secrets.yaml"),local=path133.join(bp.localDir,"secrets.yaml");return!fs142.existsSync(old)||fs142.existsSync(local)?!1:(fs142.mkdirSync(bp.localDir,{recursive:!0}),fs142.copyFileSync(old,local),fs142.chmodSync(local,384),fs142.rmSync(old),!0)}function keepOwnKnowledge(bp){let kept2=[];for(let name of["project.md","lessons.md","stack.md"]){let file3=path133.join(bp.knowledgeDir,name);if(!fs142.existsSync(file3))continue;let to=name.replace(/\.md$/,".before-squad-kit.md");fs142.renameSync(file3,path133.join(bp.knowledgeDir,to)),kept2.push(to)}return kept2}function thisComputer(bp){if(fs142.existsSync(bp.localMetaFile))return!1;let meta=readMeta(bp);return writeMeta(bp,{...meta,user:meta.user??{name:gitUser(projectRoot(bp)),role:"Project manager"},roles:meta.roles??{analyst:"Claude",architect:"Claude",builder:"Claude",tester:"Claude",reviewer:"Claude"},tools:detectTools(),speed:meta.speed??1,parallel:meta.parallel??2,map:meta.map??[],lessons:meta.lessons??[],...meta.engine==="live"&&!meta.scan?{scan:{status:"pending"}}:{}}),!0}function pickedUpLines(picked){return picked.map(p=>p.mine?`Picked up ${p.id} from ${p.branch}${p.from?`: ${p.from} handed it over to you`:""}. The squad carries on from its documents and code`:`${p.id} came from ${p.branch}: someone else owns it, so their squad works on it`)}function ensureLiveBoard(bp,projectName2){let root=projectRoot(bp);if(boardExists(bp)){hideSquadFromGit(bp);let fresh=thisComputer(bp),meta2=readMeta(bp),picked=meta2.engine==="live"?pickUpPushed(bp):[];meta2.engine==="live"&&(meta2.tools=detectTools(),writeMeta(bp,meta2),ensureAgents(bp));let n=listTickets(bp).length,d=listDecisions(bp).length,here=`This computer: new for this project. The board comes from .squad/: ${n} ${n===1?"ticket":"tickets"} and ${d} ${d===1?"decision":"decisions"}. Agent chats, live progress and screenshots stay on the computer they ran on`;return{bp,did:[...fresh?[here]:[],...pickedUpLines(picked)]}}let from=isRepo(root)?projectFromPushed(bp.squadDir):null;if(from&&boardExists(boardPaths(bp.squadDir))){let next2=ensureLiveBoard(boardPaths(bp.squadDir),projectName2);return{bp:next2.bp,did:[`Project files: the settings, rules and knowledge came from the pushed branch ${from}`,...next2.did]}}let did=[],started2=Date.now(),nested=workspaceRepos(root),hadRepo=isRepo(root),hadFiles=fs142.readdirSync(root).some(f=>f!==SQUAD_DIR),kind=["stories","plans"].some(d=>fs142.existsSync(path133.join(bp.squadDir,d)))?"older":nested.length?"repos":hadRepo?hadFiles?"repo":"empty":hadFiles?"files":"empty",saveLater=nested.length===0&&ensureRepo(root,projectName2)==="later";for(let r of nested)ensureRepo(path133.join(root,r.path),projectName2);did.push(nested.length?`Git: found ${nested.length} code ${nested.length===1?"repo":"repos"} inside`:hadRepo?`Git: found the repo, on ${currentBranch(root)}`:saveLater?"Git: made a repo. The files already here go in its first commit next, which takes a while on a big folder":hadFiles?"Git: made a repo, with the files already here in its first commit":"Git: made a new repo for the project");let id2=newProjectId(projectName2);saveConfig(bp.configFile,{...loadConfig(bp.configFile),board:{id:id2}});let next=boardPaths(bp.squadDir);moveOldSecrets(next)&&(next=boardPaths(bp.squadDir),did.push("Sign-ins: moved out of .squad, to this computer only")),hideSquadFromGit(next);let kept2=keepOwnKnowledge(next);kept2.length&&did.push(`Your notes in knowledge/ kept as ${kept2.join(", ")}`);let tools4=detectTools(),meta={id:id2,layout:LAYOUT,documentsInGit:!0,project:projectName2,key:keyFor2(projectName2),nextNumber:1,engine:"live",user:{name:gitUser(root),role:"Project manager"},roles:{analyst:"Claude",architect:"Claude",builder:"Claude",tester:"Claude",reviewer:"Claude"},tools:tools4,speed:1,map:[],lessons:[],commands:{},git:{base:currentBranch(root),remote:remotes(root)[0]??""},...nested.length?{repos:nested}:{},scan:{status:"pending"},parallel:2,setup:{...newSetup(kind,started2),...saveLater?{steps:newSetup(kind,started2).steps.map(s=>s.id==="files"?{id:s.id,state:"todo"}:s)}:{}}};return writeMeta(next,meta),ensureAgents(next),did.push(`Board: ready, with rules for the five roles; tickets are named ${meta.key}-1, ${meta.key}-2`),did.push(`AI tools: ${tools4.map(t=>`${t.name} ${t.ready?"ready":"not installed"}`).join(", ")}`),{bp:next,did}}function requireGit(){try{execSync2("git --version",{stdio:"ignore",env:gitEnv()})}catch{throw new Error("squad-kit needs git. On a Mac, run this once, then run squad console again: xcode-select --install")}}function prepareProject(root){let did=[];if(!isOwnFolder(root))throw new Error("squad-kit works inside a project folder. Pick the project's own folder, or a new, empty one.");fs142.existsSync(path133.join(root,SQUAD_DIR,"config.yaml"))||(setupProject(root),did.push("Project files: .squad/ with the settings and a README"));let paths=buildPaths(root),broken=settingsProblem(boardPaths(paths.squadDir));if(broken)throw new Error(broken);let config2=loadConfig(paths.configFile),opened=openBoard(paths.squadDir);if(opened.moved){let m=opened.moved;did.push(`Moved this board to the new layout: ${m.tickets} tickets, ${m.decisions} decisions and ${m.releases} releases, all checked.`),did.push(`The project's files are in .squad/; this computer's are in ${m.localDir}. A copy of the old files: ${m.backup}`)}boardExists(opened.bp)||requireGit();let setup=ensureLiveBoard(opened.bp,config2.project.name);return did.push(...setup.did),{paths:buildPaths(root),bp:setup.bp,did,fresh:setup.did.length>0}}import fs143 from"fs";import os23 from"os";import path134 from"path";var MAX_NAME3=80,isFolder=dir=>fs143.existsSync(dir)&&fs143.statSync(dir).isDirectory();function folderNameFor(name){return[...name.replace(/[/\\:*?"<>|\u0000-\u001f]/g," ").replace(/\s+/g," ").replace(/^[. ]+/,"").replace(/[. ]+$/,"")].slice(0,MAX_NAME3).join("").trim()}function defaultPlace(){let documents=path134.join(os23.homedir(),"Documents");return isFolder(documents)?documents:os23.homedir()}function whyNotHere(dir){let project=findSquadRoot(dir);return project?`This folder is inside the project ${path134.basename(project)}. Pick a place outside it.`:isRepo(dir)?"This folder is inside another project's code. Pick a place outside it.":null}function newProjectFolder(input7){let name=folderNameFor(input7.name??"");if(!name)throw new Error("Give the project a name.");let brd=!!(input7.file||input7.text?.trim());if(!brd&&!input7.idea?.trim())throw new Error("Add your BRD, or say what you want to build.");if(brd&&input7.idea?.trim())throw new Error("Give a BRD or an idea, not both.");let place=path134.resolve(input7.where?.trim()||defaultPlace());if(!isFolder(place))throw new Error(`There is no folder at ${place}.`);let why=whyNotHere(place);if(why)throw new Error(why);let dir=path134.join(place,name);if(fs143.existsSync(dir))throw new Error(`${name} is there already. Pick another name, or press Add project to open it.`);return dir}function startFrom(bp,input7){if(input7.idea?.trim())return startWithIdea(bp,input7.idea),"/setup";let b=addBreakdown(bp,{...input7.file?{file:input7.file}:{},...input7.text?.trim()?{text:input7.text}:{},with:input7.with??[]});return skipFirstRead(bp),`/requirements/${encodeURIComponent(b.id)}`}function dropNewProject(dir,bp){fs143.rmSync(dir,{recursive:!0,force:!0}),bp&&fs143.rmSync(bp.localDir,{recursive:!0,force:!0})}function makeNewProject(input7){let dir=newProjectFolder(input7);fs143.mkdirSync(dir);let bp;try{let ready=prepareProject(dir);return bp=ready.bp,{...ready,opens:startFrom(ready.bp,input7)}}catch(err2){throw dropNewProject(dir,bp),err2}}async function act(c,fn){try{return c.json(await fn()??{ok:!0})}catch(err2){if(isDiskTrouble(err2))return c.json({error:fileTroubleWords(err2)},400);let said=plainWords(err2,`${c.req.method} ${new URL(c.req.url).pathname} failed`);return c.json({error:said},wordsForPeople(err2)===null?500:400)}}var NOISE2=new Set(["node_modules","dist","build","coverage","__pycache__","Library","Applications"]),MAX_FOLDERS=500,has3=(dir,...parts2)=>fs144.existsSync(path135.join(dir,...parts2));function listFolder(asked){let home2=os24.homedir(),dir=path135.resolve(asked||home2);if(!fs144.existsSync(dir)||!fs144.statSync(dir).isDirectory())throw new Error(`There is no folder at ${dir}.`);let entries;try{entries=fs144.readdirSync(dir,{withFileTypes:!0})}catch{throw new Error(`squad-kit may not look inside ${dir}.`)}let visible=entries.filter(e=>!e.name.startsWith(".")),folders=visible.filter(e=>e.isDirectory()&&!NOISE2.has(e.name)).sort((a,b)=>a.name.localeCompare(b.name,void 0,{sensitivity:"base"})).slice(0,MAX_FOLDERS).map(e=>{let p=path135.join(dir,e.name);return{name:e.name,path:p,kind:has3(p,SQUAD_DIR,"config.yaml")?"project":has3(p,".git")?"repo":"folder"}}),repos=folders.filter(f=>f.kind!=="folder"&&has3(f.path,".git")).length,kind=has3(dir,SQUAD_DIR,"config.yaml")?"project":has3(dir,".git")?"repo":repos?"repos":visible.length?"files":"empty",parent=path135.dirname(dir)===dir?null:path135.dirname(dir),places=[{name:"Home",path:home2},...["Desktop","Documents","Workspace","Projects","Developer","code"].map(name=>({name,path:path135.join(home2,name)})).filter(p=>fs144.existsSync(p.path))],canAdd=kind==="project"||isOwnFolder(dir),notHere=whyNotHere(dir);return{path:dir,parent,home:home2,kind,repos,canAdd,folders,places,place:defaultPlace(),...notHere?{notHere}:{}}}function lookAt3(asked){let dir=path135.resolve(asked);if(!fs144.existsSync(dir)||!fs144.statSync(dir).isDirectory())throw new Error(`There is no folder at ${dir}.`);if(!has3(dir,SQUAD_DIR,"config.yaml")&&!isOwnFolder(dir))throw new Error("This folder can't be a project.");return firstLook(dir)}async function body(c){try{return await c.req.json()}catch{return{}}}function mountAppApi(app,opts){let projects=opts.projects,settings=()=>({parallel:parallelLimit(),max:MAX_PARALLEL,running:ticketsRunning(),welcomed:welcomed()});app.get("/api/v1/app/projects",c=>c.json({projects:projects.summaries(),...settings()})),app.post("/api/v1/app/projects",async c=>{let b=await body(c);return act(c,()=>{if(!b.folder?.trim())throw new Error("Pick a folder first.");let{project,did,added:added2}=projects.add(b.folder.trim(),b.choice==="above"||b.choice==="here"?b.choice:void 0);return{...projects.summaries().find(p=>p.id===project.id),did,added:added2}})}),app.get("/api/v1/app/ready",c=>act(c,()=>computerReady())),app.post("/api/v1/app/sample",c=>act(c,()=>{let{id:id2}=projects.openSample();return projects.summaries().find(p=>p.id===id2)})),app.post("/api/v1/app/projects/new",async c=>{let b=await body(c),str4=v=>typeof v=="string"?v:void 0,words2={name:str4(b.name),where:str4(b.where),idea:str4(b.idea),text:str4(b.text)};return act(c,()=>{let{project,did,opens}=projects.create({...words2,...b.file?{file:b.file}:{},...Array.isArray(b.with)?{with:b.with}:{}});return{...projects.summaries().find(p=>p.id===project.id),did,opens}})}),app.delete("/api/v1/app/projects/:id",c=>act(c,()=>{if(!projects.remove(c.req.param("id")))throw new Error("That project is not in the app.");return{ok:!0}})),app.get("/api/v1/app/waiting",c=>c.json({items:projects.waiting()})),app.get("/api/v1/app/folders",c=>act(c,()=>listFolder(c.req.query("path")??""))),app.get("/api/v1/app/folders/look",c=>act(c,()=>lookAt3(c.req.query("path")??""))),app.post("/api/v1/app/folders",async c=>{let b=await body(c);return act(c,()=>{let name=(b.name??"").trim();if(!name||name.startsWith(".")||/[/\\:]/.test(name))throw new Error("Give the folder a name without / or : in it.");let dir=path135.join(path135.resolve(b.parent||os24.homedir()),name);if(fs144.existsSync(dir))throw new Error(`${name} is there already.`);return fs144.mkdirSync(dir),listFolder(dir)})}),app.get("/api/v1/app/settings",c=>c.json(settings())),app.put("/api/v1/app/settings",async c=>{let b=await body(c);return act(c,()=>(b.parallel!==void 0&&setParallelLimit(Number(b.parallel)),b.welcomed===!0&&setWelcomed(),settings()))})}function mountUpdateApi(app,updates){app.get("/api/v1/app/update",c=>c.json(updates.status())),app.post("/api/v1/app/update/check",c=>act(c,()=>updates.check({fresh:!0}))),app.post("/api/v1/app/update",async c=>{let b=await body(c);return act(c,()=>updates.start({now:b.now===!0}))}),app.delete("/api/v1/app/update",c=>act(c,()=>updates.close())),app.put("/api/v1/app/update",async c=>{let b=await body(c);return act(c,()=>updates.prefs({daily:typeof b.daily=="boolean"?b.daily:void 0,later:typeof b.later=="string"||b.later===null?b.later:void 0}))}),app.post("/api/v1/app/update/restart",c=>act(c,()=>updates.restartNow()))}import{HTTPException}from"hono/http-exception";import{execFileSync as execFileSync14}from"child_process";import fs146 from"fs";import os26 from"os";import path137 from"path";import yaml19 from"js-yaml";import fs145 from"fs";import os25 from"os";import path136 from"path";import yaml18 from"js-yaml";function listFile(){return path136.join(squadHome(),"projects.json")}function readProjects(){try{let raw=JSON.parse(fs145.readFileSync(listFile(),"utf8"));return Array.isArray(raw.projects)?raw.projects.filter(p=>p&&typeof p.id=="string"&&typeof p.root=="string"):[]}catch{return[]}}function writeProjects(list6){fs145.mkdirSync(squadHome(),{recursive:!0});let file3=listFile(),tmp=`${file3}.${process.pid}.tmp`;fs145.writeFileSync(tmp,`${JSON.stringify({projects:list6},null,2)}
1832
+ `,"utf8"),fs145.renameSync(tmp,file3)}function rememberProject(id2,root){let list6=readProjects(),same5=p=>p.root===root||p.id===id2,old=list6.find(same5),entry={id:id2,root,addedAt:old?.addedAt??new Date().toISOString()},next=list6.flatMap(p=>p===old?[entry]:same5(p)?[]:[p]);return writeProjects(old?next:[...next,entry]),entry}function forgetProject(id2){let list6=readProjects(),left2=list6.filter(p=>p.id!==id2);return left2.length===list6.length?!1:(writeProjects(left2),!0)}function projectName(root){try{let raw=yaml18.load(fs145.readFileSync(path136.join(root,SQUAD_DIR,"config.yaml"),"utf8"));return String(raw?.board?.project||raw?.project?.name||path136.basename(root))}catch{return path136.basename(root)}}var norm=s=>s.trim().toLowerCase().replace(/[^\p{L}\p{N}]+/gu,"-").replace(/^-+|-+$/g,"");function findProject(ref2){let asked=ref2.trim(),asPath=path136.resolve(asked.replace(/^~(?=$|\/)/,os25.homedir()));if(fs145.existsSync(asPath)&&fs145.statSync(asPath).isDirectory()){let root=findSquadRoot(asPath);if(root)return root}let list6=readProjects().filter(p=>fs145.existsSync(path136.join(p.root,SQUAD_DIR,"config.yaml"))),hit=list6.find(p=>p.id===asked)??list6.find(p=>norm(projectName(p.root))===norm(asked))??list6.find(p=>norm(path136.basename(p.root))===norm(asked));if(hit)return hit.root;let known=list6.map(p=>projectName(p.root));throw new Error(`No project called "${asked}".${known.length?` Your projects: ${known.join(", ")}.`:""} To add one, open a terminal in its folder and run: squad console`)}import{deflateRawSync}from"zlib";var TABLE=(()=>{let t=new Uint32Array(256);for(let n=0;n<256;n+=1){let c=n;for(let k=0;k<8;k+=1)c=c&1?3988292384^c>>>1:c>>>1;t[n]=c>>>0}return t})();function crc32(data){let c=4294967295;for(let i=0;i<data.length;i+=1)c=TABLE[(c^data[i])&255]^c>>>8;return(c^4294967295)>>>0}function dosTime(d){let year=Math.max(1980,d.getFullYear());return{time:d.getHours()<<11|d.getMinutes()<<5|Math.floor(d.getSeconds()/2),date:year-1980<<9|d.getMonth()+1<<5|d.getDate()}}function zip(entries){let parts2=[],central=[],offset=0,seen=new Set;for(let e of entries){let name=e.name.replace(/\\/g,"/").replace(/^\/+/,"");if(!name||name.split("/").includes(".."))throw new Error(`Not a safe name in a zip: ${e.name}`);if(seen.has(name))throw new Error(`Two files named ${name} in one zip.`);seen.add(name);let raw=Buffer.isBuffer(e.data)?e.data:Buffer.from(e.data,"utf8"),packed=deflateRawSync(raw,{level:9}),deflate=packed.length<raw.length,body10=deflate?packed:raw,nameBytes=Buffer.from(name,"utf8"),crc=crc32(raw),{time:time2,date}=dosTime(e.date??new Date),flags=2048,method=deflate?8:0,local=Buffer.alloc(30);local.writeUInt32LE(67324752,0),local.writeUInt16LE(20,4),local.writeUInt16LE(flags,6),local.writeUInt16LE(method,8),local.writeUInt16LE(time2,10),local.writeUInt16LE(date,12),local.writeUInt32LE(crc,14),local.writeUInt32LE(body10.length,18),local.writeUInt32LE(raw.length,22),local.writeUInt16LE(nameBytes.length,26),local.writeUInt16LE(0,28),parts2.push(local,nameBytes,body10);let dir=Buffer.alloc(46);if(dir.writeUInt32LE(33639248,0),dir.writeUInt16LE(788,4),dir.writeUInt16LE(20,6),dir.writeUInt16LE(flags,8),dir.writeUInt16LE(method,10),dir.writeUInt16LE(time2,12),dir.writeUInt16LE(date,14),dir.writeUInt32LE(crc,16),dir.writeUInt32LE(body10.length,20),dir.writeUInt32LE(raw.length,24),dir.writeUInt16LE(nameBytes.length,28),dir.writeUInt16LE(0,30),dir.writeUInt16LE(0,32),dir.writeUInt16LE(0,34),dir.writeUInt16LE(0,36),dir.writeUInt32LE(33188<<16>>>0,38),dir.writeUInt32LE(offset,42),central.push(dir,nameBytes),offset+=local.length+nameBytes.length+body10.length,offset>4294967295)throw new Error("The report is too large for a zip file.")}let dirBytes=Buffer.concat(central),end=Buffer.alloc(22);return end.writeUInt32LE(101010256,0),end.writeUInt16LE(0,4),end.writeUInt16LE(0,6),end.writeUInt16LE(entries.length,8),end.writeUInt16LE(entries.length,10),end.writeUInt32LE(dirBytes.length,12),end.writeUInt32LE(offset,16),end.writeUInt16LE(0,20),Buffer.concat([...parts2,dirBytes,end])}var DAY=864e5,NEVER="Never included: your tokens, passwords, secrets.yaml, console.json or .env files.",MAX_CONVERSATION=3*1024*1024;function run5(cmd15,args){try{return execFileSync14(cmd15,args,{encoding:"utf8",stdio:["ignore","pipe","ignore"],timeout:8e3,...cmd15==="git"?{env:gitEnv()}:{}}).trim()||null}catch{return null}}function versions(){let mac=process.platform==="darwin"?run5("sw_vers",["-productVersion"]):null;return{squadKit:readInstalledPackage().version,node:process.version,git:run5("git",["--version"]),system:`${mac?`macOS ${mac}`:os26.type()} (${os26.platform()} ${os26.release()}, ${os26.arch()})`,tools:AI_TOOLS.filter(t=>t.canRun).map(t=>({name:t.label,version:t.version?.()??null}))}}function reportFolder(){if(process.env.SQUAD_KIT_HOME?.trim())return path137.join(squadHome(),"reports");let downloads=path137.join(os26.homedir(),"Downloads");return fs146.existsSync(downloads)?downloads:path137.join(squadHome(),"reports")}function slug(s){return s.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"project"}function projectOf(bp){if(bp)try{return boardExists(bp)?readMeta(bp).project||path137.basename(bp.root):path137.basename(bp.root)}catch{return path137.basename(bp.root)}}function reportName(project,ticket){return project?`${slug(project)}${ticket?`-${ticket.toUpperCase()}`:""}-report.zip`:"squad-kit-report.zip"}function planReport(input7){let project=projectOf(input7.bp),ticket=input7.ticket?.toUpperCase(),tools4=AI_TOOLS.filter(t=>t.canRun).map(t=>t.label),holds=[...ticket?[`${ticket}'s timeline, status and each step's time`,"The agent conversations for this ticket"]:[],...project&&!ticket?[`How ${project}'s tickets stand, and its settings on the board`]:[],...project?[]:["The projects in squad-kit and how each one stands"],"squad-kit's own log for the last 24 hours",`Versions: squad-kit, ${tools4.join(", ")}${tools4.length?", ":""}Node, git, the system`];return{name:reportName(project,ticket),folder:input7.dir??reportFolder(),holds,never:NEVER,command:`squad report${ticket?` ${ticket}`:""}`}}function conversationLines(updates){let lines3=updates.map(u=>JSON.stringify(u,(key,value)=>key==="data"&&typeof value=="string"&&value.length>200?`[${value.length} characters of a picture or file left out]`:value)),bytes=0,from=lines3.length;for(;from>0&&bytes+lines3[from-1].length+1<=MAX_CONVERSATION;)bytes+=lines3[--from].length+1;return`${from>0?`${JSON.stringify({note:`The first ${from} updates were left out to keep the file small.`})}
1833
1833
  `:""}${lines3.slice(from).join(`
1834
1834
  `)}
1835
1835
  `}function conversations(bp,t){let ids=[...CONVERSATIONS.map(c=>({key:c.key,id:t.sessions?.[c.key]})),{key:"chat",id:t.chat?.side}].filter(x=>!!x.id),out20=[],seen=new Set;for(let{key,id:id2}of ids){if(seen.has(id2))continue;seen.add(id2);let conv=readConversation(id2,1e5,void 0,bp.chatsDir);conv&&out20.push({name:`conversations/${key}-${id2.slice(0,8)}.jsonl`,text:conversationLines(conv.updates)})}return out20}function dump(v){return yaml19.dump(v,{lineWidth:120,noRefs:!0,skipInvalid:!0})}function projectSettings(bp){try{let{user:_user,usage:_usage,...meta}=readMeta(bp);return dump({folder:bp.root,id:bp.projectId,...meta})}catch(err2){return`The board settings could not be read: ${err2.message}
@@ -1852,10 +1852,10 @@ ${proofAgainPrompt(again2)}`:prompt,permissions:["read"],schema:PROOF_SCHEMA,che
1852
1852
  `):" (nothing written)","","What is inside:"," README.txt this list",...files.map(f=>` ${f.name} ${f.what}`),"","Never included: tokens, passwords, secrets.yaml, console.json or .env files. Anything in the files above that","looked like a key, a token or a password was taken out and shows as [removed], [token removed] or [secret removed].",""].join(`
1853
1853
  `),...files.map(f=>f.text)].map((text3,i)=>({name:i===0?"README.txt":files[i-1].name,data:scrubSecrets(text3,known),date:at3}));for(let e of entries)if(isSecretFile(e.name))throw new Error(`A report never holds ${e.name}.`);return entries}function writeReport(input7){let plan=planReport(input7),data=zip(reportEntries(input7)),folders=[...new Set([plan.folder,path137.join(squadHome(),"reports")])];for(let folder of folders)try{fs146.mkdirSync(folder,{recursive:!0});let name=plan.name;for(let n=2;fs146.existsSync(path137.join(folder,name));n+=1)name=plan.name.replace(/\.zip$/,`-${n}.zip`);let file3=path137.join(folder,name);return fs146.writeFileSync(file3,data),{...plan,folder,name,file:file3,bytes:data.length}}catch(err2){if(folder===folders.at(-1))throw writeLog({level:"error",text:`Could not save a problem report in ${folders.join(" or in ")}: ${err2.code??err2.message}`}),new Error("squad-kit could not save the report. Check that the disk has room, then try again. An engineer can see why in squad-kit's log.")}throw new Error("squad-kit could not save the report.")}function onUnexpected(err2,c){return err2 instanceof HTTPException?err2.getResponse():(log.error(`${c.req.method} ${new URL(c.req.url).pathname} failed: ${err2.stack??err2.message}`),c.json({error:"squad-kit ran into a problem it did not expect, so this did not finish."},500))}function mountSupportApi(app,opts){let limit=()=>{let pause2=activePause();return{pause:pause2,...pause2?{text:limitText(pause2)}:{}}};app.get("/api/v1/app/usage-limit",c=>c.json(limit())),app.get("/api/v1/app/save-trouble",c=>{let all=saveTroubles();return c.json({text:all[0]?.text??null,detail:all.map(t=>`${t.root}: ${t.detail}`).join(`
1854
1854
  `),projects:all.map(t=>t.root)})}),app.post("/api/v1/app/usage-limit/try-now",c=>act(c,()=>(endPause()&&log.info("Try now: the queue carries on before the usage limit was due to reset"),limit())));let input7=q5=>{let p=q5.project?opts.projects.get(q5.project):void 0;if(q5.project&&!p)throw new Error("That project is not open in squad-kit.");if(q5.ticket&&!p)throw new Error("Name the project of the ticket.");return{...p?{bp:p.bp}:{projects:opts.projects.summaries()},...q5.ticket?{ticket:q5.ticket}:{},...q5.note?{note:q5.note.slice(0,4e3)}:{}}};app.get("/api/v1/app/report",c=>act(c,()=>planReport(input7({project:c.req.query("project"),ticket:c.req.query("ticket")})))),app.post("/api/v1/app/report",async c=>{let b=await body(c);return act(c,()=>{let r=writeReport(input7(b)),project=b.project?opts.projects.summaries().find(p=>p.id===b.project)?.name??b.project:void 0;return log.info(`Saved a problem report: ${r.file}`,{...project?{project}:{},...b.ticket?{ticket:b.ticket}:{}}),r})})}import fs147 from"fs";import path138 from"path";var HOUR=60*6e4,DAY2=24*HOUR,WAIT_POLL_MS=1e3,file=()=>path138.join(squadHome(),"update.json"),UpdateService=class{constructor(deps={}){this.deps=deps;this.pkg=(deps.installed??readInstalledPackage)(),this.saved=read6(),this.resume()}deps;saved;pkg;checking=null;waitTimer=null;startedAt=new Date().toISOString();installed(){return this.pkg}now(){return(this.deps.now??Date.now)()}stamp(){return new Date(this.now()).toISOString()}save(patch){this.saved={...this.saved,...patch};try{fs147.mkdirSync(squadHome(),{recursive:!0}),fs147.writeFileSync(file(),`${JSON.stringify(this.saved,null,2)}
1855
- `,"utf8")}catch{}}set(progress2){this.save({progress:{...progress2,at:this.stamp()}})}progress(){return this.saved.progress??{state:"idle"}}resume(){let p=this.progress(),current=this.installed().version;p.state==="restarting"?p.to&&isNewer(p.to,current)?this.set({...p,state:"failed",reason:`squad-kit restarted, but it still runs ${current}. The new version went somewhere else on this computer.`,permission:!1}):this.set({state:"done",from:p.from,to:current,carried:p.carried,waitedFor:p.waitedFor,now:p.now}):p.state==="waiting"?this.set({state:"idle"}):p.state==="installing"&&this.set({...p,state:"failed",reason:"squad-kit closed while it installed the update. Try again.",permission:!1})}watch(){checkForUpdates()&&this.check().catch(()=>{});let timer=setInterval(()=>{checkForUpdates()&&this.due()&&this.check().catch(()=>{})},HOUR);return timer.unref(),()=>clearInterval(timer)}due(){let at3=Date.parse(this.saved.checkedAt??"");return Number.isFinite(at3)?this.now()-at3>=(this.saved.checkError?HOUR:DAY2):!0}check(opts={}){return opts.fresh&&this.checking?this.checking.then(()=>this.check(opts)):(this.checking??=this.look(opts.fresh===!0).finally(()=>this.checking=null),this.checking)}async look(fresh){let installed3=this.installed(),latest;try{latest=(await fetchLatest(installed3.name,5e3,this.deps.fetch)).latest}catch{return this.save({checkedAt:this.stamp(),checkError:"Could not reach the npm registry. Check the internet connection."}),this.status()}this.save({checkedAt:this.stamp(),latest,checkError:void 0});let p=this.progress(),range=isNewer(latest,installed3.version)?{from:installed3.version,to:latest}:p.state==="done"&&p.from?{from:p.from,to:installed3.version}:null;if(range&&(fresh||this.notesStale(range))){let items3=await fetchReleasesBetween(installed3.repositoryUrl,range.from,range.to,{fetch:this.deps.fetch});this.save({releases:{...range,at:this.stamp(),items:items3}})}return this.status()}notesStale(range){let r=this.saved.releases;if(!r||r.from!==range.from||r.to!==range.to||!r.items.find(i=>i.version===range.to)?.lines.length)return!0;let at3=Date.parse(r.at);return!Number.isFinite(at3)||this.now()-at3>=DAY2}releasesFor(from,to){let r=this.saved.releases;return r&&r.to===to?r.items.filter(i=>(!from||isNewer(i.version,from))&&!isNewer(i.version,to)):this.saved.notes?.version===to?[this.saved.notes]:[]}status(){let installed3=this.installed(),latest=this.saved.latest??null,newer2=!!(latest&&isNewer(latest,installed3.version)),progress2=this.progress();progress2.state==="done"&&this.now()-Date.parse(progress2.at??"")>DAY2&&(progress2={state:"idle"});let about2=progress2.state==="done"?installed3.version:latest??installed3.version,releases=this.releasesFor(progress2.state==="done"?progress2.from:installed3.version,about2),notes=releases.find(r=>r.version===about2),withLines=releases.filter(r=>r.lines.length),target=progress2.to??(newer2&&latest?latest:"latest");return{current:installed3.version,latest,newer:newer2,major:!!(newer2&&latest&&isMajorBump(latest,installed3.version)),checkedAt:this.saved.checkedAt??null,...this.saved.checkError?{checkError:this.saved.checkError}:{},...notes?.lines.length?{notes:notes.lines}:{},...withLines.length?{releases:withLines}:{},notesUrl:notes?.url??changelogBlobUrl(installed3.repositoryUrl,about2),devInstall:installed3.isDevInstall,command:upgradePlan(installed3,target).text,daily:checkForUpdates(),later:this.saved.later??null,startedAt:this.startedAt,progress:progress2}}prefs(p){return typeof p.daily=="boolean"&&setCheckForUpdates(p.daily),p.later!==void 0&&this.save({later:p.later??void 0}),this.status()}async start(opts={}){let p=this.progress();if(p.state==="waiting"&&opts.now)return this.stopWaiting(),this.install(p.to,{waitedFor:p.waitedFor,now:!0}),this.status();if(p.state==="waiting"||p.state==="installing"||p.state==="restarting")return this.status();this.saved.latest||await this.check();let s=this.status();if(!s.latest||!s.newer)throw new Error(s.checkError??`You are on the latest version (${s.current}).`);if(s.devInstall)throw new Error(`This squad-kit runs from a source folder, so it does not update itself. Run: ${s.command}`);if(s.major)throw new Error(`squad-kit ${s.latest} is a big change. Read what is new first, then run: ${s.command}`);let to=s.latest;this.hold(!0);let busy=this.busy();return!busy.length||opts.now?(this.install(to,{waitedFor:[],now:!!(opts.now&&busy.length)}),this.status()):(this.set({state:"waiting",from:s.current,to,busy,waitedFor:busy}),this.waitTimer=setInterval(()=>{let still2=this.busy(),cur=this.progress();if(cur.state!=="waiting")return this.stopWaiting();if(still2.length){JSON.stringify(still2)!==JSON.stringify(cur.busy)&&this.set({...cur,busy:still2});return}this.stopWaiting(),this.install(to,{waitedFor:cur.waitedFor})},WAIT_POLL_MS),this.waitTimer.unref(),this.status())}stopWaiting(){this.waitTimer&&clearInterval(this.waitTimer),this.waitTimer=null}async install(to,how2){let installed3=this.installed(),plan=upgradePlan(installed3,to);this.set({state:"installing",from:installed3.version,to,...how2});let result;try{result=await(this.deps.install??((cmd15,args)=>runInstall(cmd15,args,{timeoutMs:INSTALL_TIMEOUT_MS})))(plan.cmd,plan.args)}catch(err2){result={ok:!1,code:null,output:"",timedOut:!1,startError:err2.message}}if(!result.ok){this.hold(!1);let why=whyInstallFailed(result,plan.cmd);this.set({state:"failed",from:installed3.version,to,...how2,...why,output:result.output.trim().slice(-4e3)});return}this.restartInto(to,how2)}restartInto(to,how2={}){this.set({state:"restarting",from:this.pkg.version,to,...how2,carried:this.carrying()}),(this.deps.restart??(()=>{}))()}restartNow(){let p=this.progress();if(p.state==="restarting")return this.status();if(p.state!=="failed"||!p.to)throw new Error("There is no update to restart for. Check for updates first.");return this.restartInto(p.to,{waitedFor:p.waitedFor}),this.status()}close(){let p=this.progress();if(p.state==="installing")throw new Error("The update is installing. It takes about a minute.");return p.state==="waiting"&&(this.stopWaiting(),this.hold(!1)),p.state!=="idle"&&this.set({state:"idle"}),this.status()}busy(){return this.deps.busy?.()??[]}carrying(){return this.deps.carrying?.()??[]}hold(on){(this.deps.hold??holdNewRuns)(on)}};function read6(){try{let raw=JSON.parse(fs147.readFileSync(file(),"utf8"));return raw&&typeof raw=="object"?raw:{}}catch{return{}}}function besideLabel(key){return key.startsWith("__chat:")?`Answering in the chat on ${key.slice(7)}`:key.startsWith("__change:")?`Writing the change for ${key.slice(9)}`:key.startsWith("__impact:")?`The Architect checks what ${key.slice(9)} changes`:key.startsWith("__tech:")?`The Architect writes the ${key.slice(7)} decision`:key==="__blueprint"?"The Architect reads the code for the architecture page":key==="__setup"?"squad-kit tries the project once: its tests and its app":key==="__talk"?"The Architect answers about the overview":key.startsWith("__breakdown:")?`The Analyst works on ${key.slice(12)}`:key.startsWith("__send:")?`Sending ${key.slice(7)} to the tracker`:"The Architect reads the project"}function label2(p){try{return boardExists(p.bp)&&readMeta(p.bp).project||projectName(p.root)}catch{return projectName(p.root)}}function runsNow(projects){let items3=[];for(let r of agentRuns()){let p=projects.all().find(x=>x.root===r.root),name=p?label2(p):projectName(r.root);for(let key of r.keys){if(key.startsWith("__")){let ticket=/^__(?:chat|change):(.+)$/.exec(key)?.[1];items3.push({project:name,...ticket?{ticket}:{},title:besideLabel(key)});continue}let title2=key;try{title2=p&&readTicket(p.bp,key)?.title||key}catch{}items3.push({project:name,ticket:key,title:title2})}}return items3}function ticketsInProgress(projects){let items3=[];for(let p of projects.all())try{if(!boardExists(p.bp)||readMeta(p.bp).engine!=="live")continue;let name=label2(p);for(let t of listTickets(p.bp))t.squad&&t.stage!=="done"&&items3.push({project:name,ticket:t.id,title:t.title})}catch{}return items3}import fs148 from"fs";import path139 from"path";import{fileURLToPath as fileURLToPath3}from"url";import{Readable as Readable2}from"stream";function pathnameForStatic(c){let rawUrl=c.env.incoming?.url;if(typeof rawUrl=="string"&&rawUrl.startsWith("/")){let pathOnly=rawUrl.split("?",1)[0]??rawUrl;try{return decodeURIComponent(pathOnly)}catch{return pathOnly}}try{return decodeURIComponent(new URL(c.req.url).pathname)}catch{return new URL(c.req.url).pathname}}function mountStaticAssets(app,_opts){let distDir=resolveConsoleUiDist();app.get("/logo.svg",c=>serveFile(c,distDir,"logo.svg","image/svg+xml")),app.get("/favicon.ico",c=>serveFile(c,distDir,"favicon.ico","image/x-icon")),app.get("*",c=>{let pathname=pathnameForStatic(c);if(pathname.startsWith("/assets/"))return assetResponse(c,distDir,pathname);if(c.req.path.startsWith("/api/")||c.req.path.startsWith("/healthz"))return c.notFound();if(!distDir)return c.text("UI not built. Run `pnpm -C console-ui build`.",404);let indexHtml=path139.join(distDir,"index.html");return fs148.existsSync(indexHtml)?c.html(fs148.readFileSync(indexHtml,"utf8"),200,{"cache-control":"no-store"}):c.text("UI not built",404)})}function assetResponse(c,distDir,pathname){if(!distDir)return c.text("UI not built",404);let rel=pathname.replace(/^\//,""),abs=path139.normalize(path139.join(distDir,rel));if(!isInside2(abs,distDir))return c.text("forbidden",403);if(!fs148.existsSync(abs)||!fs148.statSync(abs).isFile())return c.text("not found",404);let body10=Readable2.toWeb(fs148.createReadStream(abs));return new Response(body10,{headers:{"content-type":contentTypeFor(abs),"cache-control":"public, max-age=31536000, immutable"}})}function serveFile(c,distDir,name,ct){if(!distDir)return c.text("UI not built",404);let abs=path139.join(distDir,name);return fs148.existsSync(abs)?new Response(fs148.readFileSync(abs),{headers:{"content-type":ct,"cache-control":"public, max-age=86400"}}):c.text("not found",404)}function contentTypeFor(p){return p.endsWith(".js")?"application/javascript; charset=utf-8":p.endsWith(".css")?"text/css; charset=utf-8":p.endsWith(".html")?"text/html; charset=utf-8":p.endsWith(".svg")?"image/svg+xml":p.endsWith(".png")?"image/png":p.endsWith(".woff2")?"font/woff2":p.endsWith(".json")?"application/json":"application/octet-stream"}function isInside2(abs,parent){let a=path139.resolve(abs)+path139.sep,p=path139.resolve(parent)+path139.sep;return a.startsWith(p)}function resolveConsoleUiDist(){let fromEnv=process.env.SQUAD_CONSOLE_UI_DIST;if(fromEnv&&fs148.existsSync(path139.join(fromEnv,"index.html")))return fromEnv;let candidates=[fileURLToPath3(new URL("./console-ui/",import.meta.url)),fileURLToPath3(new URL("../../dist/console-ui/",import.meta.url)),fileURLToPath3(new URL("../console-ui/dist/",import.meta.url)),fileURLToPath3(new URL("../../console-ui/dist/",import.meta.url))];for(let c of candidates)if(fs148.existsSync(path139.join(c,"index.html")))return c;return null}function authMiddleware(expected){return async(c,next)=>{let presented=readPresentedToken(c.req.raw);if(!presented||!timingSafeEqualHex(presented,expected))return c.json({error:"unauthorized",detail:"missing or invalid session token"},401);await next()}}function readPresentedToken(req){let auth2=req.headers.get("authorization");return auth2&&auth2.toLowerCase().startsWith("bearer ")?auth2.slice(7).trim():req.method!=="GET"&&req.method!=="HEAD"?null:new URL(req.url).searchParams.get("t")}var OWN_NAMES=["127.0.0.1","localhost","[::1]"],READS2=new Set(["GET","HEAD","OPTIONS"]),NOT_OUR_ADDRESS=`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>squad-kit</title></head><body style="font:16px/1.5 system-ui,sans-serif;margin:3rem auto;max-width:32rem;padding:0 1rem"><h1 style="font-size:20px">This is not squad-kit's own address</h1><p>Open squad-kit from the link it shows when it starts, on this computer.</p></body></html>`;function ownAddressOnly(port){let hosts=new Set(OWN_NAMES.map(n=>`${n}:${port}`)),origins=new Set([...hosts].map(h=>`http://${h}`));return async(c,next)=>{if(!hosts.has((c.req.header("host")??"").toLowerCase()))return c.req.header("accept")?.includes("text/html")?c.html(NOT_OUR_ADDRESS,403):c.json({error:"forbidden",detail:"This is not squad-kit's own address."},403);let origin=c.req.header("origin");if(!READS2.has(c.req.method)&&origin!==void 0&&!origins.has(origin.toLowerCase()))return c.json({error:"forbidden",detail:"Changes come only from squad-kit's own pages."},403);await next()}}function timingSafeEqualHex(a,b){if(a.length!==b.length)return!1;let mismatch=0;for(let i=0;i<a.length;i+=1)mismatch|=a.charCodeAt(i)^b.charCodeAt(i);return mismatch===0}import fs165 from"fs";import path157 from"path";import{Hono}from"hono";import fs151 from"fs";import path142 from"path";import{execSync as execSync3}from"child_process";import{fileURLToPath as fileURLToPath5}from"url";function answerOf(t,id2){return t.questions?.find(q5=>q5.id===id2)?.answer}var darkMode={analystLines:["Reading the ticket and the screenshot","Looking at how staff use the dashboard at night"],questions:[{id:"q1",from:"analyst",text:"Who needs this most?",options:["Night shift staff","Managers","Everyone"]},{id:"q2",from:"analyst",text:"At first, should it follow the computer's theme?",options:["Yes","No, start light"]},{id:"q3",from:"architect",text:"Must you use a tech the client chose?",options:["No, you choose","Yes:"],inputOption:"Yes:"},{id:"q4",from:"architect",text:"Do staff share computers?",options:["Yes, shared desks","No, each has their own"]}],replies:{q1:"I ask so the switch sits where these people look first. Night shift staff live on the case list, so the switch would go in its top bar.",q2:"Some computers already switch to dark at night. With Yes, new users start with the same theme as their computer, and can still change it.",q3:"I mean a design library for buttons, menus and colors, like Angular Material. Angular itself is already saved as a project decision. If the client requires a library, I must use its dark theme.",q4:"If staff share desks, a choice saved in the browser is lost when they move. Then I would save the theme in each person's account instead."},planLines:[{who:"architect",text:"Reading the code for the theme"},{who:"architect",text:"Reading the code for accounts"},{who:"architect",text:"Checking Angular Material for dark theme support"},{who:"spec",text:"Writing the plan: 6 small steps and 9 tests"}],story(t){let who4=answerOf(t,"q1")?.choice;return`As a ${who4==="Managers"?"manager":who4==="Everyone"?"staff member":"night shift staff member"}, I want a dark mode, so my eyes don't get tired.`},criteria(t){let followsComputer=answerOf(t,"q2")?.choice!=="No, start light";return[{given:"I'm logged in",when:"I press the theme switch",then:"the dashboard turns dark"},{given:"I chose dark",when:"I log in on another computer",then:"it is still dark"},followsComputer?{given:"I'm new",when:"I log in",then:"the theme follows my computer"}:{given:"I'm new",when:"I log in",then:"the dashboard starts light"}]},decision(t){let shared=answerOf(t,"q4")?.choice!=="No, each has their own",client=answerOf(t,"q3"),lib=client?.choice==="Yes:"?client.value?.trim():void 0;return{title:shared?"Save each person's theme in their account":"Save the theme in the browser",options:[{name:"In the browser",note:"Simple, but lost when staff change desks.",chosen:!shared},{name:"In the account",note:"Follows the person to any computer.",chosen:shared}],why:shared?"Staff share desks, from your answer to question 4.":"Each person has their own computer, from your answer to question 4, so the simple way is enough.",clientCheck:lib?/material/i.test(lib)?`${lib} supports dark themes, so there is no conflict.`:`${lib} will be checked for dark theme support in step 3.`:"No client choice for this. The squad uses the existing styles."}},steps:[{title:"Add a theme setting to each account",live:["Reading the code for accounts","Writing the code for saved settings","Adding a theme column to the database"]},{title:"Theme switch in the header",live:["Writing the code for the header","Placing the switch next to the user menu"]},{title:"Dark colors for every page",live:["Writing the dark colors","Checking 14 pages for fixed colors"]},{title:"Save the choice in the account",live:["Writing the code that saves the choice","Writing the code for the theme"]},{title:"Follow the computer theme for new users",live:["Reading the browser theme setting","Writing the code for the theme"]},{title:'Tests for every "done when"',live:["Writing the test for the saved theme","Writing 9 new tests"]}],tests:21,reviewFix:{ask:"Asked for 1 fix: the page flashes white for a moment on login",fixed:"Fixed: the theme now loads before the page shows"},proof:[{criterion:"The switch turns the dashboard dark",before:"sk148-light.png",after:"sk148-dark.png",beforeLabel:"Before",afterLabel:"After",note:"Recorded by the Tester in Chrome at 1440 by 900",shows:"met"},{criterion:"Still dark on another computer",before:"sk148-dark.png",after:"sk148-dark-b.png",beforeLabel:"Computer A",afterLabel:"Computer B",note:"Same account, logged in on a second browser",shows:"met"},{criterion:"New users follow their computer theme",before:"sk148-light.png",after:"sk148-dark.png",beforeLabel:"Computer set to light",afterLabel:"Computer set to dark",note:"A new account on two computers with different settings",shows:"met"}],releaseNote:()=>"**Dark mode.** Switch the dashboard to dark. Your choice follows you to any computer."};function genericScript(t){let what=t.title.charAt(0).toLowerCase()+t.title.slice(1),isBug=t.kind==="bug";return{analystLines:isBug?["Reading the bug report","Following the steps to see it happen"]:["Reading the ticket","Looking at similar past tickets"],questions:isBug?[]:[{id:"q1",from:"analyst",text:"Who needs this most?",options:["Staff who use it daily","Managers","Everyone"]},{id:"q2",from:"analyst",text:"How soon is it needed?",options:["This week","This month","No rush"]},{id:"q3",from:"architect",text:"Must you use a tech the client chose for this?",options:["No, you choose","Yes:"],inputOption:"Yes:"}],replies:{q1:"I ask so the plan puts this where those people already work.",q2:"If it is urgent, I keep the first version small and plan the rest as a follow-up ticket.",q3:"I mean a library or tool the client requires, like a design library. If there is none, I follow the project decisions already saved."},planLines:[{who:"architect",text:"Reading the screens this touches"},{who:"architect",text:"Checking the saved project decisions"},{who:"spec",text:"Writing the plan in small steps"}],story:()=>isBug?`Fix: ${what}.`:`As a ${answerOf(t,"q1")?.choice==="Managers"?"manager":"staff member"}, I want ${what}, so my work is easier.`,criteria:()=>isBug?[{given:"the steps in the bug report",when:"I follow them",then:"the problem no longer happens"},{given:"the fix is in",when:"the checks run",then:"a new test guards this bug"}]:[{given:"I'm logged in",when:`I use ${what}`,then:"it works as the ticket describes"},{given:"something goes wrong",when:`I use ${what}`,then:"I see a clear message"}],decision:()=>isBug?void 0:{title:"Build it inside the existing screens",options:[{name:"A new page",note:"More work, and staff must learn where it is."},{name:"Inside the existing screens",note:"Staff find it where they already work.",chosen:!0}],why:"Staff should not have to learn a new place.",clientCheck:"Follows the project decisions already saved."},steps:isBug?[{title:"Reproduce the bug in a test",live:["Writing a test that shows the bug","The test fails, as expected"]},{title:"Fix the cause",live:["Reading the code behind it","Editing the fix"]},{title:"Check nothing else broke",live:["Running the related checks"]}]:[{title:"Read the related code",live:["Reading the screens this touches"]},{title:`Build ${what}`,live:["Editing the screen","Editing the API"]},{title:"Connect it to the existing screens",live:["Editing the navigation"]},{title:'Tests for every "done when"',live:["Writing the tests"]}],tests:isBug?12:16,proof:[],releaseNote:()=>isBug?`**Fixed:** ${what}.`:`**${t.title}.**`}}var SCRIPTS={"SK-148":darkMode};function scriptFor(t){return SCRIPTS[t.id]??genericScript(t)}var AMBIENT={"SK-151":["Analyst is reading the ticket","Analyst is checking the hearing calendar"],"SK-153":["Writing the code for the upload page","Writing the code for the upload limits"],"SK-146":["Running checks for the court filter","Checking filters on 3 screen sizes"],"SK-145":["Reviewer is reading the login changes","Reviewer is checking the session handling"]};var SAMPLE={questions:{input:182e3,cached:151e3,output:3100,usd:.46},spec:{input:96e3,cached:88e3,output:2400,usd:.21},design:{input:248e3,cached:205e3,output:6200,usd:.61},build:{input:142e4,cached:129e4,output:21e3,usd:1.34},fix:{input:38e4,cached:34e4,output:5800,usd:.41},review:{input:41e4,cached:352e3,output:3900,usd:.47},proof:{input:133e3,cached:101e3,output:2200,usd:.36}},PRICE={strongest:1,balanced:.6,fast:.08};function sampleUsage(bp,t,who4,step2,quality,scale=1){let s=SAMPLE[step2];if(!s)return null;let level=standardLevel({role:who4,step:step2,quality,express:t.track==="express"}).level??"balanced",tool5=aiTool(readAgent(bp,who4).tool),usage={inputTokens:Math.round(s.input*scale),cachedTokens:Math.round(s.cached*scale),outputTokens:Math.round(s.output*scale),costUsd:Math.round(s.usd*PRICE[level]*scale*100)/100};return{key:{role:who4,step:step2,level,model:tool5?.modelFor(level)??""},usage}}function countSampleStep(bp,t,who4,step2,quality,at3=Date.now(),size2=1){if(who4==="squad")return;let scale=size2*(.8+(Number(t.id.replace(/\D/g,""))*7+(t.history?.length??0)*13)%9/20),run7=sampleUsage(bp,t,who4,step2,quality??DEFAULT_QUALITY,scale);run7&&(t.usage=addRun(t.usage,run7.key,run7.usage),t.quality=quality??DEFAULT_QUALITY,addToMonth(bp,run7.usage,at3))}function countSampleHistory(bp,t,size2=1){let runs2=t.history??[],quality=t.quality;t.history=[];for(let h of runs2)countSampleStep(bp,t,h.who,h.step,quality,Date.parse(h.to),size2),t.history.push(h)}import fs149 from"fs";import path140 from"path";import{fileURLToPath as fileURLToPath4}from"url";var DEMO_SUMMARY="A web portal where court clerks and judges manage cases, hearings and documents.";function demoOverview(readAt,approved){return{users:"Court clerks, judges and team leads. They sign in with their ministry account, and each role sees its own screens.",can:["Find a case by its number or by a party name","Plan hearings on a calendar, in day/month/year","Upload case documents up to 20 MB","Export any list page to PDF","Switch between Arabic and English, right to left"],worth:["The session ends after 20 minutes with no warning, so a clerk can lose a half-filled form.","Big case lists need paging, or the page freezes.","No test opens the hearing calendar in a real browser yet."],ideas:[{title:"Warn before the session ends",why:"Clerks lose what they typed when the session ends with no warning."},{title:"Bulk assign cases to a clerk",why:"Team leads assign cases one by one today."},{title:"Add browser tests for the hearing calendar",why:"Nothing checks the calendar in a real browser yet."}],parts:[{id:"people",name:"Clerks and judges",what:"In a web browser",files:[],kind:"people"},{id:"pages",name:"Pages",what:"Case list, hearings, documents",files:["web/src/app/"],kind:"screens"},{id:"api",name:"Web API",what:"Cases, hearings and uploads",files:["api/Controllers/"],kind:"server"},{id:"rules",name:"Case rules",what:"Filing and hearing rules in one place",files:["api/Domain/"],kind:"logic"},{id:"db",name:"Database",what:"SQL Server, with migrations",files:["api/Migrations/"],kind:"data"},{id:"signin",name:"Ministry sign-in",what:"Who may open which screens",files:["api/Auth/"],kind:"outside"},{id:"tests",name:"Tests \xB7 18",what:"Jest for the logic, Playwright for the screens",files:["web/src/app/","e2e/"],kind:"tests"}],links:[{from:"people",to:"pages",label:"uses"},{from:"pages",to:"api",label:"calls"},{from:"api",to:"rules",label:"follows"},{from:"rules",to:"db",label:"stores in"},{from:"api",to:"signin",label:"signs in with"},{from:"tests",to:"pages",label:"checks"},{from:"tests",to:"rules",label:"checks"}],readAt,...approved?{approved}:{}}}var FOUND=[{title:"The front end is Angular 17",why:"Every screen in web/ is an Angular component."},{title:"The back end is a .NET 8 API",why:"api/ is one ASP.NET Core project."},{title:"SQL Server, changed only through migrations",why:"Every table change is a migration in api/Migrations/."},{title:"Angular Material for buttons and menus",why:"The client asks for it across all ministry portals."},{title:"Jest for logic, Playwright for screens",why:"Both run in the checks today."}],READS3=["Reading README.md","Reading web/package.json","Looking for *.component.ts","Reading web/src/app/cases/case-list.component.ts","Reading web/src/app/hearings/calendar.component.ts","Reading api/Controllers/CasesController.cs","Reading api/Domain/HearingRules.cs",'Searching for "Authorize"',"Reading api/Migrations/","Reading e2e/cases.spec.ts"],DEMO_FILES=214,PACE={files:1,tool:2,read:26,commands:7,tests:6,app:6,overview:1};function demoShotSource(){for(let rel of["../templates/demo/proof/sk139-before.png","../../templates/demo/proof/sk139-before.png"]){let p=fileURLToPath4(new URL(rel,import.meta.url));if(fs149.existsSync(p))return p}return null}function copyShot(bp){let src=demoShotSource();if(src)return fs149.mkdirSync(bp.proofDir,{recursive:!0}),fs149.copyFileSync(src,path140.join(bp.proofDir,"setup-app.png")),"setup-app.png"}function seedDoneSetup(bp,user,readAt){let start=Date.parse(readAt)-6e4,s=newSetup("repo",start,start+1e3),t=start+1e3;for(let id2 of["tool","read","commands","tests","app","overview"])begin(s,id2,void 0,t),t+=PACE[id2]*1e3*(id2==="read"?6:3),finish(s,id2,"done",{note:doneNote(id2)},t);s.proof=proofOf(copyShot(bp)),s.readyAt=new Date(t).toISOString(),s.approvedAt=new Date(t+12e4).toISOString(),s.qualityAt=s.approvedAt;let meta=readMeta(bp);writeMeta(bp,{...meta,scan:{status:"done",at:readAt,summary:DEMO_SUMMARY},setup:s});let o=demoOverview(readAt,{by:user,at:s.approvedAt});writeOverview(bp,{...o,ideas:o.ideas.map((i,n)=>n===0?{...i,ticket:"SK-143"}:i)})}function seedFreshSetup(bp){let meta=readMeta(bp);writeMeta(bp,{...meta,map:[],scan:{status:"pending"},setup:newSetup("repo",Date.now()-1200)}),fs149.rmSync(path140.join(bp.knowledgeDir,"overview.md"),{force:!0})}function doneNote(id2){switch(id2){case"tool":return"The squad works with it";case"read":return`Read the project: ${DEMO_FILES} files`;case"commands":return"Found how to run the tests and start the app";case"tests":return"Ran your tests: 18 pass";case"app":return"Opened the app";case"overview":return"What squad-kit understood, for you to check";default:return"A .squad folder in your project. Your code is not changed."}}function proofOf(shot){return{files:DEMO_FILES,read:DEMO_FILES,branch:"main",unsaved:0,tests:[{ok:!0,command:"npm test",passed:18,seconds:41}],app:{ok:!0,url:"http://localhost:4200",command:"npm start",...shot?{shot}:{}}}}function readDone(bp,meta){let at3=now();meta.scan={status:"done",at:at3,summary:DEMO_SUMMARY},meta.map=[{area:"Front end",detail:"Angular 17 with Angular Material, in web/"},{area:"Back end",detail:".NET 8 API, in api/"},{area:"Database",detail:"SQL Server, migrations in api/Migrations/"},{area:"Tests",detail:"Jest for logic, Playwright for screens"}],meta.commands={setup:"npm install",test:"npm test",start:"npm start",url:"http://localhost:4200",...meta.commands};let before=readOverview(bp);writeOverview(bp,{...demoOverview(at3),...before.ideas.some(i=>i.ticket)?{ideas:before.ideas}:{}});let known=listDecisions(bp);for(let f of FOUND){if(known.some(d2=>d2.title===f.title))continue;let d={id:nextDecisionId(bp),title:f.title,why:f.why,options:[f.title],chosen:f.title,from:"Project scan",approvedBy:"",proposed:!0,date:at3,checkedBy:null,tickets:[]};writeDecision(bp,d)}}var START_NOTE={read:"Starting\u2026",commands:"Installing what it needs",tests:"Running the tests",app:"Starting the app"};function playDemoSetup(bp,meta,at3=Date.now()){playDemoTalk(bp,meta,at3);let s=meta.setup;if(!s||s.approvedAt)return;let cur=currentStep(s);if(!cur||cur.state==="stopped"||cur.state==="failed")return;if(s.stopping){cur.state==="now"?finish(s,cur.id,"stopped",{note:"You stopped it."},at3):Object.assign(cur,{state:"stopped",note:"You stopped it."}),delete s.stopping,cur.id==="read"&&(meta.scan={status:"failed",at:now(),error:"You stopped it. Press Start again when you are ready."}),writeMeta(bp,meta);return}if(cur.state==="todo"){cur.id==="read"&&(meta.scan={status:"running",at:now()},s.proof={files:DEMO_FILES,read:0,branch:"main",unsaved:0},s.reads=[]),begin(s,cur.id,START_NOTE[cur.id],at3),writeMeta(bp,meta);return}let took=(at3-Date.parse(cur.since??new Date(at3).toISOString()))/1e3,need=PACE[cur.id]/(meta.speed||1);if(took<need){if(cur.id==="read"){let i=Math.min(READS3.length-1,Math.floor(took/need*READS3.length)),opened=Math.round(took/need*DEMO_FILES*.6);cur.note=`Reading the code \xB7 ${opened} files`,s.reads?.at(-1)!==READS3[i]&&(s.reads=[...s.reads??[],READS3[i]].slice(-8)),s.proof={...s.proof,read:opened},meta.scan={...meta.scan,status:"running",now:"Reading the code"},writeMeta(bp,meta)}else cur.id==="app"&&took>=need/2&&cur.note?.startsWith("Starting")&&(cur.note="Taking a screenshot",writeMeta(bp,meta));return}cur.id==="read"&&(readDone(bp,meta),s.proof={...s.proof,read:DEMO_FILES}),cur.id==="tests"&&(s.proof={...s.proof,tests:proofOf(void 0).tests}),cur.id==="app"&&(s.proof={...s.proof,app:proofOf(copyShot(bp)).app}),finish(s,cur.id,"done",{note:doneNote(cur.id)},at3),cur.id==="overview"&&(s.readyAt=new Date(at3).toISOString()),writeMeta(bp,meta)}function playDemoTalk(bp,meta,at3){let t=readTalk(bp);if(!t?.open)return;if(t.stopping){writeTalk(bp,{...t,thinking:!1,stopping:void 0,messages:t.thinking?[...t.messages,{who:"squad",text:"You stopped this answer. Ask again, or carry on.",at:now(),note:"stopped"}]:t.messages});return}if(!t.thinking)return;let last=t.messages.at(-1);if(last&&at3-Date.parse(last.at)<2200/(meta.speed||1)||!last&&at3-Date.parse(meta.setup?.readyAt??new Date(0).toISOString())<0)return;let said=t.messages.filter(m=>m.who==="you").length,text3=said===0?"Thanks for checking. What did I get wrong: who uses it, what it does, the picture of its parts, or one of the rules?":said===1?"I see. I will read the project again with that in mind and change the overview and the rules to match. Anything else before I do?":"Got it. Press **Read it again with this** and I will read the project again with what you said.";writeTalk(bp,{...t,thinking:!1,messages:[...t.messages,{who:"architect",text:text3,at:now(),...said===0?{options:["Who uses it","What it can do","One of the rules"]}:{}}]})}import fs150 from"fs";import path141 from"path";var BRD=[{title:"Case Portal BRD v1"},{p:"Business requirements for the commercial court's online case portal. Version 1, from the court's business team."},{h1:"1 Purpose"},{p:"Lawyers and parties book hearings, pay court fees and follow their cases online, in **Arabic and English**. Court staff manage hearing days and see reports. The portal runs on the court's Angular front end and .NET services."},{h1:"2 Users"},{bullets:["Lawyer: books hearings and pays fees for their clients.","Party: follows their own case and pays its fees.","Clerk: manages court rooms and hearing days.","Court admin: sees the reports and the audit log."]},{h1:"3 Book a hearing"},{h2:"3.1 Find a free slot"},{p:"A lawyer picks a court and a date range and sees the free hearing slots."},{h2:"3.2 Judge calendar"},{p:"A calendar shows the days each judge sits, with the free slots marked."},{h2:"3.3 Holding a slot"},{p:"While the lawyer fills in the booking, the slot is held for **10 minutes** so nobody else takes it."},{h2:"3.4 Confirm a booking"},{p:"The booking is confirmed once the fee is paid, or straight away for legal aid cases."},{h2:"3.5 Cancel or move a hearing"},{p:"A lawyer can cancel a hearing, or move it to another free slot, up to 48 hours before it."},{h1:"4 Court fees"},{h2:"4.1 Fees by case type"},{table:[["Case type","Fee (SAR)"],["Commercial claim","500"],["Appeal","750"],["Urgent order","300"]]},{h2:"4.2 Paying online"},{p:"Fees are paid by card in the portal."},{h2:"4.3 Receipts"},{p:"Each payment gets a receipt as a PDF, in Arabic and English."},{h2:"4.4 Refunds"},{p:"When a hearing is cancelled, the fee is refunded in full to the card that paid."},{h2:"4.5 Legal aid"},{p:"Cases with legal aid pay no fee. A clerk marks a case as legal aid."},{h1:"5 Notifications"},{numbers:["A confirmation by email and SMS when a hearing is booked.","A reminder one day before the hearing.","Every message in the language the person chose, Arabic or English."]},{h1:"6 Courts and hearing days"},{h2:"6.1 Court rooms"},{p:"Clerks add and edit court rooms, with their size and equipment."},{h2:"6.2 Hearing days"},{p:"Clerks open and close hearing days for each judge."},{h2:"6.3 Holidays and late cancels"},{p:"A clerk blocks the slots of public holidays. The fee is kept when a party cancels less than 48 hours before the hearing."},{h1:"7 Reports"},{bullets:["Hearings per court each week.","Fees paid and refunded each month.","Every report can be exported to Excel.","An audit log shows who booked, moved or cancelled a hearing."]},{h1:"8 Later phases"},{p:"A mobile app for parties comes in a later phase. It is not part of this version."}],esc=s=>s.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");function runs(text3){return text3.split(/(\*\*[^*]+\*\*)/).filter(Boolean).map(t=>t.startsWith("**")?`<w:r><w:rPr><w:b/></w:rPr><w:t xml:space="preserve">${esc(t.slice(2,-2))}</w:t></w:r>`:`<w:r><w:t xml:space="preserve">${esc(t)}</w:t></w:r>`).join("")}var para=(text3,style,num4)=>`<w:p>${style||num4?`<w:pPr>${style?`<w:pStyle w:val="${style}"/>`:""}${num4?`<w:numPr><w:ilvl w:val="0"/><w:numId w:val="${num4}"/></w:numPr>`:""}</w:pPr>`:""}${runs(text3)}</w:p>`;function blockXml(b){return"title"in b?para(b.title,"Title"):"h1"in b?para(b.h1,"Heading1"):"h2"in b?para(b.h2,"Heading2"):"p"in b?para(b.p):"bullets"in b?b.bullets.map(x=>para(x,void 0,1)).join(""):"numbers"in b?b.numbers.map(x=>para(x,void 0,2)).join(""):`<w:tbl>${b.table.map(row3=>`<w:tr>${row3.map(c=>`<w:tc>${para(c)}</w:tc>`).join("")}</w:tr>`).join("")}</w:tbl>`}var W='xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"';function demoBrdDocx(){let style=(id2,name)=>`<w:style w:type="paragraph" w:styleId="${id2}"><w:name w:val="${name}"/></w:style>`,level=fmt=>`<w:lvl w:ilvl="0"><w:numFmt w:val="${fmt}"/></w:lvl>`;return zip([{name:"[Content_Types].xml",data:'<?xml version="1.0" encoding="UTF-8"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/><Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/><Override PartName="/word/numbering.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"/></Types>'},{name:"_rels/.rels",data:'<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>'},{name:"word/_rels/document.xml.rels",data:'<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering" Target="numbering.xml"/></Relationships>'},{name:"word/styles.xml",data:`<?xml version="1.0" encoding="UTF-8"?><w:styles ${W}>${style("Title","Title")}${style("Heading1","heading 1")}${style("Heading2","heading 2")}</w:styles>`},{name:"word/numbering.xml",data:`<?xml version="1.0" encoding="UTF-8"?><w:numbering ${W}><w:abstractNum w:abstractNumId="0">${level("bullet")}</w:abstractNum><w:abstractNum w:abstractNumId="1">${level("decimal")}</w:abstractNum><w:num w:numId="1"><w:abstractNumId w:val="0"/></w:num><w:num w:numId="2"><w:abstractNumId w:val="1"/></w:num></w:numbering>`},{name:"word/document.xml",data:`<?xml version="1.0" encoding="UTF-8"?><w:document ${W}><w:body>${BRD.map(blockXml).join("")}</w:body></w:document>`}])}var ref=(section,title2,page)=>({section,title:title2,page}),PLAN=[{title:"Book a hearing",goal:"Lawyers book a hearing slot online instead of at the court desk.",ref:ref("3","Book a hearing",1),features:[{title:"Find a free slot",summary:"Lawyers see the free hearing slots of a court.",ref:ref("3.1","Find a free slot",1),stories:[{title:"Search free hearing slots",who:"lawyer",want:"to search the free hearing slots of a court by date",why:"I can pick one that suits my client",done:[["I'm signed in as a lawyer","I pick a court and a date range","I see the free slots, earliest first"],["no slot is free in that range","I search",'I see "No free slots" and the next free day']],size:"M",ref:ref("3.1","Find a free slot",1)},{title:"See a judge's free days",who:"lawyer",want:"a calendar of the days each judge sits",why:"I can plan around them",done:[["a court with three judges","I open the calendar","I see each judge's days with the free slots marked"]],size:"S",needs:[1],ref:ref("3.2","Judge calendar",2)}]},{title:"Book and confirm",summary:"A slot is held while the lawyer books, then confirmed.",ref:ref("3.3","Holding a slot",2),stories:[{title:"Hold a slot for 10 minutes",who:"lawyer",want:"my chosen slot held while I fill in the booking",why:"nobody else takes it meanwhile",done:[["a free slot","I pick it","it shows as held for me, with 10 minutes left"],["a slot held by someone else","I look at it","it shows as taken"],["10 minutes pass without booking","the time ends","the slot is free again"]],size:"M",needs:[1],ref:ref("3.3","Holding a slot",2)},{title:"Confirm a hearing booking",who:"lawyer",want:"my booking confirmed once the fee is paid",why:"the hearing is fixed in the court calendar",done:[["a held slot and a paid fee","I confirm","the hearing shows in the case and the calendar"],["a legal aid case","I book","it is confirmed with no fee"]],size:"M",needs:[3],ref:ref("3.4","Confirm a booking",2)},{title:"Cancel or move a hearing",who:"lawyer",want:"to cancel a hearing or move it to another free slot",why:"plans can change",done:[["a hearing more than 48 hours away","I move it to a free slot","the old slot is free and the new one is mine"],["a hearing less than 48 hours away","I try to cancel it","I see that it is too late to change online"]],size:"M",needs:[4],ref:ref("3.5","Cancel or move a hearing",2)}]},{title:"Notifications",summary:"People hear about their hearings by email and SMS.",ref:ref("5","Notifications",3),stories:[{title:"Email and SMS when a hearing is booked",who:"party",want:"an email and an SMS when my hearing is booked",why:"I know the date without asking my lawyer",done:[["a hearing is confirmed","the booking ends","the lawyer and the party get an email and an SMS with the date and court"]],size:"S",needs:[4],ref:ref("5","Notifications",3)},{title:"Reminder one day before the hearing",who:"party",want:"a reminder the day before my hearing",why:"I do not miss it",done:[["a hearing tomorrow","the reminders go out at 9:00","the lawyer and the party get a reminder"]],size:"S",needs:[4],ref:ref("5","Notifications",3)},{title:"Messages in Arabic and English",who:"party",want:"every message in the language I chose",why:"I understand it",done:[["my language is Arabic","a message is sent to me","it is in Arabic, right to left"],["my language is English","a message is sent to me","it is in English"]],size:"S",needs:[6],ref:ref("5","Notifications",3)}]}]},{title:"Pay court fees",goal:"Parties and lawyers pay court fees online and get a receipt.",ref:ref("4","Court fees",2),features:[{title:"Pay online",summary:"The fee of a case, paid by card, with a receipt.",ref:ref("4.2","Paying online",3),stories:[{title:"Show the fee for a case type",who:"lawyer",want:"to see the fee of a case before I book",why:"I can tell my client what it costs",done:[["a commercial claim","I start a booking","I see the fee: 500 SAR"]],size:"S",ref:ref("4.1","Fees by case type",2)},{title:"Pay the fee by card",who:"lawyer",want:"to pay the fee by card in the portal",why:"the hearing is confirmed at once",done:[["a held slot and its fee","I pay by card","the fee shows as paid and the booking is confirmed"],["the card is declined","I pay","I see why and the slot stays held"]],size:"L",needs:[9,4],ref:ref("4.2","Paying online",3)},{title:"Receipt as a PDF in two languages",who:"party",want:"a receipt for every payment",why:"I have proof that I paid",done:[["a paid fee","I open the case","I can download the receipt as a PDF in Arabic and English"]],size:"M",needs:[10],ref:ref("4.3","Receipts",3)}]},{title:"Refunds and legal aid",summary:"Money back when a hearing is cancelled, and no fee with legal aid.",ref:ref("4.4","Refunds",3),stories:[{title:"Refund the fee when a hearing is cancelled",who:"lawyer",want:"the fee refunded when my hearing is cancelled",why:"my client does not pay for a hearing that did not happen",done:[["a paid hearing","it is cancelled","the fee goes back to the card that paid"],["a refund was sent","I open the case","I see the refund and its date"]],size:"M",needs:[10,5],ref:ref("4.4","Refunds",3)},{title:"No fee for legal aid cases",who:"clerk",want:"to mark a case as legal aid",why:"its party pays no fee",done:[["a case","I mark it as legal aid","its bookings show no fee"]],size:"M",needs:[9],ref:ref("4.5","Legal aid",3)}]}]},{title:"Admin and reports",goal:"Court staff manage hearing days and see how the courts are used.",ref:ref("6","Courts and hearing days",4),features:[{title:"Courts and hearing days",summary:"Clerks keep the court rooms and hearing days up to date.",ref:ref("6.1","Court rooms",4),stories:[{title:"Add and edit court rooms",who:"clerk",want:"to add and edit court rooms",why:"bookings go to rooms that exist",done:[["I am a clerk","I add a room with its size and equipment","it shows in the list of rooms"]],size:"S",ref:ref("6.1","Court rooms",4)},{title:"Open and close hearing days",who:"clerk",want:"to open and close a judge's hearing days",why:"only real days can be booked",done:[["a judge","I open next Monday for hearings","its slots can be booked"],["a day with bookings","I try to close it","I see the bookings to move first"]],size:"M",needs:[14],ref:ref("6.2","Hearing days",4)},{title:"Block slots on public holidays",who:"clerk",want:"to block the slots of a public holiday",why:"nobody books a day the court is closed",done:[["a public holiday","I block it","none of its slots can be booked"]],size:"S",needs:[15],ref:ref("6.3","Holidays and late cancels",4)}]},{title:"Reports",summary:"How the courts are used, and who changed what.",ref:ref("7","Reports",4),stories:[{title:"Hearings per court each week",who:"court admin",want:"a weekly report of hearings per court",why:"I see which courts are busy",done:[["last week had hearings","I open the report","I see the hearings of each court, day by day"]],size:"M",needs:[4],ref:ref("7","Reports",4)},{title:"Fees report for each month",who:"court admin",want:"a monthly report of the fees paid and paid back",why:"the finance team can check them",done:[["a month with payments and money paid back","I open the report","I see the totals paid and paid back per case type"]],size:"M",needs:[10,12],ref:ref("7","Reports",4)},{title:"Export a report to Excel",who:"court admin",want:"to export any report to Excel",why:"I can share it",done:[["a report on screen","I press Export","I get an Excel file with the same rows"]],size:"S",needs:[17],ref:ref("7","Reports",4)},{title:"Audit log of hearing changes",who:"court admin",want:"a log of who booked, moved or cancelled a hearing",why:"I can answer complaints",done:[["a hearing was moved","I open the audit log","I see who moved it, when, and from which slot"]],size:"M",needs:[5],ref:ref("7","Reports",4)}]}]}],LEFT_OUT2=[{text:"A mobile app for parties",why:"The BRD says it comes in a later phase.",ref:ref("8","Later phases",5)}],SUMMARY="The court's case portal: lawyers and parties book hearings, pay court fees and get notified, in Arabic and English. Clerks manage court rooms and hearing days, and court admins see reports and an audit log.";function demoQuestions(){return[{id:"q1",from:"analyst",kind:"clash",text:"Section 4.4 refunds the fee in full for every cancelled hearing. Section 6.3 keeps the fee when a party cancels less than 48 hours before. Which one is right?",options:["Refund in full every time (4.4)","Keep the fee within 48 hours (6.3)"],refs:[ref("4.4","Refunds",3),ref("6.3","Holidays and late cancels",4)],impact:"Changes 2 stories in Pay court fees"},{id:"q2",from:"analyst",kind:"gap",text:"Can parties also pay the court fee at the court desk, or only online?",options:["Online only","Online or at the court desk"],refs:[ref("4.2","Paying online",3)],impact:"Changes the whole Pay court fees epic"}]}var READ_DECIDED=[{text:"A held slot is freed after 10 minutes without a warning first. The BRD does not ask for one.",story:3},{text:"Messages go in the language the person chose on their profile, Arabic when none is set.",story:8}];function fromAnswer(q5){let a=q5.answer;if(!a)return null;let pick2=a.choice===SKIP_CHOICE?q5.options[0]:a.choice;return q5.id==="q1"?a.choice==="Other"?{text:`Refunds: ${a.value??""}`,story:12}:pick2===q5.options[1]?{text:"The fee is kept when a party cancels less than 48 hours before the hearing, as section 6.3 says.",story:12}:{text:"Every cancelled hearing is refunded in full, as section 4.4 says.",story:12}:q5.id==="q2"?a.choice==="Other"?{text:`Paying the fee: ${a.value??""}`,story:10}:pick2===q5.options[1]?{text:"Parties can also pay at the court desk: a clerk marks the fee as paid from the case page.",story:10}:{text:"Fees are paid online only, by card.",story:10}:null}function storyOf(p,key){return{key,title:p.title,story:`As a ${p.who}, I want ${p.want}, so that ${p.why}.`,criteria:p.done.map(([given,when2,then])=>({given,when:when2,then})),size:p.size,dependsOn:[],ref:p.ref}}function planKeys(b,first2){return new Map(PLAN.flatMap(e=>e.features.flatMap(f=>f.stories)).map((_,i)=>[i+1,`S-${first2+i}`]).filter(([,k])=>findItem(b,k)))}function addOutline(b){b.epics=PLAN.map(e=>({key:nextKey(b,"epic"),title:e.title,goal:e.goal,ref:e.ref,features:e.features.map(f=>({key:nextKey(b,"feature"),title:f.title,summary:f.summary,ref:f.ref,stories:[]})),stories:[]})),b.leftOut=LEFT_OUT2.map(l=>({...l}))}function addStories(b,index,first2){let epic=b.epics[index],plan=PLAN[index];if(!epic||!plan)return 0;let n=0;plan.features.forEach((fp,fi)=>{let f=epic.features[fi];if(f)for(let sp of fp.stories)f.stories.push(storyOf(sp,nextKey(b,"story"))),n+=1});let keys=planKeys(b,first2),flat=PLAN.flatMap(e=>e.features.flatMap(x=>x.stories));for(let[i,sp]of flat.entries()){let s=findItem(b,keys.get(i+1)??"")?.story;s&&(s.dependsOn=(sp.needs??[]).map(x=>keys.get(x)).filter(k=>!!k))}return n}function placeDecided(b,first2){let keys=planKeys(b,first2),all=[...READ_DECIDED,...b.questions.map(fromAnswer).filter(a=>!!a)];for(let d of b.decided){if(d.item)continue;let at3=all.find(r=>r.text===d.text)?.story,key=at3?keys.get(at3):void 0;key&&(d.item=key)}}function ago(minutes4){return new Date(Date.now()-minutes4*6e4).toISOString()}function clearDemoRequirements(bp){fs150.rmSync(requirementsDir(bp),{recursive:!0,force:!0})}function seedDemoRequirements(bp,user){clearDemoRequirements(bp);let id2="BR-1",dir=breakdownDir(bp,id2),file3="source/Case Portal BRD v1.docx";fs150.mkdirSync(path141.join(dir,"source"),{recursive:!0});let docx=demoBrdDocx();fs150.writeFileSync(path141.join(dir,file3),docx);let text3=prepareText(dir,{file:file3,kind:"docx"}),questions=demoQuestions();questions[0].answer={choice:questions[0].options[0]},questions[1].answer={choice:questions[1].options[1]};let b={id:id2,title:"Case Portal BRD v1",status:"ready",source:{file:file3,name:"Case Portal BRD v1.docx",kind:"docx",bytes:docx.length,pages:text3.parts,addedBy:user,addedAt:ago(95)},summary:SUMMARY,questions,epics:[],decided:[],leftOut:[],createdAt:ago(95),createdBy:user,owner:user,next:{epic:1,feature:1,story:1}};addOutline(b),PLAN.forEach((_,i)=>addStories(b,i,1));let at3=ago(88);for(let r of READ_DECIDED)b.decided.push({id:nextDecidedId(b.decided),text:r.text,by:"analyst",at:at3});for(let q5 of questions){let a=fromAnswer(q5);a&&b.decided.push({id:nextDecidedId(b.decided),text:a.text,by:"answer",at:ago(70)})}placeDecided(b,1),writeBreakdown(bp,b);let stories=allStories(b).length,state={activity:[{at:ago(66),who:"analyst",text:`The breakdown is ready for you: ${b.epics.length} epics, 7 features and ${stories} stories`},{at:ago(70),who:"you",text:"Answered 2 questions"},{at:ago(86),who:"analyst",text:"Has 2 questions for you"},{at:ago(86),who:"analyst",text:"Read the document: found 3 epics and 7 features, and decided 2 small things itself"},{at:ago(95),who:"squad",text:"Turned the Word file into text. Kept the headings and tables"},{at:ago(95),who:"you",text:"Added Case Portal BRD v1.docx"}],job:{step:"draft",status:"done",found:{epics:b.epics.length,features:7,stories}}};writeBreakdownState(bp,id2,state)}var queueing=new Set;function playDemoQueue(bp){queueing.has(bp.root)||(queueing.add(bp.root),fillBuildQueue(bp,{free:()=>1/0,pause:null,payment:async()=>"subscription"}).catch(()=>{}).finally(()=>queueing.delete(bp.root)))}var BEAT_MS=2500,beats=new Map;function beat(bp,id2,speed){let key=`${bp.root}:${id2}`,last=beats.get(key),at3=Date.now();return last===void 0?(beats.set(key,at3),!1):at3-last<BEAT_MS/Math.max(.1,speed)?!1:(beats.set(key,at3),!0)}function playDemoBreakdowns(bp,meta){for(let{id:id2,job}of breakdownJobs(bp,meta)){if(job.status==="stopping"){breakdownStopped(bp,id2);continue}if(!(job.heldFor==="long"||!beat(bp,id2,meta.speed)))try{playStep(bp,id2)}catch(err2){updateBreakdown(bp,id2,x=>{job.step==="reply"||job.step==="redo"||(x.status="failed",x.error=err2.message)}),updateBreakdownState(bp,id2,s=>{s.job&&Object.assign(s.job,{status:"failed",error:err2.message,now:void 0})})}}}function playStep(bp,id2){let state=readBreakdownState(bp,id2),job=state.job,b=readBreakdown(bp,id2);switch(job.status==="pending"&&updateBreakdownState(bp,id2,s=>Object.assign(s.job,{status:"running",startedAt:now()})),job.step){case"text":{let p=prepareText(breakdownDir(bp,id2),b.source);updateBreakdownState(bp,id2,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")});return}case"read":return playRead(bp,b,state);case"reply":return playReply(bp,b,job.question);case"draft":return playDraft(bp,b);case"redo":return playRedo(bp,b,job.item??"",job.ask??"")}}function playRead(bp,b,state){let of=b.source.kind==="pdf"?b.source.pages??1:Math.max(1,textParts(breakdownDir(bp,b.id)).length),unit=b.source.kind==="pdf"?"page":"part",step2=(state.job.found?.epics??0)+1;if(step2<=3){updateBreakdownState(bp,b.id,s=>Object.assign(s.job,{now:"Reading the document",at:{done:Math.max(1,Math.round(step2*of/3)),of,unit},found:{epics:step2,features:[2,5,7][step2-1],stories:0}}));return}let questions=demoQuestions();updateBreakdown(bp,b.id,x=>{x.summary=SUMMARY,x.questions=questions;for(let r of READ_DECIDED)x.decided.some(d=>d.text===r.text)||x.decided.push({id:nextDecidedId(x.decided),text:r.text,by:"analyst",at:now()});x.status="questions"}),updateBreakdownState(bp,b.id,s=>{s.job={step:"read",status:"done",found:{epics:3,features:7,stories:0},at:{done:of,of,unit}},addBreakdownActivity(s,"analyst","Read the document: found 3 epics and 7 features, and decided 2 small things itself"),addBreakdownActivity(s,"analyst","Has 2 questions for you")})}var REPLIES={q1:"Two sections of the BRD give different refund rules. Refunding every time is simpler for lawyers; keeping the fee within 48 hours protects the court from late cancels. The refund story is built the way you pick.",q2:"The BRD only names paying by card. If parties can pay at the desk too, clerks need a way to mark a fee as paid, which adds a small story to the fees epic."};function playReply(bp,b,qid){updateBreakdown(bp,b.id,x=>{let q5=x.questions.find(y=>y.id===qid);q5&&(q5.thread=[...q5.thread??[],{who:"analyst",text:REPLIES[q5.id]??"Pick the option closest to what you mean. I will write down what it means for the stories.",at:now()}],q5.thinking=!1)}),updateBreakdownState(bp,b.id,s=>Object.assign(s.job,{status:"done",now:void 0}))}function playDraft(bp,b){if(!b.epics.length){let after=updateBreakdown(bp,b.id,x=>{addOutline(x);for(let q5 of x.questions){let a=fromAnswer(q5);a&&x.decided.push({id:nextDecidedId(x.decided),text:a.text,by:q5.answer?.choice===SKIP_CHOICE?"analyst":"answer",at:now()})}});updateBreakdownState(bp,b.id,s=>{Object.assign(s.job,{now:`Writing the stories of ${after.epics[0]?.title} (epic 1 of ${after.epics.length})`,found:{epics:after.epics.length,features:7,stories:0}}),addBreakdownActivity(s,"analyst",`Wrote the outline: ${after.epics.length} epics and 7 features`)});return}let i=b.epics.findIndex(e=>!e.features.some(f=>f.stories.length)&&!e.stories.length);if(i>=0){let first2=b.next.story-allStories(b).length,made=0,after=updateBreakdown(bp,b.id,x=>{made=addStories(x,i,first2),placeDecided(x,first2)}),next=after.epics[i+1];updateBreakdownState(bp,b.id,s=>{Object.assign(s.job,{now:next?`Writing the stories of ${next.title} (epic ${i+2} of ${after.epics.length})`:"Checking the breakdown",found:{epics:after.epics.length,features:7,stories:allStories(after).length}}),addBreakdownActivity(s,"analyst",`Wrote ${made} stories for ${after.epics[i].title}`)});return}let stories=allStories(b).length;updateBreakdown(bp,b.id,x=>{x.status="ready",delete x.error}),updateBreakdownState(bp,b.id,s=>{s.job={step:"draft",status:"done",found:{epics:b.epics.length,features:7,stories}},addBreakdownActivity(s,"analyst",`The breakdown is ready for you: ${b.epics.length} epics, 7 features and ${stories} stories`)})}function playRedo(bp,b,item,ask2){let rewritten=[];updateBreakdown(bp,b.id,x=>{let before=structuredClone(x);if(item===x.id){x.summary=`${x.summary??""} ${ask2}`.trim();return}let f=findItem(x,item);f&&(f.level==="story"&&!f.story.ticket?f.story.criteria.push({given:"the story as you asked",when:"it is built",then:ask2.replace(/\.$/,"")}):f.level==="feature"?f.feature.summary=ask2:f.level==="epic"&&(f.item.goal=ask2),x.status==="approved"&&(rewritten=rewrittenStories(before,x)))}),updateBreakdownState(bp,b.id,s=>{s.job={step:"redo",status:"done",item,ask:ask2},rewritten.length&&(s.rewritten=[...new Set([...s.rewritten??[],...rewritten])]),addBreakdownActivity(s,"analyst",`Redid ${item===b.id?"the breakdown":item}`)})}function ago2(minutes4){return new Date(Date.now()-minutes4*6e4).toISOString()}function daysAgo(days){return new Date(Date.now()-days*864e5).toISOString()}function gitUserName(){try{return execSync3("git config user.name",{encoding:"utf8",stdio:["ignore","pipe","ignore"],env:gitEnv()}).trim().split(/\s+/)[0]||"You"}catch{return"You"}}function base(id2,title2,over){return{id:id2,title:title2,ask:title2,kind:"feature",source:{type:"manual"},priority:"normal",stage:"tickets",status:"",waiting:null,createdAt:ago2(600),createdBy:"Momen",builder:"Codex",reviewer:"Claude",activity:[],...over}}var log2=entries=>entries.map(([m,who4,text3])=>({at:ago2(m),who:who4,text:text3})),ran=entries=>entries.map(([who4,step2,from,to])=>({who:who4,step:step2,from:ago2(from),to:ago2(to)}));function demoTickets(user){let darkMode2=base("SK-148","Dark mode for the dashboard",{ask:"Staff on night shifts say the white screen hurts their eyes. Add a dark mode to the dashboard.",source:{type:"azure",ref:"AB#4120"},priority:"high",stage:"specs",status:"4 questions for you",createdBy:user,createdAt:ago2(9),activity:log2([[6,"analyst","Asked 4 questions"],[7,"analyst","Looking at how staff use the dashboard at night"],[8,"analyst","Reading the ticket and the screenshot"],[9,"you","Pulled AB#4120 from Azure DevOps"]])});return darkMode2.questions=scriptFor(darkMode2).questions.map(q5=>({...q5,options:[...q5.options]})),darkMode2.waiting={kind:"questions",summary:"4 questions from the Analyst and Architect"},[base("SK-139","Search by case number",{ask:"Clerks scroll long lists to find a case. Let them type the case number and jump to it.",source:{type:"azure",ref:"AB#4098"},stage:"review",status:"Proof ready to check",waiting:{kind:"proof",summary:"The proof is ready for you to check"},createdAt:ago2(46),quality:"best",questions:[{id:"q1",from:"analyst",text:"Should a part of a number find cases too, or only the full number?",options:["Only the full number","Part of a number too"],answer:{choice:"Part of a number too"}},{id:"q2",from:"architect",text:"Where should the search box go?",options:["Above the case list","In the top bar on every page"],answer:{choice:"Above the case list"}}],decision:{title:"Where the case search lives",options:[{name:"A search box above the case list",note:"One place, next to the cases it finds.",chosen:!0},{name:"A search box in the top bar",note:"On every page, but far from the list."}],why:"Clerks search while they look at the list, and Angular Material has the search field already."},follows:["ADR-011"],history:ran([["analyst","questions",44.6,44.1],["analyst","spec",39.8,39.65],["architect","design",35.8,35.1],["builder","build",29.5,14.5],["squad","checks",14.4,13.2],["tester","proof",13.1,11.9],["reviewer","review",5.3,3]]),story:"As a clerk, I want to search by case number, so I find a case in seconds.",criteria:[{given:"I'm on the case list",when:"I type a full case number",then:"that case opens at once"},{given:"I type part of a number",when:"I pause",then:"I see the matching cases"}],steps:[{title:"Search box above the case list",status:"done",minutes:3},{title:"Search by number in the API",status:"done",minutes:5},{title:"Show matches while typing",status:"done",minutes:4},{title:'Tests for every "done when"',status:"done",minutes:3}],gates:{checks:"pass",checksText:"18 of 18 checks pass",review:"pass",reviewBy:"Claude",proof:"pass"},specApproved:{by:"Momen",at:ago2(36)},stageTimes:{stage:"review",since:ago2(12),spent:{tickets:35,specs:250,implement:960,test:140}},proof:[{criterion:"Typing a full case number opens the case",before:"sk139-before.png",after:"sk139-after.png",beforeLabel:"Before",afterLabel:"After",note:"Case 2024/118 typed in full opens straight away, at 1440 by 900.",page:"/cases",shows:"met"},{criterion:"I see the matching cases",before:"sk139-before.png",after:"sk139-after.png",beforeLabel:"Before",afterLabel:"After",note:'Typing "2024/1" lists the three cases that start with it.',page:"/cases",more:[{after:"sk148-light.png",page:"/cases?q=1446",note:'Typing "1446" lists the cases of that year.'},{after:"sk148-dark-b.png",page:"/hearings?q=1446",note:"The same search on the hearings list shows the matching hearings."}],shows:"met"}],releaseNote:"**Search by case number.** Type a case number and jump straight to the case.",activity:log2([[3,"reviewer",'Approved: meets both "done when" points and follows ADR-011'],[5.3,"reviewer","Reading the changes (round 1 of 2, Claude)"],[11.9,"tester","Saved 1 screenshot"],[13.1,"tester","Taking screenshots after the change"],[13.2,"tester","All 18 checks pass"],[14.4,"tester","squad-kit is running the project checks"],[14.5,"builder","Finished all steps"],[29.5,"builder","Started building"],[30,"you","Approved the design"],[35.1,"architect","Wrote the design"],[36,"you","Approved the spec"],[39.65,"analyst","Wrote the spec"],[40,"you","Answered 2 questions"],[44.1,"analyst","Asked 2 questions"],[44.11,"analyst","Read the case list page, the case search in the API and saved decision ADR-011"],[45,"you","Pulled AB#4098 from Azure DevOps"]])}),base("SK-140","Date shows in the wrong format",{kind:"bug",ask:"On the hearing page the date shows as month/day/year.",stage:"bugs",priority:"high",severity:"stops-work",foundIn:"v2.3.1",status:"Found in v2.3.1",activity:log2([[50,"triage","Sorted the bug. Priority: High. Linked to v2.3.1"]])}),base("SK-143","Session timeout warning",{stage:"release",release:"v2.4.0",status:"Ready for v2.4.0",quality:"best",mergedAt:ago2(1200),story:"As a clerk, I want a warning before I am logged out, so I can save my work.",criteria:[{given:"I have been idle for 14 minutes",when:"one minute is left",then:"I see a warning with a Stay signed in button"}],history:ran([["analyst","questions",1440,1440-.6],["analyst","spec",23.5*60,23.5*60-.2],["architect","design",1380,1380-.8],["builder","build",22.5*60,22.5*60-11],["squad","checks",22.3*60,22.3*60-1.1],["tester","proof",22.2*60,22.2*60-1.4],["reviewer","review",1320,1320-2.2]]),activity:log2([[1200,"you","Checked the proof. Looks right"]]),gates:{checks:"pass",checksText:"16 of 16 checks pass",review:"pass",reviewBy:"Claude",proof:"pass"},releaseNote:"**Session timeout warning.** You get a warning one minute before you are logged out."}),base("SK-145","Faster login",{stage:"review",live:!0,status:"Reviewer is reading the login changes",squad:null,clock:{who:"reviewer",step:"review",since:ago2(1.2),limit:15}}),base("SK-146","Filter cases by court",{stage:"test",live:!0,progress:67,status:"Running checks for the court filter",clock:{who:"squad",step:"checks",since:ago2(.8),limit:15}}),base("SK-147","Export case lists to PDF",{stage:"release",release:"v2.4.0",status:"Ready for v2.4.0",quality:"best",mergedAt:ago2(1560),history:ran([["analyst","questions",1800,1800-.7],["analyst","spec",29.6*60,29.6*60-.3],["architect","design",29.2*60,29.2*60-1],["builder","build",28.8*60,28.8*60-18],["squad","checks",28.4*60,28.4*60-1.3],["builder","fix",28.3*60,28.3*60-4],["squad","checks",28.2*60,28.2*60-1.2],["tester","proof",28.1*60,28.1*60-1.6],["reviewer","review",27.9*60,27.9*60-2.4]]),activity:log2([[1560,"you","Checked the proof. Looks right"]]),gates:{checks:"pass",checksText:"16 of 16 checks pass",review:"pass",reviewBy:"Claude",proof:"pass"},releaseNote:"**Export to PDF.** Download any case list as a PDF, ready to print."}),base("SK-138","Fix: calendar week starts on Sunday",{kind:"bug",stage:"done",release:"v2.3.1",status:"Released in v2.3.1",quality:"best",createdAt:daysAgo(19),mergedAt:daysAgo(18),gates:{checks:"pass",checksText:"15 of 15 checks pass",review:"pass",reviewBy:"Claude",proof:"pass"},history:ran([["analyst","questions",19*1440-10,19*1440-10.5],["analyst","spec",19*1440-30,19*1440-30.2],["architect","design",19*1440-40,19*1440-40.7],["builder","build",19*1440-50,19*1440-58],["squad","checks",19*1440-59,19*1440-60],["tester","proof",19*1440-61,19*1440-62.2],["reviewer","review",19*1440-63,19*1440-65]])}),base("SK-141","Fix: logout button on small screens",{kind:"bug",stage:"done",track:"express",release:"v2.3.1",status:"Released in v2.3.1",quality:"best",createdAt:daysAgo(18),mergedAt:daysAgo(17.9),gates:{checks:"pass",checksText:"15 of 15 checks pass",review:"pass",reviewBy:"Claude",proof:"pass"},history:ran([["architect","design",18*1440-5,18*1440-5.4],["builder","build",18*1440-6,18*1440-9],["squad","checks",18*1440-9.2,18*1440-10.1],["tester","proof",18*1440-10.3,18*1440-11.2],["reviewer","review",18*1440-11.5,18*1440-12.6]])}),darkMode2,base("SK-149","Arabic names in PDF exports",{source:{type:"azure",ref:"AB#4126"},stage:"tickets",status:"From Azure, in queue"}),base("SK-150","Case notes autosave",{stage:"implement",status:"Stuck after 3 test runs",waiting:{kind:"stuck",summary:"The squad is stuck and needs one decision"},stuck:{gate:"checks",question:"The checks still fail after 3 tries: the tests show two clerks editing the same case notes overwrite each other.",detail:`test (npm test):
1855
+ `,"utf8")}catch{}}set(progress2){this.save({progress:{...progress2,at:this.stamp()}})}progress(){return this.saved.progress??{state:"idle"}}resume(){let p=this.progress(),current=this.installed().version;p.state==="restarting"?p.to&&isNewer(p.to,current)?this.set({...p,state:"failed",reason:`squad-kit restarted, but it still runs ${current}. The new version went somewhere else on this computer.`,permission:!1}):this.set({state:"done",from:p.from,to:current,carried:p.carried,waitedFor:p.waitedFor,now:p.now}):p.state==="waiting"?this.set({state:"idle"}):p.state==="installing"&&this.set({...p,state:"failed",reason:"squad-kit closed while it installed the update. Try again.",permission:!1})}watch(){checkForUpdates()&&this.check().catch(()=>{});let timer=setInterval(()=>{checkForUpdates()&&this.due()&&this.check().catch(()=>{})},HOUR);return timer.unref(),()=>clearInterval(timer)}due(){let at3=Date.parse(this.saved.checkedAt??"");return Number.isFinite(at3)?this.now()-at3>=(this.saved.checkError?HOUR:DAY2):!0}check(opts={}){return opts.fresh&&this.checking?this.checking.then(()=>this.check(opts)):(this.checking??=this.look(opts.fresh===!0).finally(()=>this.checking=null),this.checking)}async look(fresh){let installed3=this.installed(),latest;try{latest=(await fetchLatest(installed3.name,5e3,this.deps.fetch)).latest}catch{return this.save({checkedAt:this.stamp(),checkError:"Could not reach the npm registry. Check the internet connection."}),this.status()}this.save({checkedAt:this.stamp(),latest,checkError:void 0});let p=this.progress(),range=isNewer(latest,installed3.version)?{from:installed3.version,to:latest}:p.state==="done"&&p.from?{from:p.from,to:installed3.version}:null;if(range&&(fresh||this.notesStale(range))){let items3=await fetchReleasesBetween(installed3.repositoryUrl,range.from,range.to,{fetch:this.deps.fetch});this.save({releases:{...range,at:this.stamp(),items:items3}})}return this.status()}notesStale(range){let r=this.saved.releases;if(!r||r.from!==range.from||r.to!==range.to||!r.items.find(i=>i.version===range.to)?.lines.length)return!0;let at3=Date.parse(r.at);return!Number.isFinite(at3)||this.now()-at3>=DAY2}releasesFor(from,to){let r=this.saved.releases;return r&&r.to===to?r.items.filter(i=>(!from||isNewer(i.version,from))&&!isNewer(i.version,to)):this.saved.notes?.version===to?[this.saved.notes]:[]}status(){let installed3=this.installed(),latest=this.saved.latest??null,newer2=!!(latest&&isNewer(latest,installed3.version)),progress2=this.progress();progress2.state==="done"&&this.now()-Date.parse(progress2.at??"")>DAY2&&(progress2={state:"idle"});let about2=progress2.state==="done"?installed3.version:latest??installed3.version,releases=this.releasesFor(progress2.state==="done"?progress2.from:installed3.version,about2),notes=releases.find(r=>r.version===about2),withLines=releases.filter(r=>r.lines.length),target=progress2.to??(newer2&&latest?latest:"latest");return{current:installed3.version,latest,newer:newer2,major:!!(newer2&&latest&&isMajorBump(latest,installed3.version)),checkedAt:this.saved.checkedAt??null,...this.saved.checkError?{checkError:this.saved.checkError}:{},...notes?.lines.length?{notes:notes.lines}:{},...withLines.length?{releases:withLines}:{},notesUrl:notes?.url??changelogBlobUrl(installed3.repositoryUrl,about2),devInstall:installed3.isDevInstall,command:upgradePlan(installed3,target).text,daily:checkForUpdates(),later:this.saved.later??null,startedAt:this.startedAt,progress:progress2}}prefs(p){return typeof p.daily=="boolean"&&setCheckForUpdates(p.daily),p.later!==void 0&&this.save({later:p.later??void 0}),this.status()}async start(opts={}){let p=this.progress();if(p.state==="waiting"&&opts.now)return this.stopWaiting(),this.install(p.to,{waitedFor:p.waitedFor,now:!0}),this.status();if(p.state==="waiting"||p.state==="installing"||p.state==="restarting")return this.status();this.saved.latest||await this.check();let s=this.status();if(!s.latest||!s.newer)throw new Error(s.checkError??`You are on the latest version (${s.current}).`);if(s.devInstall)throw new Error(`This squad-kit runs from a source folder, so it does not update itself. Run: ${s.command}`);if(s.major)throw new Error(`squad-kit ${s.latest} is a big change. Read what is new first, then run: ${s.command}`);let to=s.latest;this.hold(!0);let busy=this.busy();return!busy.length||opts.now?(this.install(to,{waitedFor:[],now:!!(opts.now&&busy.length)}),this.status()):(this.set({state:"waiting",from:s.current,to,busy,waitedFor:busy}),this.waitTimer=setInterval(()=>{let still2=this.busy(),cur=this.progress();if(cur.state!=="waiting")return this.stopWaiting();if(still2.length){JSON.stringify(still2)!==JSON.stringify(cur.busy)&&this.set({...cur,busy:still2});return}this.stopWaiting(),this.install(to,{waitedFor:cur.waitedFor})},WAIT_POLL_MS),this.waitTimer.unref(),this.status())}stopWaiting(){this.waitTimer&&clearInterval(this.waitTimer),this.waitTimer=null}async install(to,how2){let installed3=this.installed(),plan=upgradePlan(installed3,to);this.set({state:"installing",from:installed3.version,to,...how2});let result;try{result=await(this.deps.install??((cmd15,args)=>runInstall(cmd15,args,{timeoutMs:INSTALL_TIMEOUT_MS})))(plan.cmd,plan.args)}catch(err2){result={ok:!1,code:null,output:"",timedOut:!1,startError:err2.message}}if(!result.ok){this.hold(!1);let why=whyInstallFailed(result,plan.cmd);this.set({state:"failed",from:installed3.version,to,...how2,...why,output:result.output.trim().slice(-4e3)});return}this.restartInto(to,how2)}restartInto(to,how2={}){this.set({state:"restarting",from:this.pkg.version,to,...how2,carried:this.carrying()}),(this.deps.restart??(()=>{}))()}restartNow(){let p=this.progress();if(p.state==="restarting")return this.status();if(p.state!=="failed"||!p.to)throw new Error("There is no update to restart for. Check for updates first.");return this.restartInto(p.to,{waitedFor:p.waitedFor}),this.status()}close(){let p=this.progress();if(p.state==="installing")throw new Error("The update is installing. It takes about a minute.");return p.state==="waiting"&&(this.stopWaiting(),this.hold(!1)),p.state!=="idle"&&this.set({state:"idle"}),this.status()}busy(){return this.deps.busy?.()??[]}carrying(){return this.deps.carrying?.()??[]}hold(on){(this.deps.hold??holdNewRuns)(on)}};function read6(){try{let raw=JSON.parse(fs147.readFileSync(file(),"utf8"));return raw&&typeof raw=="object"?raw:{}}catch{return{}}}function besideLabel(key){return key.startsWith("__chat:")?`Answering in the chat on ${key.slice(7)}`:key.startsWith("__change:")?`Writing the change for ${key.slice(9)}`:key.startsWith("__impact:")?`The Architect checks what ${key.slice(9)} changes`:key.startsWith("__tech:")?`The Architect writes the ${key.slice(7)} decision`:key==="__blueprint"?"The Architect reads the code for the architecture page":key==="__setup"?"squad-kit tries the project once: its tests and its app":key==="__talk"?"The Architect answers about the overview":key.startsWith("__breakdown:")?`The Analyst works on ${key.slice(12)}`:key.startsWith("__send:")?`Sending ${key.slice(7)} to the tracker`:"The Architect reads the project"}function label2(p){try{return boardExists(p.bp)&&readMeta(p.bp).project||projectName(p.root)}catch{return projectName(p.root)}}function runsNow(projects){let items3=[];for(let r of agentRuns()){let p=projects.all().find(x=>x.root===r.root),name=p?label2(p):projectName(r.root);for(let key of r.keys){if(key.startsWith("__")){let ticket=/^__(?:chat|change):(.+)$/.exec(key)?.[1];items3.push({project:name,...ticket?{ticket}:{},title:besideLabel(key)});continue}let title2=key;try{title2=p&&readTicket(p.bp,key)?.title||key}catch{}items3.push({project:name,ticket:key,title:title2})}}return items3}function ticketsInProgress(projects){let items3=[];for(let p of projects.all())try{if(!boardExists(p.bp)||readMeta(p.bp).engine!=="live")continue;let name=label2(p);for(let t of listTickets(p.bp))t.squad&&t.stage!=="done"&&items3.push({project:name,ticket:t.id,title:t.title})}catch{}return items3}import fs148 from"fs";import path139 from"path";import{fileURLToPath as fileURLToPath3}from"url";import{Readable as Readable2}from"stream";function pathnameForStatic(c){let rawUrl=c.env.incoming?.url;if(typeof rawUrl=="string"&&rawUrl.startsWith("/")){let pathOnly=rawUrl.split("?",1)[0]??rawUrl;try{return decodeURIComponent(pathOnly)}catch{return pathOnly}}try{return decodeURIComponent(new URL(c.req.url).pathname)}catch{return new URL(c.req.url).pathname}}function mountStaticAssets(app,_opts){let distDir=resolveConsoleUiDist();app.get("/logo.svg",c=>serveFile(c,distDir,"logo.svg","image/svg+xml")),app.get("/favicon.ico",c=>serveFile(c,distDir,"favicon.ico","image/x-icon")),app.get("*",c=>{let pathname=pathnameForStatic(c);if(pathname.startsWith("/assets/"))return assetResponse(c,distDir,pathname);if(c.req.path.startsWith("/api/")||c.req.path.startsWith("/healthz"))return c.notFound();if(!distDir)return c.text("UI not built. Run `pnpm -C console-ui build`.",404);let indexHtml=path139.join(distDir,"index.html");return fs148.existsSync(indexHtml)?c.html(fs148.readFileSync(indexHtml,"utf8"),200,{"cache-control":"no-store"}):c.text("UI not built",404)})}function assetResponse(c,distDir,pathname){if(!distDir)return c.text("UI not built",404);let rel=pathname.replace(/^\//,""),abs=path139.normalize(path139.join(distDir,rel));if(!isInside2(abs,distDir))return c.text("forbidden",403);if(!fs148.existsSync(abs)||!fs148.statSync(abs).isFile())return c.text("not found",404);let body10=Readable2.toWeb(fs148.createReadStream(abs));return new Response(body10,{headers:{"content-type":contentTypeFor(abs),"cache-control":"public, max-age=31536000, immutable"}})}function serveFile(c,distDir,name,ct){if(!distDir)return c.text("UI not built",404);let abs=path139.join(distDir,name);return fs148.existsSync(abs)?new Response(fs148.readFileSync(abs),{headers:{"content-type":ct,"cache-control":"public, max-age=86400"}}):c.text("not found",404)}function contentTypeFor(p){return p.endsWith(".js")?"application/javascript; charset=utf-8":p.endsWith(".css")?"text/css; charset=utf-8":p.endsWith(".html")?"text/html; charset=utf-8":p.endsWith(".svg")?"image/svg+xml":p.endsWith(".png")?"image/png":p.endsWith(".woff2")?"font/woff2":p.endsWith(".json")?"application/json":"application/octet-stream"}function isInside2(abs,parent){let a=path139.resolve(abs)+path139.sep,p=path139.resolve(parent)+path139.sep;return a.startsWith(p)}function resolveConsoleUiDist(){let fromEnv=process.env.SQUAD_CONSOLE_UI_DIST;if(fromEnv&&fs148.existsSync(path139.join(fromEnv,"index.html")))return fromEnv;let candidates=[fileURLToPath3(new URL("./console-ui/",import.meta.url)),fileURLToPath3(new URL("../../dist/console-ui/",import.meta.url)),fileURLToPath3(new URL("../console-ui/dist/",import.meta.url)),fileURLToPath3(new URL("../../console-ui/dist/",import.meta.url))];for(let c of candidates)if(fs148.existsSync(path139.join(c,"index.html")))return c;return null}function authMiddleware(expected){return async(c,next)=>{let presented=readPresentedToken(c.req.raw);if(!presented||!timingSafeEqualHex(presented,expected))return c.json({error:"unauthorized",detail:"missing or invalid session token"},401);await next()}}function readPresentedToken(req){let auth2=req.headers.get("authorization");return auth2&&auth2.toLowerCase().startsWith("bearer ")?auth2.slice(7).trim():req.method!=="GET"&&req.method!=="HEAD"?null:new URL(req.url).searchParams.get("t")}var OWN_NAMES=["127.0.0.1","localhost","[::1]"],READS2=new Set(["GET","HEAD","OPTIONS"]),NOT_OUR_ADDRESS=`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>squad-kit</title></head><body style="font:16px/1.5 system-ui,sans-serif;margin:3rem auto;max-width:32rem;padding:0 1rem"><h1 style="font-size:20px">This is not squad-kit's own address</h1><p>Open squad-kit from the link it shows when it starts, on this computer.</p></body></html>`;function ownAddressOnly(port){let hosts=new Set(OWN_NAMES.map(n=>`${n}:${port}`)),origins=new Set([...hosts].map(h=>`http://${h}`));return async(c,next)=>{if(!hosts.has((c.req.header("host")??"").toLowerCase()))return c.req.header("accept")?.includes("text/html")?c.html(NOT_OUR_ADDRESS,403):c.json({error:"forbidden",detail:"This is not squad-kit's own address."},403);let origin=c.req.header("origin");if(!READS2.has(c.req.method)&&origin!==void 0&&!origins.has(origin.toLowerCase()))return c.json({error:"forbidden",detail:"Changes come only from squad-kit's own pages."},403);await next()}}function timingSafeEqualHex(a,b){if(a.length!==b.length)return!1;let mismatch=0;for(let i=0;i<a.length;i+=1)mismatch|=a.charCodeAt(i)^b.charCodeAt(i);return mismatch===0}import fs165 from"fs";import path157 from"path";import{Hono}from"hono";import fs151 from"fs";import path142 from"path";import{execSync as execSync3}from"child_process";import{fileURLToPath as fileURLToPath5}from"url";function answerOf(t,id2){return t.questions?.find(q5=>q5.id===id2)?.answer}var darkMode={analystLines:["Reading the ticket and the screenshot","Looking at how staff use the dashboard at night"],questions:[{id:"q1",from:"analyst",text:"Who needs this most?",options:["Night shift staff","Managers","Everyone"]},{id:"q2",from:"analyst",text:"At first, should it follow the computer's theme?",options:["Yes","No, start light"]},{id:"q3",from:"architect",text:"Must you use a tech the client chose?",options:["No, you choose","Yes:"],inputOption:"Yes:"},{id:"q4",from:"architect",text:"Do staff share computers?",options:["Yes, shared desks","No, each has their own"]}],replies:{q1:"I ask so the switch sits where these people look first. Night shift staff live on the case list, so the switch would go in its top bar.",q2:"Some computers already switch to dark at night. With Yes, new users start with the same theme as their computer, and can still change it.",q3:"I mean a design library for buttons, menus and colors, like Angular Material. Angular itself is already saved as a project decision. If the client requires a library, I must use its dark theme.",q4:"If staff share desks, a choice saved in the browser is lost when they move. Then I would save the theme in each person's account instead."},planLines:[{who:"architect",text:"Reading the code for the theme"},{who:"architect",text:"Reading the code for accounts"},{who:"architect",text:"Checking Angular Material for dark theme support"},{who:"spec",text:"Writing the plan: 6 small steps and 9 tests"}],story(t){let who4=answerOf(t,"q1")?.choice;return`As a ${who4==="Managers"?"manager":who4==="Everyone"?"staff member":"night shift staff member"}, I want a dark mode, so my eyes don't get tired.`},criteria(t){let followsComputer=answerOf(t,"q2")?.choice!=="No, start light";return[{given:"I'm logged in",when:"I press the theme switch",then:"the dashboard turns dark"},{given:"I chose dark",when:"I log in on another computer",then:"it is still dark"},followsComputer?{given:"I'm new",when:"I log in",then:"the theme follows my computer"}:{given:"I'm new",when:"I log in",then:"the dashboard starts light"}]},decision(t){let shared=answerOf(t,"q4")?.choice!=="No, each has their own",client=answerOf(t,"q3"),lib=client?.choice==="Yes:"?client.value?.trim():void 0;return{title:shared?"Save each person's theme in their account":"Save the theme in the browser",options:[{name:"In the browser",note:"Simple, but lost when staff change desks.",chosen:!shared},{name:"In the account",note:"Follows the person to any computer.",chosen:shared}],why:shared?"Staff share desks, from your answer to question 4.":"Each person has their own computer, from your answer to question 4, so the simple way is enough.",clientCheck:lib?/material/i.test(lib)?`${lib} supports dark themes, so there is no conflict.`:`${lib} will be checked for dark theme support in step 3.`:"No client choice for this. The squad uses the existing styles."}},steps:[{title:"Add a theme setting to each account",live:["Reading the code for accounts","Writing the code for saved settings","Adding a theme column to the database"]},{title:"Theme switch in the header",live:["Writing the code for the header","Placing the switch next to the user menu"]},{title:"Dark colors for every page",live:["Writing the dark colors","Checking 14 pages for fixed colors"]},{title:"Save the choice in the account",live:["Writing the code that saves the choice","Writing the code for the theme"]},{title:"Follow the computer theme for new users",live:["Reading the browser theme setting","Writing the code for the theme"]},{title:'Tests for every "done when"',live:["Writing the test for the saved theme","Writing 9 new tests"]}],tests:21,reviewFix:{ask:"Asked for 1 fix: the page flashes white for a moment on login",fixed:"Fixed: the theme now loads before the page shows"},proof:[{criterion:"The switch turns the dashboard dark",before:"sk148-light.png",after:"sk148-dark.png",beforeLabel:"Before",afterLabel:"After",note:"Recorded by the Tester in Chrome at 1440 by 900",shows:"met"},{criterion:"Still dark on another computer",before:"sk148-dark.png",after:"sk148-dark-b.png",beforeLabel:"Computer A",afterLabel:"Computer B",note:"Same account, logged in on a second browser",shows:"met"},{criterion:"New users follow their computer theme",before:"sk148-light.png",after:"sk148-dark.png",beforeLabel:"Computer set to light",afterLabel:"Computer set to dark",note:"A new account on two computers with different settings",shows:"met"}],releaseNote:()=>"**Dark mode.** Switch the dashboard to dark. Your choice follows you to any computer."};function genericScript(t){let what=t.title.charAt(0).toLowerCase()+t.title.slice(1),isBug=t.kind==="bug";return{analystLines:isBug?["Reading the bug report","Following the steps to see it happen"]:["Reading the ticket","Looking at similar past tickets"],questions:isBug?[]:[{id:"q1",from:"analyst",text:"Who needs this most?",options:["Staff who use it daily","Managers","Everyone"]},{id:"q2",from:"analyst",text:"How soon is it needed?",options:["This week","This month","No rush"]},{id:"q3",from:"architect",text:"Must you use a tech the client chose for this?",options:["No, you choose","Yes:"],inputOption:"Yes:"}],replies:{q1:"I ask so the plan puts this where those people already work.",q2:"If it is urgent, I keep the first version small and plan the rest as a follow-up ticket.",q3:"I mean a library or tool the client requires, like a design library. If there is none, I follow the project decisions already saved."},planLines:[{who:"architect",text:"Reading the screens this touches"},{who:"architect",text:"Checking the saved project decisions"},{who:"spec",text:"Writing the plan in small steps"}],story:()=>isBug?`Fix: ${what}.`:`As a ${answerOf(t,"q1")?.choice==="Managers"?"manager":"staff member"}, I want ${what}, so my work is easier.`,criteria:()=>isBug?[{given:"the steps in the bug report",when:"I follow them",then:"the problem no longer happens"},{given:"the fix is in",when:"the checks run",then:"a new test guards this bug"}]:[{given:"I'm logged in",when:`I use ${what}`,then:"it works as the ticket describes"},{given:"something goes wrong",when:`I use ${what}`,then:"I see a clear message"}],decision:()=>isBug?void 0:{title:"Build it inside the existing screens",options:[{name:"A new page",note:"More work, and staff must learn where it is."},{name:"Inside the existing screens",note:"Staff find it where they already work.",chosen:!0}],why:"Staff should not have to learn a new place.",clientCheck:"Follows the project decisions already saved."},steps:isBug?[{title:"Reproduce the bug in a test",live:["Writing a test that shows the bug","The test fails, as expected"]},{title:"Fix the cause",live:["Reading the code behind it","Editing the fix"]},{title:"Check nothing else broke",live:["Running the related checks"]}]:[{title:"Read the related code",live:["Reading the screens this touches"]},{title:`Build ${what}`,live:["Editing the screen","Editing the API"]},{title:"Connect it to the existing screens",live:["Editing the navigation"]},{title:'Tests for every "done when"',live:["Writing the tests"]}],tests:isBug?12:16,proof:[],releaseNote:()=>isBug?`**Fixed:** ${what}.`:`**${t.title}.**`}}var SCRIPTS={"SK-148":darkMode};function scriptFor(t){return SCRIPTS[t.id]??genericScript(t)}var AMBIENT={"SK-151":["Analyst is reading the ticket","Analyst is checking the hearing calendar"],"SK-153":["Fixing a failed test","Running the tests"],"SK-146":["Running checks for the court filter","Checking filters on 3 screen sizes"],"SK-145":["Reviewer is reading the login changes","Reviewer is checking the session handling"]};var SAMPLE={questions:{input:182e3,cached:151e3,output:3100,usd:.46},spec:{input:96e3,cached:88e3,output:2400,usd:.21},design:{input:248e3,cached:205e3,output:6200,usd:.61},build:{input:142e4,cached:129e4,output:21e3,usd:1.34},fix:{input:38e4,cached:34e4,output:5800,usd:.41},review:{input:41e4,cached:352e3,output:3900,usd:.47},proof:{input:133e3,cached:101e3,output:2200,usd:.36}},PRICE={strongest:1,balanced:.6,fast:.08};function sampleUsage(bp,t,who4,step2,quality,scale=1){let s=SAMPLE[step2];if(!s)return null;let level=standardLevel({role:who4,step:step2,quality,express:t.track==="express"}).level??"balanced",tool5=aiTool(readAgent(bp,who4).tool),usage={inputTokens:Math.round(s.input*scale),cachedTokens:Math.round(s.cached*scale),outputTokens:Math.round(s.output*scale),costUsd:Math.round(s.usd*PRICE[level]*scale*100)/100};return{key:{role:who4,step:step2,level,model:tool5?.modelFor(level)??""},usage}}function countSampleStep(bp,t,who4,step2,quality,at3=Date.now(),size2=1){if(who4==="squad")return;let scale=size2*(.8+(Number(t.id.replace(/\D/g,""))*7+(t.history?.length??0)*13)%9/20),run7=sampleUsage(bp,t,who4,step2,quality??DEFAULT_QUALITY,scale);run7&&(t.usage=addRun(t.usage,run7.key,run7.usage),t.quality=quality??DEFAULT_QUALITY,addToMonth(bp,run7.usage,at3))}function countSampleHistory(bp,t,size2=1){let runs2=t.history??[],quality=t.quality;t.history=[];for(let h of runs2)countSampleStep(bp,t,h.who,h.step,quality,Date.parse(h.to),size2),t.history.push(h)}import fs149 from"fs";import path140 from"path";import{fileURLToPath as fileURLToPath4}from"url";var DEMO_SUMMARY="A web portal where court clerks and judges manage cases, hearings and documents.";function demoOverview(readAt,approved){return{users:"Court clerks, judges and team leads. They sign in with their ministry account, and each role sees its own screens.",can:["Find a case by its number or by a party name","Plan hearings on a calendar, in day/month/year","Upload case documents up to 20 MB","Export any list page to PDF","Switch between Arabic and English, right to left"],worth:["The session ends after 20 minutes with no warning, so a clerk can lose a half-filled form.","Big case lists need paging, or the page freezes.","No test opens the hearing calendar in a real browser yet."],ideas:[{title:"Warn before the session ends",why:"Clerks lose what they typed when the session ends with no warning."},{title:"Bulk assign cases to a clerk",why:"Team leads assign cases one by one today."},{title:"Add browser tests for the hearing calendar",why:"Nothing checks the calendar in a real browser yet."}],parts:[{id:"people",name:"Clerks and judges",what:"In a web browser",files:[],kind:"people"},{id:"pages",name:"Pages",what:"Case list, hearings, documents",files:["web/src/app/"],kind:"screens"},{id:"api",name:"Web API",what:"Cases, hearings and uploads",files:["api/Controllers/"],kind:"server"},{id:"rules",name:"Case rules",what:"Filing and hearing rules in one place",files:["api/Domain/"],kind:"logic"},{id:"db",name:"Database",what:"SQL Server, with migrations",files:["api/Migrations/"],kind:"data"},{id:"signin",name:"Ministry sign-in",what:"Who may open which screens",files:["api/Auth/"],kind:"outside"},{id:"tests",name:"Tests \xB7 18",what:"Jest for the logic, Playwright for the screens",files:["web/src/app/","e2e/"],kind:"tests"}],links:[{from:"people",to:"pages",label:"uses"},{from:"pages",to:"api",label:"calls"},{from:"api",to:"rules",label:"follows"},{from:"rules",to:"db",label:"stores in"},{from:"api",to:"signin",label:"signs in with"},{from:"tests",to:"pages",label:"checks"},{from:"tests",to:"rules",label:"checks"}],readAt,...approved?{approved}:{}}}var FOUND=[{title:"The front end is Angular 17",why:"Every screen in web/ is an Angular component."},{title:"The back end is a .NET 8 API",why:"api/ is one ASP.NET Core project."},{title:"SQL Server, changed only through migrations",why:"Every table change is a migration in api/Migrations/."},{title:"Angular Material for buttons and menus",why:"The client asks for it across all ministry portals."},{title:"Jest for logic, Playwright for screens",why:"Both run in the checks today."}],READS3=["Reading README.md","Reading web/package.json","Looking for *.component.ts","Reading web/src/app/cases/case-list.component.ts","Reading web/src/app/hearings/calendar.component.ts","Reading api/Controllers/CasesController.cs","Reading api/Domain/HearingRules.cs",'Searching for "Authorize"',"Reading api/Migrations/","Reading e2e/cases.spec.ts"],DEMO_FILES=214,PACE={files:1,tool:2,read:26,commands:7,tests:6,app:6,overview:1};function demoShotSource(){for(let rel of["../templates/demo/proof/sk139-before.png","../../templates/demo/proof/sk139-before.png"]){let p=fileURLToPath4(new URL(rel,import.meta.url));if(fs149.existsSync(p))return p}return null}function copyShot(bp){let src=demoShotSource();if(src)return fs149.mkdirSync(bp.proofDir,{recursive:!0}),fs149.copyFileSync(src,path140.join(bp.proofDir,"setup-app.png")),"setup-app.png"}function seedDoneSetup(bp,user,readAt){let start=Date.parse(readAt)-6e4,s=newSetup("repo",start,start+1e3),t=start+1e3;for(let id2 of["tool","read","commands","tests","app","overview"])begin(s,id2,void 0,t),t+=PACE[id2]*1e3*(id2==="read"?6:3),finish(s,id2,"done",{note:doneNote(id2)},t);s.proof=proofOf(copyShot(bp)),s.readyAt=new Date(t).toISOString(),s.approvedAt=new Date(t+12e4).toISOString(),s.qualityAt=s.approvedAt;let meta=readMeta(bp);writeMeta(bp,{...meta,scan:{status:"done",at:readAt,summary:DEMO_SUMMARY},setup:s});let o=demoOverview(readAt,{by:user,at:s.approvedAt});writeOverview(bp,{...o,ideas:o.ideas.map((i,n)=>n===0?{...i,ticket:"SK-143"}:i)})}function seedFreshSetup(bp){let meta=readMeta(bp);writeMeta(bp,{...meta,map:[],scan:{status:"pending"},setup:newSetup("repo",Date.now()-1200)}),fs149.rmSync(path140.join(bp.knowledgeDir,"overview.md"),{force:!0})}function doneNote(id2){switch(id2){case"tool":return"The squad works with it";case"read":return`Read the project: ${DEMO_FILES} files`;case"commands":return"Found how to run the tests and start the app";case"tests":return"Ran your tests: 18 pass";case"app":return"Opened the app";case"overview":return"What squad-kit understood, for you to check";default:return"A .squad folder in your project. Your code is not changed."}}function proofOf(shot){return{files:DEMO_FILES,read:DEMO_FILES,branch:"main",unsaved:0,tests:[{ok:!0,command:"npm test",passed:18,seconds:41}],app:{ok:!0,url:"http://localhost:4200",command:"npm start",...shot?{shot}:{}}}}function readDone(bp,meta){let at3=now();meta.scan={status:"done",at:at3,summary:DEMO_SUMMARY},meta.map=[{area:"Front end",detail:"Angular 17 with Angular Material, in web/"},{area:"Back end",detail:".NET 8 API, in api/"},{area:"Database",detail:"SQL Server, migrations in api/Migrations/"},{area:"Tests",detail:"Jest for logic, Playwright for screens"}],meta.commands={setup:"npm install",test:"npm test",start:"npm start",url:"http://localhost:4200",...meta.commands};let before=readOverview(bp);writeOverview(bp,{...demoOverview(at3),...before.ideas.some(i=>i.ticket)?{ideas:before.ideas}:{}});let known=listDecisions(bp);for(let f of FOUND){if(known.some(d2=>d2.title===f.title))continue;let d={id:nextDecisionId(bp),title:f.title,why:f.why,options:[f.title],chosen:f.title,from:"Project scan",approvedBy:"",proposed:!0,date:at3,checkedBy:null,tickets:[]};writeDecision(bp,d)}}var START_NOTE={read:"Starting\u2026",commands:"Installing what it needs",tests:"Running the tests",app:"Starting the app"};function playDemoSetup(bp,meta,at3=Date.now()){playDemoTalk(bp,meta,at3);let s=meta.setup;if(!s||s.approvedAt)return;let cur=currentStep(s);if(!cur||cur.state==="stopped"||cur.state==="failed")return;if(s.stopping){cur.state==="now"?finish(s,cur.id,"stopped",{note:"You stopped it."},at3):Object.assign(cur,{state:"stopped",note:"You stopped it."}),delete s.stopping,cur.id==="read"&&(meta.scan={status:"failed",at:now(),error:"You stopped it. Press Start again when you are ready."}),writeMeta(bp,meta);return}if(cur.state==="todo"){cur.id==="read"&&(meta.scan={status:"running",at:now()},s.proof={files:DEMO_FILES,read:0,branch:"main",unsaved:0},s.reads=[]),begin(s,cur.id,START_NOTE[cur.id],at3),writeMeta(bp,meta);return}let took=(at3-Date.parse(cur.since??new Date(at3).toISOString()))/1e3,need=PACE[cur.id]/(meta.speed||1);if(took<need){if(cur.id==="read"){let i=Math.min(READS3.length-1,Math.floor(took/need*READS3.length)),opened=Math.round(took/need*DEMO_FILES*.6);cur.note=`Reading the code \xB7 ${opened} files`,s.reads?.at(-1)!==READS3[i]&&(s.reads=[...s.reads??[],READS3[i]].slice(-8)),s.proof={...s.proof,read:opened},meta.scan={...meta.scan,status:"running",now:"Reading the code"},writeMeta(bp,meta)}else cur.id==="app"&&took>=need/2&&cur.note?.startsWith("Starting")&&(cur.note="Taking a screenshot",writeMeta(bp,meta));return}cur.id==="read"&&(readDone(bp,meta),s.proof={...s.proof,read:DEMO_FILES}),cur.id==="tests"&&(s.proof={...s.proof,tests:proofOf(void 0).tests}),cur.id==="app"&&(s.proof={...s.proof,app:proofOf(copyShot(bp)).app}),finish(s,cur.id,"done",{note:doneNote(cur.id)},at3),cur.id==="overview"&&(s.readyAt=new Date(at3).toISOString()),writeMeta(bp,meta)}function playDemoTalk(bp,meta,at3){let t=readTalk(bp);if(!t?.open)return;if(t.stopping){writeTalk(bp,{...t,thinking:!1,stopping:void 0,messages:t.thinking?[...t.messages,{who:"squad",text:"You stopped this answer. Ask again, or carry on.",at:now(),note:"stopped"}]:t.messages});return}if(!t.thinking)return;let last=t.messages.at(-1);if(last&&at3-Date.parse(last.at)<2200/(meta.speed||1)||!last&&at3-Date.parse(meta.setup?.readyAt??new Date(0).toISOString())<0)return;let said=t.messages.filter(m=>m.who==="you").length,text3=said===0?"Thanks for checking. What did I get wrong: who uses it, what it does, the picture of its parts, or one of the rules?":said===1?"I see. I will read the project again with that in mind and change the overview and the rules to match. Anything else before I do?":"Got it. Press **Read it again with this** and I will read the project again with what you said.";writeTalk(bp,{...t,thinking:!1,messages:[...t.messages,{who:"architect",text:text3,at:now(),...said===0?{options:["Who uses it","What it can do","One of the rules"]}:{}}]})}import fs150 from"fs";import path141 from"path";var BRD=[{title:"Case Portal BRD v1"},{p:"Business requirements for the commercial court's online case portal. Version 1, from the court's business team."},{h1:"1 Purpose"},{p:"Lawyers and parties book hearings, pay court fees and follow their cases online, in **Arabic and English**. Court staff manage hearing days and see reports. The portal runs on the court's Angular front end and .NET services."},{h1:"2 Users"},{bullets:["Lawyer: books hearings and pays fees for their clients.","Party: follows their own case and pays its fees.","Clerk: manages court rooms and hearing days.","Court admin: sees the reports and the audit log."]},{h1:"3 Book a hearing"},{h2:"3.1 Find a free slot"},{p:"A lawyer picks a court and a date range and sees the free hearing slots."},{h2:"3.2 Judge calendar"},{p:"A calendar shows the days each judge sits, with the free slots marked."},{h2:"3.3 Holding a slot"},{p:"While the lawyer fills in the booking, the slot is held for **10 minutes** so nobody else takes it."},{h2:"3.4 Confirm a booking"},{p:"The booking is confirmed once the fee is paid, or straight away for legal aid cases."},{h2:"3.5 Cancel or move a hearing"},{p:"A lawyer can cancel a hearing, or move it to another free slot, up to 48 hours before it."},{h1:"4 Court fees"},{h2:"4.1 Fees by case type"},{table:[["Case type","Fee (SAR)"],["Commercial claim","500"],["Appeal","750"],["Urgent order","300"]]},{h2:"4.2 Paying online"},{p:"Fees are paid by card in the portal."},{h2:"4.3 Receipts"},{p:"Each payment gets a receipt as a PDF, in Arabic and English."},{h2:"4.4 Refunds"},{p:"When a hearing is cancelled, the fee is refunded in full to the card that paid."},{h2:"4.5 Legal aid"},{p:"Cases with legal aid pay no fee. A clerk marks a case as legal aid."},{h1:"5 Notifications"},{numbers:["A confirmation by email and SMS when a hearing is booked.","A reminder one day before the hearing.","Every message in the language the person chose, Arabic or English."]},{h1:"6 Courts and hearing days"},{h2:"6.1 Court rooms"},{p:"Clerks add and edit court rooms, with their size and equipment."},{h2:"6.2 Hearing days"},{p:"Clerks open and close hearing days for each judge."},{h2:"6.3 Holidays and late cancels"},{p:"A clerk blocks the slots of public holidays. The fee is kept when a party cancels less than 48 hours before the hearing."},{h1:"7 Reports"},{bullets:["Hearings per court each week.","Fees paid and refunded each month.","Every report can be exported to Excel.","An audit log shows who booked, moved or cancelled a hearing."]},{h1:"8 Later phases"},{p:"A mobile app for parties comes in a later phase. It is not part of this version."}],esc=s=>s.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");function runs(text3){return text3.split(/(\*\*[^*]+\*\*)/).filter(Boolean).map(t=>t.startsWith("**")?`<w:r><w:rPr><w:b/></w:rPr><w:t xml:space="preserve">${esc(t.slice(2,-2))}</w:t></w:r>`:`<w:r><w:t xml:space="preserve">${esc(t)}</w:t></w:r>`).join("")}var para=(text3,style,num4)=>`<w:p>${style||num4?`<w:pPr>${style?`<w:pStyle w:val="${style}"/>`:""}${num4?`<w:numPr><w:ilvl w:val="0"/><w:numId w:val="${num4}"/></w:numPr>`:""}</w:pPr>`:""}${runs(text3)}</w:p>`;function blockXml(b){return"title"in b?para(b.title,"Title"):"h1"in b?para(b.h1,"Heading1"):"h2"in b?para(b.h2,"Heading2"):"p"in b?para(b.p):"bullets"in b?b.bullets.map(x=>para(x,void 0,1)).join(""):"numbers"in b?b.numbers.map(x=>para(x,void 0,2)).join(""):`<w:tbl>${b.table.map(row3=>`<w:tr>${row3.map(c=>`<w:tc>${para(c)}</w:tc>`).join("")}</w:tr>`).join("")}</w:tbl>`}var W='xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"';function demoBrdDocx(){let style=(id2,name)=>`<w:style w:type="paragraph" w:styleId="${id2}"><w:name w:val="${name}"/></w:style>`,level=fmt=>`<w:lvl w:ilvl="0"><w:numFmt w:val="${fmt}"/></w:lvl>`;return zip([{name:"[Content_Types].xml",data:'<?xml version="1.0" encoding="UTF-8"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/><Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/><Override PartName="/word/numbering.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"/></Types>'},{name:"_rels/.rels",data:'<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>'},{name:"word/_rels/document.xml.rels",data:'<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering" Target="numbering.xml"/></Relationships>'},{name:"word/styles.xml",data:`<?xml version="1.0" encoding="UTF-8"?><w:styles ${W}>${style("Title","Title")}${style("Heading1","heading 1")}${style("Heading2","heading 2")}</w:styles>`},{name:"word/numbering.xml",data:`<?xml version="1.0" encoding="UTF-8"?><w:numbering ${W}><w:abstractNum w:abstractNumId="0">${level("bullet")}</w:abstractNum><w:abstractNum w:abstractNumId="1">${level("decimal")}</w:abstractNum><w:num w:numId="1"><w:abstractNumId w:val="0"/></w:num><w:num w:numId="2"><w:abstractNumId w:val="1"/></w:num></w:numbering>`},{name:"word/document.xml",data:`<?xml version="1.0" encoding="UTF-8"?><w:document ${W}><w:body>${BRD.map(blockXml).join("")}</w:body></w:document>`}])}var ref=(section,title2,page)=>({section,title:title2,page}),PLAN=[{title:"Book a hearing",goal:"Lawyers book a hearing slot online instead of at the court desk.",ref:ref("3","Book a hearing",1),features:[{title:"Find a free slot",summary:"Lawyers see the free hearing slots of a court.",ref:ref("3.1","Find a free slot",1),stories:[{title:"Search free hearing slots",who:"lawyer",want:"to search the free hearing slots of a court by date",why:"I can pick one that suits my client",done:[["I'm signed in as a lawyer","I pick a court and a date range","I see the free slots, earliest first"],["no slot is free in that range","I search",'I see "No free slots" and the next free day']],size:"M",ref:ref("3.1","Find a free slot",1)},{title:"See a judge's free days",who:"lawyer",want:"a calendar of the days each judge sits",why:"I can plan around them",done:[["a court with three judges","I open the calendar","I see each judge's days with the free slots marked"]],size:"S",needs:[1],ref:ref("3.2","Judge calendar",2)}]},{title:"Book and confirm",summary:"A slot is held while the lawyer books, then confirmed.",ref:ref("3.3","Holding a slot",2),stories:[{title:"Hold a slot for 10 minutes",who:"lawyer",want:"my chosen slot held while I fill in the booking",why:"nobody else takes it meanwhile",done:[["a free slot","I pick it","it shows as held for me, with 10 minutes left"],["a slot held by someone else","I look at it","it shows as taken"],["10 minutes pass without booking","the time ends","the slot is free again"]],size:"M",needs:[1],ref:ref("3.3","Holding a slot",2)},{title:"Confirm a hearing booking",who:"lawyer",want:"my booking confirmed once the fee is paid",why:"the hearing is fixed in the court calendar",done:[["a held slot and a paid fee","I confirm","the hearing shows in the case and the calendar"],["a legal aid case","I book","it is confirmed with no fee"]],size:"M",needs:[3],ref:ref("3.4","Confirm a booking",2)},{title:"Cancel or move a hearing",who:"lawyer",want:"to cancel a hearing or move it to another free slot",why:"plans can change",done:[["a hearing more than 48 hours away","I move it to a free slot","the old slot is free and the new one is mine"],["a hearing less than 48 hours away","I try to cancel it","I see that it is too late to change online"]],size:"M",needs:[4],ref:ref("3.5","Cancel or move a hearing",2)}]},{title:"Notifications",summary:"People hear about their hearings by email and SMS.",ref:ref("5","Notifications",3),stories:[{title:"Email and SMS when a hearing is booked",who:"party",want:"an email and an SMS when my hearing is booked",why:"I know the date without asking my lawyer",done:[["a hearing is confirmed","the booking ends","the lawyer and the party get an email and an SMS with the date and court"]],size:"S",needs:[4],ref:ref("5","Notifications",3)},{title:"Reminder one day before the hearing",who:"party",want:"a reminder the day before my hearing",why:"I do not miss it",done:[["a hearing tomorrow","the reminders go out at 9:00","the lawyer and the party get a reminder"]],size:"S",needs:[4],ref:ref("5","Notifications",3)},{title:"Messages in Arabic and English",who:"party",want:"every message in the language I chose",why:"I understand it",done:[["my language is Arabic","a message is sent to me","it is in Arabic, right to left"],["my language is English","a message is sent to me","it is in English"]],size:"S",needs:[6],ref:ref("5","Notifications",3)}]}]},{title:"Pay court fees",goal:"Parties and lawyers pay court fees online and get a receipt.",ref:ref("4","Court fees",2),features:[{title:"Pay online",summary:"The fee of a case, paid by card, with a receipt.",ref:ref("4.2","Paying online",3),stories:[{title:"Show the fee for a case type",who:"lawyer",want:"to see the fee of a case before I book",why:"I can tell my client what it costs",done:[["a commercial claim","I start a booking","I see the fee: 500 SAR"]],size:"S",ref:ref("4.1","Fees by case type",2)},{title:"Pay the fee by card",who:"lawyer",want:"to pay the fee by card in the portal",why:"the hearing is confirmed at once",done:[["a held slot and its fee","I pay by card","the fee shows as paid and the booking is confirmed"],["the card is declined","I pay","I see why and the slot stays held"]],size:"L",needs:[9,4],ref:ref("4.2","Paying online",3)},{title:"Receipt as a PDF in two languages",who:"party",want:"a receipt for every payment",why:"I have proof that I paid",done:[["a paid fee","I open the case","I can download the receipt as a PDF in Arabic and English"]],size:"M",needs:[10],ref:ref("4.3","Receipts",3)}]},{title:"Refunds and legal aid",summary:"Money back when a hearing is cancelled, and no fee with legal aid.",ref:ref("4.4","Refunds",3),stories:[{title:"Refund the fee when a hearing is cancelled",who:"lawyer",want:"the fee refunded when my hearing is cancelled",why:"my client does not pay for a hearing that did not happen",done:[["a paid hearing","it is cancelled","the fee goes back to the card that paid"],["a refund was sent","I open the case","I see the refund and its date"]],size:"M",needs:[10,5],ref:ref("4.4","Refunds",3)},{title:"No fee for legal aid cases",who:"clerk",want:"to mark a case as legal aid",why:"its party pays no fee",done:[["a case","I mark it as legal aid","its bookings show no fee"]],size:"M",needs:[9],ref:ref("4.5","Legal aid",3)}]}]},{title:"Admin and reports",goal:"Court staff manage hearing days and see how the courts are used.",ref:ref("6","Courts and hearing days",4),features:[{title:"Courts and hearing days",summary:"Clerks keep the court rooms and hearing days up to date.",ref:ref("6.1","Court rooms",4),stories:[{title:"Add and edit court rooms",who:"clerk",want:"to add and edit court rooms",why:"bookings go to rooms that exist",done:[["I am a clerk","I add a room with its size and equipment","it shows in the list of rooms"]],size:"S",ref:ref("6.1","Court rooms",4)},{title:"Open and close hearing days",who:"clerk",want:"to open and close a judge's hearing days",why:"only real days can be booked",done:[["a judge","I open next Monday for hearings","its slots can be booked"],["a day with bookings","I try to close it","I see the bookings to move first"]],size:"M",needs:[14],ref:ref("6.2","Hearing days",4)},{title:"Block slots on public holidays",who:"clerk",want:"to block the slots of a public holiday",why:"nobody books a day the court is closed",done:[["a public holiday","I block it","none of its slots can be booked"]],size:"S",needs:[15],ref:ref("6.3","Holidays and late cancels",4)}]},{title:"Reports",summary:"How the courts are used, and who changed what.",ref:ref("7","Reports",4),stories:[{title:"Hearings per court each week",who:"court admin",want:"a weekly report of hearings per court",why:"I see which courts are busy",done:[["last week had hearings","I open the report","I see the hearings of each court, day by day"]],size:"M",needs:[4],ref:ref("7","Reports",4)},{title:"Fees report for each month",who:"court admin",want:"a monthly report of the fees paid and paid back",why:"the finance team can check them",done:[["a month with payments and money paid back","I open the report","I see the totals paid and paid back per case type"]],size:"M",needs:[10,12],ref:ref("7","Reports",4)},{title:"Export a report to Excel",who:"court admin",want:"to export any report to Excel",why:"I can share it",done:[["a report on screen","I press Export","I get an Excel file with the same rows"]],size:"S",needs:[17],ref:ref("7","Reports",4)},{title:"Audit log of hearing changes",who:"court admin",want:"a log of who booked, moved or cancelled a hearing",why:"I can answer complaints",done:[["a hearing was moved","I open the audit log","I see who moved it, when, and from which slot"]],size:"M",needs:[5],ref:ref("7","Reports",4)}]}]}],LEFT_OUT2=[{text:"A mobile app for parties",why:"The BRD says it comes in a later phase.",ref:ref("8","Later phases",5)}],SUMMARY="The court's case portal: lawyers and parties book hearings, pay court fees and get notified, in Arabic and English. Clerks manage court rooms and hearing days, and court admins see reports and an audit log.";function demoQuestions(){return[{id:"q1",from:"analyst",kind:"clash",text:"Section 4.4 refunds the fee in full for every cancelled hearing. Section 6.3 keeps the fee when a party cancels less than 48 hours before. Which one is right?",options:["Refund in full every time (4.4)","Keep the fee within 48 hours (6.3)"],refs:[ref("4.4","Refunds",3),ref("6.3","Holidays and late cancels",4)],impact:"Changes 2 stories in Pay court fees"},{id:"q2",from:"analyst",kind:"gap",text:"Can parties also pay the court fee at the court desk, or only online?",options:["Online only","Online or at the court desk"],refs:[ref("4.2","Paying online",3)],impact:"Changes the whole Pay court fees epic"}]}var READ_DECIDED=[{text:"A held slot is freed after 10 minutes without a warning first. The BRD does not ask for one.",story:3},{text:"Messages go in the language the person chose on their profile, Arabic when none is set.",story:8}];function fromAnswer(q5){let a=q5.answer;if(!a)return null;let pick2=a.choice===SKIP_CHOICE?q5.options[0]:a.choice;return q5.id==="q1"?a.choice==="Other"?{text:`Refunds: ${a.value??""}`,story:12}:pick2===q5.options[1]?{text:"The fee is kept when a party cancels less than 48 hours before the hearing, as section 6.3 says.",story:12}:{text:"Every cancelled hearing is refunded in full, as section 4.4 says.",story:12}:q5.id==="q2"?a.choice==="Other"?{text:`Paying the fee: ${a.value??""}`,story:10}:pick2===q5.options[1]?{text:"Parties can also pay at the court desk: a clerk marks the fee as paid from the case page.",story:10}:{text:"Fees are paid online only, by card.",story:10}:null}function storyOf(p,key){return{key,title:p.title,story:`As a ${p.who}, I want ${p.want}, so that ${p.why}.`,criteria:p.done.map(([given,when2,then])=>({given,when:when2,then})),size:p.size,dependsOn:[],ref:p.ref}}function planKeys(b,first2){return new Map(PLAN.flatMap(e=>e.features.flatMap(f=>f.stories)).map((_,i)=>[i+1,`S-${first2+i}`]).filter(([,k])=>findItem(b,k)))}function addOutline(b){b.epics=PLAN.map(e=>({key:nextKey(b,"epic"),title:e.title,goal:e.goal,ref:e.ref,features:e.features.map(f=>({key:nextKey(b,"feature"),title:f.title,summary:f.summary,ref:f.ref,stories:[]})),stories:[]})),b.leftOut=LEFT_OUT2.map(l=>({...l}))}function addStories(b,index,first2){let epic=b.epics[index],plan=PLAN[index];if(!epic||!plan)return 0;let n=0;plan.features.forEach((fp,fi)=>{let f=epic.features[fi];if(f)for(let sp of fp.stories)f.stories.push(storyOf(sp,nextKey(b,"story"))),n+=1});let keys=planKeys(b,first2),flat=PLAN.flatMap(e=>e.features.flatMap(x=>x.stories));for(let[i,sp]of flat.entries()){let s=findItem(b,keys.get(i+1)??"")?.story;s&&(s.dependsOn=(sp.needs??[]).map(x=>keys.get(x)).filter(k=>!!k))}return n}function placeDecided(b,first2){let keys=planKeys(b,first2),all=[...READ_DECIDED,...b.questions.map(fromAnswer).filter(a=>!!a)];for(let d of b.decided){if(d.item)continue;let at3=all.find(r=>r.text===d.text)?.story,key=at3?keys.get(at3):void 0;key&&(d.item=key)}}function ago(minutes4){return new Date(Date.now()-minutes4*6e4).toISOString()}function clearDemoRequirements(bp){fs150.rmSync(requirementsDir(bp),{recursive:!0,force:!0})}function seedDemoRequirements(bp,user){clearDemoRequirements(bp);let id2="BR-1",dir=breakdownDir(bp,id2),file3="source/Case Portal BRD v1.docx";fs150.mkdirSync(path141.join(dir,"source"),{recursive:!0});let docx=demoBrdDocx();fs150.writeFileSync(path141.join(dir,file3),docx);let text3=prepareText(dir,{file:file3,kind:"docx"}),questions=demoQuestions();questions[0].answer={choice:questions[0].options[0]},questions[1].answer={choice:questions[1].options[1]};let b={id:id2,title:"Case Portal BRD v1",status:"ready",source:{file:file3,name:"Case Portal BRD v1.docx",kind:"docx",bytes:docx.length,pages:text3.parts,addedBy:user,addedAt:ago(95)},summary:SUMMARY,questions,epics:[],decided:[],leftOut:[],createdAt:ago(95),createdBy:user,owner:user,next:{epic:1,feature:1,story:1}};addOutline(b),PLAN.forEach((_,i)=>addStories(b,i,1));let at3=ago(88);for(let r of READ_DECIDED)b.decided.push({id:nextDecidedId(b.decided),text:r.text,by:"analyst",at:at3});for(let q5 of questions){let a=fromAnswer(q5);a&&b.decided.push({id:nextDecidedId(b.decided),text:a.text,by:"answer",at:ago(70)})}placeDecided(b,1),writeBreakdown(bp,b);let stories=allStories(b).length,state={activity:[{at:ago(66),who:"analyst",text:`The breakdown is ready for you: ${b.epics.length} epics, 7 features and ${stories} stories`},{at:ago(70),who:"you",text:"Answered 2 questions"},{at:ago(86),who:"analyst",text:"Has 2 questions for you"},{at:ago(86),who:"analyst",text:"Read the document: found 3 epics and 7 features, and decided 2 small things itself"},{at:ago(95),who:"squad",text:"Turned the Word file into text. Kept the headings and tables"},{at:ago(95),who:"you",text:"Added Case Portal BRD v1.docx"}],job:{step:"draft",status:"done",found:{epics:b.epics.length,features:7,stories}}};writeBreakdownState(bp,id2,state)}var queueing=new Set;function playDemoQueue(bp){queueing.has(bp.root)||(queueing.add(bp.root),fillBuildQueue(bp,{free:()=>1/0,pause:null,payment:async()=>"subscription"}).catch(()=>{}).finally(()=>queueing.delete(bp.root)))}var BEAT_MS=2500,beats=new Map;function beat(bp,id2,speed){let key=`${bp.root}:${id2}`,last=beats.get(key),at3=Date.now();return last===void 0?(beats.set(key,at3),!1):at3-last<BEAT_MS/Math.max(.1,speed)?!1:(beats.set(key,at3),!0)}function playDemoBreakdowns(bp,meta){for(let{id:id2,job}of breakdownJobs(bp,meta)){if(job.status==="stopping"){breakdownStopped(bp,id2);continue}if(!(job.heldFor==="long"||!beat(bp,id2,meta.speed)))try{playStep(bp,id2)}catch(err2){updateBreakdown(bp,id2,x=>{job.step==="reply"||job.step==="redo"||(x.status="failed",x.error=err2.message)}),updateBreakdownState(bp,id2,s=>{s.job&&Object.assign(s.job,{status:"failed",error:err2.message,now:void 0})})}}}function playStep(bp,id2){let state=readBreakdownState(bp,id2),job=state.job,b=readBreakdown(bp,id2);switch(job.status==="pending"&&updateBreakdownState(bp,id2,s=>Object.assign(s.job,{status:"running",startedAt:now()})),job.step){case"text":{let p=prepareText(breakdownDir(bp,id2),b.source);updateBreakdownState(bp,id2,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")});return}case"read":return playRead(bp,b,state);case"reply":return playReply(bp,b,job.question);case"draft":return playDraft(bp,b);case"redo":return playRedo(bp,b,job.item??"",job.ask??"")}}function playRead(bp,b,state){let of=b.source.kind==="pdf"?b.source.pages??1:Math.max(1,textParts(breakdownDir(bp,b.id)).length),unit=b.source.kind==="pdf"?"page":"part",step2=(state.job.found?.epics??0)+1;if(step2<=3){updateBreakdownState(bp,b.id,s=>Object.assign(s.job,{now:"Reading the document",at:{done:Math.max(1,Math.round(step2*of/3)),of,unit},found:{epics:step2,features:[2,5,7][step2-1],stories:0}}));return}let questions=demoQuestions();updateBreakdown(bp,b.id,x=>{x.summary=SUMMARY,x.questions=questions;for(let r of READ_DECIDED)x.decided.some(d=>d.text===r.text)||x.decided.push({id:nextDecidedId(x.decided),text:r.text,by:"analyst",at:now()});x.status="questions"}),updateBreakdownState(bp,b.id,s=>{s.job={step:"read",status:"done",found:{epics:3,features:7,stories:0},at:{done:of,of,unit}},addBreakdownActivity(s,"analyst","Read the document: found 3 epics and 7 features, and decided 2 small things itself"),addBreakdownActivity(s,"analyst","Has 2 questions for you")})}var REPLIES={q1:"Two sections of the BRD give different refund rules. Refunding every time is simpler for lawyers; keeping the fee within 48 hours protects the court from late cancels. The refund story is built the way you pick.",q2:"The BRD only names paying by card. If parties can pay at the desk too, clerks need a way to mark a fee as paid, which adds a small story to the fees epic."};function playReply(bp,b,qid){updateBreakdown(bp,b.id,x=>{let q5=x.questions.find(y=>y.id===qid);q5&&(q5.thread=[...q5.thread??[],{who:"analyst",text:REPLIES[q5.id]??"Pick the option closest to what you mean. I will write down what it means for the stories.",at:now()}],q5.thinking=!1)}),updateBreakdownState(bp,b.id,s=>Object.assign(s.job,{status:"done",now:void 0}))}function playDraft(bp,b){if(!b.epics.length){let after=updateBreakdown(bp,b.id,x=>{addOutline(x);for(let q5 of x.questions){let a=fromAnswer(q5);a&&x.decided.push({id:nextDecidedId(x.decided),text:a.text,by:q5.answer?.choice===SKIP_CHOICE?"analyst":"answer",at:now()})}});updateBreakdownState(bp,b.id,s=>{Object.assign(s.job,{now:`Writing the stories of ${after.epics[0]?.title} (epic 1 of ${after.epics.length})`,found:{epics:after.epics.length,features:7,stories:0}}),addBreakdownActivity(s,"analyst",`Wrote the outline: ${after.epics.length} epics and 7 features`)});return}let i=b.epics.findIndex(e=>!e.features.some(f=>f.stories.length)&&!e.stories.length);if(i>=0){let first2=b.next.story-allStories(b).length,made=0,after=updateBreakdown(bp,b.id,x=>{made=addStories(x,i,first2),placeDecided(x,first2)}),next=after.epics[i+1];updateBreakdownState(bp,b.id,s=>{Object.assign(s.job,{now:next?`Writing the stories of ${next.title} (epic ${i+2} of ${after.epics.length})`:"Checking the breakdown",found:{epics:after.epics.length,features:7,stories:allStories(after).length}}),addBreakdownActivity(s,"analyst",`Wrote ${made} stories for ${after.epics[i].title}`)});return}let stories=allStories(b).length;updateBreakdown(bp,b.id,x=>{x.status="ready",delete x.error}),updateBreakdownState(bp,b.id,s=>{s.job={step:"draft",status:"done",found:{epics:b.epics.length,features:7,stories}},addBreakdownActivity(s,"analyst",`The breakdown is ready for you: ${b.epics.length} epics, 7 features and ${stories} stories`)})}function playRedo(bp,b,item,ask2){let rewritten=[];updateBreakdown(bp,b.id,x=>{let before=structuredClone(x);if(item===x.id){x.summary=`${x.summary??""} ${ask2}`.trim();return}let f=findItem(x,item);f&&(f.level==="story"&&!f.story.ticket?f.story.criteria.push({given:"the story as you asked",when:"it is built",then:ask2.replace(/\.$/,"")}):f.level==="feature"?f.feature.summary=ask2:f.level==="epic"&&(f.item.goal=ask2),x.status==="approved"&&(rewritten=rewrittenStories(before,x)))}),updateBreakdownState(bp,b.id,s=>{s.job={step:"redo",status:"done",item,ask:ask2},rewritten.length&&(s.rewritten=[...new Set([...s.rewritten??[],...rewritten])]),addBreakdownActivity(s,"analyst",`Redid ${item===b.id?"the breakdown":item}`)})}function ago2(minutes4){return new Date(Date.now()-minutes4*6e4).toISOString()}function daysAgo(days){return new Date(Date.now()-days*864e5).toISOString()}function gitUserName(){try{return execSync3("git config user.name",{encoding:"utf8",stdio:["ignore","pipe","ignore"],env:gitEnv()}).trim().split(/\s+/)[0]||"You"}catch{return"You"}}function base(id2,title2,over){return{id:id2,title:title2,ask:title2,kind:"feature",source:{type:"manual"},priority:"normal",stage:"tickets",status:"",waiting:null,createdAt:ago2(600),createdBy:"Momen",builder:"Codex",reviewer:"Claude",activity:[],...over}}var log2=entries=>entries.map(([m,who4,text3])=>({at:ago2(m),who:who4,text:text3})),ran=entries=>entries.map(([who4,step2,from,to])=>({who:who4,step:step2,from:ago2(from),to:ago2(to)}));function demoTickets(user){let darkMode2=base("SK-148","Dark mode for the dashboard",{ask:"Staff on night shifts say the white screen hurts their eyes. Add a dark mode to the dashboard.",source:{type:"azure",ref:"AB#4120"},priority:"high",stage:"specs",status:"4 questions for you",createdBy:user,createdAt:ago2(9),activity:log2([[6,"analyst","Asked 4 questions"],[7,"analyst","Looking at how staff use the dashboard at night"],[8,"analyst","Reading the ticket and the screenshot"],[9,"you","Pulled AB#4120 from Azure DevOps"]])});return darkMode2.questions=scriptFor(darkMode2).questions.map(q5=>({...q5,options:[...q5.options]})),darkMode2.waiting={kind:"questions",summary:"4 questions from the Analyst and Architect"},[base("SK-139","Search by case number",{ask:"Clerks scroll long lists to find a case. Let them type the case number and jump to it.",source:{type:"azure",ref:"AB#4098"},stage:"review",status:"Proof ready to check",waiting:{kind:"proof",summary:"The proof is ready for you to check"},createdAt:ago2(46),quality:"best",questions:[{id:"q1",from:"analyst",text:"Should a part of a number find cases too, or only the full number?",options:["Only the full number","Part of a number too"],answer:{choice:"Part of a number too"}},{id:"q2",from:"architect",text:"Where should the search box go?",options:["Above the case list","In the top bar on every page"],answer:{choice:"Above the case list"}}],decision:{title:"Where the case search lives",options:[{name:"A search box above the case list",note:"One place, next to the cases it finds.",chosen:!0},{name:"A search box in the top bar",note:"On every page, but far from the list."}],why:"Clerks search while they look at the list, and Angular Material has the search field already."},follows:["ADR-011"],history:ran([["analyst","questions",44.6,44.1],["analyst","spec",39.8,39.65],["architect","design",35.8,35.1],["builder","build",29.5,14.5],["squad","checks",14.4,13.2],["tester","proof",13.1,11.9],["reviewer","review",5.3,3]]),story:"As a clerk, I want to search by case number, so I find a case in seconds.",criteria:[{given:"I'm on the case list",when:"I type a full case number",then:"that case opens at once"},{given:"I type part of a number",when:"I pause",then:"I see the matching cases"}],steps:[{title:"Search box above the case list",status:"done",minutes:3},{title:"Search by number in the API",status:"done",minutes:5},{title:"Show matches while typing",status:"done",minutes:4},{title:'Tests for every "done when"',status:"done",minutes:3}],gates:{checks:"pass",checksText:"18 of 18 checks pass",review:"pass",reviewBy:"Claude",proof:"pass"},specApproved:{by:"Momen",at:ago2(36)},stageTimes:{stage:"review",since:ago2(12),spent:{tickets:35,specs:250,implement:960,test:140}},proof:[{criterion:"Typing a full case number opens the case",before:"sk139-before.png",after:"sk139-after.png",beforeLabel:"Before",afterLabel:"After",note:"Case 2024/118 typed in full opens straight away, at 1440 by 900.",page:"/cases",shows:"met"},{criterion:"I see the matching cases",before:"sk139-before.png",after:"sk139-after.png",beforeLabel:"Before",afterLabel:"After",note:'Typing "2024/1" lists the three cases that start with it.',page:"/cases",more:[{after:"sk148-light.png",page:"/cases?q=1446",note:'Typing "1446" lists the cases of that year.'},{after:"sk148-dark-b.png",page:"/hearings?q=1446",note:"The same search on the hearings list shows the matching hearings."}],shows:"met"}],releaseNote:"**Search by case number.** Type a case number and jump straight to the case.",activity:log2([[3,"reviewer",'Approved: meets both "done when" points and follows ADR-011'],[5.3,"reviewer","Reading the changes (round 1 of 2, Claude)"],[11.9,"tester","Saved 1 screenshot"],[13.1,"tester","Taking screenshots after the change"],[13.2,"tester","All 18 checks pass"],[14.4,"tester","squad-kit is running the project checks"],[14.5,"builder","Finished all steps"],[29.5,"builder","Started building"],[30,"you","Approved the design"],[35.1,"architect","Wrote the design"],[36,"you","Approved the spec"],[39.65,"analyst","Wrote the spec"],[40,"you","Answered 2 questions"],[44.1,"analyst","Asked 2 questions"],[44.11,"analyst","Read the case list page, the case search in the API and saved decision ADR-011"],[45,"you","Pulled AB#4098 from Azure DevOps"]])}),base("SK-140","Date shows in the wrong format",{kind:"bug",ask:"On the hearing page the date shows as month/day/year.",stage:"bugs",priority:"high",severity:"stops-work",foundIn:"v2.3.1",status:"Found in v2.3.1",activity:log2([[50,"triage","Sorted the bug. Priority: High. Linked to v2.3.1"]])}),base("SK-143","Session timeout warning",{stage:"release",release:"v2.4.0",status:"Ready for v2.4.0",quality:"best",mergedAt:ago2(1200),story:"As a clerk, I want a warning before I am logged out, so I can save my work.",criteria:[{given:"I have been idle for 14 minutes",when:"one minute is left",then:"I see a warning with a Stay signed in button"}],history:ran([["analyst","questions",1440,1440-.6],["analyst","spec",23.5*60,23.5*60-.2],["architect","design",1380,1380-.8],["builder","build",22.5*60,22.5*60-11],["squad","checks",22.3*60,22.3*60-1.1],["tester","proof",22.2*60,22.2*60-1.4],["reviewer","review",1320,1320-2.2]]),activity:log2([[1200,"you","Checked the proof. Looks right"]]),gates:{checks:"pass",checksText:"16 of 16 checks pass",review:"pass",reviewBy:"Claude",proof:"pass"},releaseNote:"**Session timeout warning.** You get a warning one minute before you are logged out."}),base("SK-145","Faster login",{stage:"review",live:!0,status:"Reviewer is reading the login changes",squad:null,clock:{who:"reviewer",step:"review",since:ago2(1.2),limit:15}}),base("SK-146","Filter cases by court",{stage:"test",live:!0,progress:67,status:"Running checks for the court filter",clock:{who:"squad",step:"checks",since:ago2(.8),limit:15}}),base("SK-147","Export case lists to PDF",{stage:"release",release:"v2.4.0",status:"Ready for v2.4.0",quality:"best",mergedAt:ago2(1560),history:ran([["analyst","questions",1800,1800-.7],["analyst","spec",29.6*60,29.6*60-.3],["architect","design",29.2*60,29.2*60-1],["builder","build",28.8*60,28.8*60-18],["squad","checks",28.4*60,28.4*60-1.3],["builder","fix",28.3*60,28.3*60-4],["squad","checks",28.2*60,28.2*60-1.2],["tester","proof",28.1*60,28.1*60-1.6],["reviewer","review",27.9*60,27.9*60-2.4]]),activity:log2([[1560,"you","Checked the proof. Looks right"]]),gates:{checks:"pass",checksText:"16 of 16 checks pass",review:"pass",reviewBy:"Claude",proof:"pass"},releaseNote:"**Export to PDF.** Download any case list as a PDF, ready to print."}),base("SK-138","Fix: calendar week starts on Sunday",{kind:"bug",stage:"done",release:"v2.3.1",status:"Released in v2.3.1",quality:"best",createdAt:daysAgo(19),mergedAt:daysAgo(18),gates:{checks:"pass",checksText:"15 of 15 checks pass",review:"pass",reviewBy:"Claude",proof:"pass"},history:ran([["analyst","questions",19*1440-10,19*1440-10.5],["analyst","spec",19*1440-30,19*1440-30.2],["architect","design",19*1440-40,19*1440-40.7],["builder","build",19*1440-50,19*1440-58],["squad","checks",19*1440-59,19*1440-60],["tester","proof",19*1440-61,19*1440-62.2],["reviewer","review",19*1440-63,19*1440-65]])}),base("SK-141","Fix: logout button on small screens",{kind:"bug",stage:"done",track:"express",release:"v2.3.1",status:"Released in v2.3.1",quality:"best",createdAt:daysAgo(18),mergedAt:daysAgo(17.9),gates:{checks:"pass",checksText:"15 of 15 checks pass",review:"pass",reviewBy:"Claude",proof:"pass"},history:ran([["architect","design",18*1440-5,18*1440-5.4],["builder","build",18*1440-6,18*1440-9],["squad","checks",18*1440-9.2,18*1440-10.1],["tester","proof",18*1440-10.3,18*1440-11.2],["reviewer","review",18*1440-11.5,18*1440-12.6]])}),darkMode2,base("SK-149","Arabic names in PDF exports",{source:{type:"azure",ref:"AB#4126"},stage:"tickets",status:"From Azure, in queue"}),base("SK-150","Case notes autosave",{stage:"implement",status:"Stuck after 3 test runs",waiting:{kind:"stuck",summary:"The squad is stuck and needs one decision"},stuck:{gate:"checks",question:"The checks still fail after 3 tries: the tests show two clerks editing the same case notes overwrite each other.",detail:`test (npm test):
1856
1856
  autosave.spec.ts: two clerks editing the same case notes overwrite each other.`,options:[{do:"retry",name:"Let the Builder try 3 more times",note:"It gets the full error and tries again."},{do:"yourself",name:"Fix it yourself",note:"Open the ticket's copy in your editor and fix it. Then the checks, review and proof run again."},{do:"continue",name:"Continue anyway",note:"Go on with the checks failing. Write why: it shows on the ticket, the pull request and the release notes."},{do:"plan",name:"Send it back to the plan",note:"The Spec writer rewrites the plan with what went wrong."}]},steps:[{title:"Read the related code",status:"done",minutes:2},{title:"Build case notes autosave",status:"done",minutes:6},{title:"Connect it to the existing screens",status:"todo"},{title:'Tests for every "done when"',status:"todo"}],story:"As a clerk, I want my case notes saved as I type, so I never lose my work.",progress:50,activity:log2([[2,"tester","Checks failed 3 times. Stopped and asked you"],[12,"builder","Finished step 2: Build case notes autosave"]])}),base("SK-142","Highlight the case you picked",{ask:"After a search, clerks lose track of the case they opened. Highlight it in the list.",stage:"review",status:"Proof ready to check",waiting:{kind:"proof",summary:"The proof is ready for you to check"},story:"As a clerk, I want the case I picked to stand out in the list, so I do not lose my place.",criteria:[{given:"I picked a case from the search",when:"the case list shows",then:"that row is highlighted"}],steps:[{title:"Remember the picked case",status:"done",minutes:2},{title:"Highlight its row in the list",status:"done",minutes:3},{title:"Check the colors in dark mode",status:"done",minutes:2}],work:{steps:3,tests:0,screens:1},gates:{checks:"unchecked",checksText:"No tests ran \xB7 build passes, the app starts",review:"pass",reviewBy:"Claude",proof:"pass"},specApproved:{by:"Momen",at:ago2(40)},stageTimes:{stage:"review",since:ago2(6),spent:{tickets:30,specs:200,implement:420,test:90}},proof:[{criterion:"That row is highlighted",before:"sk139-before.png",after:"sk139-after.png",beforeLabel:"Before",afterLabel:"After",note:"Recorded by the Tester in Chrome at 1280 by 800",shows:"met"}],releaseNote:"**Highlight the picked case.** The case you opened stands out in the list.",activity:log2([[3,"reviewer","Approved the changes"],[5,"tester","Not checked: No tests ran \xB7 build passes, the app starts. You chose to go on without tests in this project"],[6,"tester","squad-kit is running the project checks"]])}),base("SK-144","Hearing reminders by email",{stage:"test",status:"The tests were made weaker",waiting:{kind:"stuck",summary:"The tests were made weaker: needs one decision"},retryPhase:"restore-tests",gates:{checks:"fail",checksText:"Tests were weakened",review:"pending",proof:"pending"},stuck:{gate:"checks",kind:"weakened",question:"This change makes the tests weaker than on main, so passing checks would prove little.",points:["web/src/app/reminders/reminder.service.spec.ts has fewer tests: 6 before, 4 now.","In web/src/app/hearings/hearing-list.spec.ts: tests switched off with it.skip.",'The test scripts or settings in web/package.json changed: "test" was "jest", now "jest --passWithNoTests".'],checks:["fewer:web/src/app/reminders/reminder.service.spec.ts:4","skip:web/src/app/hearings/hearing-list.spec.ts:it.skip:1","config:web/package.json:5d1c0a2e"],detail:`fewer: web/src/app/reminders/reminder.service.spec.ts
1857
1857
  skip: web/src/app/hearings/hearing-list.spec.ts
1858
- config: web/package.json`,options:[{do:"retry",name:"Send it back to the Builder",note:"It puts the tests back as they were, then the checks run again."},{do:"yourself",name:"Fix it yourself",note:"Open the ticket's copy in your editor and fix it. Then the checks, review and proof run again."},{do:"continue",name:"Continue anyway",note:"Go on with the tests as they are now; the checks still run. Write why: it shows on the ticket, the pull request and the release notes."},{do:"plan",name:"Send it back to the plan",note:"The Spec writer rewrites the plan with what went wrong."}]},story:"As a clerk, I want hearing reminders sent by email too, so people without SMS still come.",specApproved:{by:"Momen",at:ago2(70)},steps:[{title:"Add email to the reminder service",status:"done",minutes:6},{title:"Send at the same times as SMS",status:"done",minutes:4},{title:'Tests for every "done when"',status:"done",minutes:3}],progress:100,activity:log2([[2,"tester","Stopped before the checks: the tests were made weaker."],[4,"builder","Finished all steps"]])}),base("SK-135","Case search on a phone",{ask:"Clerks in court use their phones. The case search must work on a phone screen.",stage:"review",status:"The screenshots show it is not working yet",waiting:{kind:"stuck",summary:"The screenshots show it is not working yet: needs one decision"},retryPhase:"proof-fix",proofRound:2,story:"As a clerk in court, I want to search cases on my phone, so I find a case without a laptop.",criteria:[{given:"I'm on the case list on a phone",when:"I open it",then:"the search box shows above the list"},{given:"I'm on the case list on a phone",when:"I scroll the results",then:"the page fits the screen and does not scroll sideways"},{given:"I searched on a phone",when:"I open a case",then:"the back button returns to my results"}],specApproved:{by:"Momen",at:ago2(90)},steps:[{title:"Search box on small screens",status:"done",minutes:4},{title:"Results list on small screens",status:"done",minutes:5},{title:'Tests for every "done when"',status:"done",minutes:3}],work:{steps:3,tests:6,screens:3},gates:{checks:"pass",checksText:"14 of 14 checks pass",review:"pass",reviewBy:"Claude",proof:"fail"},proof:[{criterion:"The search box shows above the list",before:"sk139-before.png",after:"sk139-after.png",beforeLabel:"Before",afterLabel:"After",note:"The search box sits above the case list on a phone screen.",page:"/cases",shows:"met"},{criterion:"The page fits the screen and does not scroll sideways",before:"sk139-before.png",after:"sk139-after.png",beforeLabel:"Before",afterLabel:"After",note:"The results list is wider than the phone screen.",page:"/cases",shows:"broken",why:"At phone width the results list runs off the right edge, so the page scrolls sideways."},{criterion:"The back button returns to my results",before:"sk139-before.png",after:"sk139-after.png",beforeLabel:"Before",afterLabel:"After",note:"The case page is open.",page:"/cases/4417",more:[{after:"sk148-light.png",page:"/cases",note:"The results before opening the case."}],shows:"not-shown",why:"The pictures show the case page and the results before it, but not the results after pressing the back button."}],stuck:{gate:"proof",kind:"broken",question:'The screenshots show "done when" 2 is not working yet: at phone width the results list runs off the right edge, so the page scrolls sideways.',points:['"Done when" 2: The page fits the screen and does not scroll sideways','"Done when" 3, The back button returns to my results: The pictures show the case page and the results before it, but not the results after pressing the back button.'],options:[{do:"retry",name:"Let the Builder try again",note:"It gets what the screenshots show. Then the checks, the review and the screenshots run again."},{do:"continue",name:"Continue anyway",note:'Go on with a "done when" not working yet. Write why: it shows on the ticket, the pull request and the release notes.'},{do:"plan",name:"Send it back to the plan",note:"The Spec writer rewrites the plan with what went wrong."}]},activity:log2([[3,"tester",'Still not working after 2 rounds: "done when" 2'],[4,"reviewer","Checked the screenshots: 1 met, 1 not working yet, 1 not shown yet"],[12,"builder","Fixed what the screenshots showed"],[20,"tester",'The screenshots show one "done when" not working yet, so the Builder fixes it: "done when" 2: At phone width the results list runs off the right edge.']])}),base("SK-155","Hearing room on the case page",{ask:"Clerks want to see the hearing room on the case page, with the room number.",stage:"review",status:"The screenshots do not show it yet",waiting:{kind:"stuck",summary:"The screenshots do not show it yet: needs one decision"},retryPhase:"proof",story:"As a clerk, I want to see the hearing room on the case page, so I know where to go.",criteria:[{given:"a case with a hearing",when:"I open the case page",then:"the hearing room shows with its number"},{given:"a case with no hearing yet",when:"I open the case page",then:"it says no room yet"}],specApproved:{by:"Momen",at:ago2(70)},steps:[{title:"Add the hearing room to the case page",status:"done",minutes:3},{title:'Tests for every "done when"',status:"done",minutes:2}],work:{steps:2,tests:3,screens:2},gates:{checks:"pass",checksText:"9 of 9 checks pass",review:"pass",reviewBy:"Claude",proof:"fail"},proof:[{criterion:"The hearing room shows with its number",before:"sk139-before.png",after:"sk139-after.png",beforeLabel:"Before",afterLabel:"After",note:"Room 4 shows under the hearing date.",page:"/cases/4417",shows:"met"},{criterion:"It says no room yet",before:"sk139-before.png",after:"sk139-after.png",beforeLabel:"Before",afterLabel:"After",note:"The case page is open.",page:"/cases/4502",shows:"not-shown",why:"The picture shows a case with a hearing, so the words for a case with no hearing are not in it."}],stuck:{gate:"proof",kind:"not-shown",question:'The squad could not show "done when" 2 on a screen. Try it yourself, or ask a question.',points:['"Done when" 2, It says no room yet: The picture shows a case with a hearing, so the words for a case with no hearing are not in it.'],options:[{do:"retry",name:"Take the screenshots again",note:"Only the proof runs again. Nothing is rebuilt. You can add a note for the Tester."},{do:"continue",name:"Continue anyway",note:'Go on with a "done when" not shown yet. Write why: it shows on the ticket, the pull request and the release notes.'},{do:"plan",name:"Send it back to the plan",note:"The Spec writer rewrites the plan with what went wrong."}]},activity:log2([[3,"tester",'Could not show "done when" 2 on a screen after 2 more tries'],[4,"reviewer","Checked the screenshots: 1 met, 1 not shown yet"],[8,"tester",'The screenshots do not show "done when" 2 clearly yet, so the Tester takes it again (try 2 of 2)'],[11,"tester",'The screenshots do not show "done when" 2 clearly yet, so the Tester takes it again (try 1 of 2)']])}),base("SK-152","Court filter on the hearing calendar",{ask:"Clerks want the court filter from the case list on the hearing calendar too.",stage:"implement",status:"SK-152 needs SK-146, which is not merged yet",waiting:{kind:"depends",summary:"SK-152 needs SK-146, which is not merged yet"},dependsOn:["SK-146"],specApproved:{by:"Momen",at:ago2(22)},story:"As a clerk, I want to filter the hearing calendar by court, so I see only my court's hearings.",criteria:[{given:"I am on the hearing calendar",when:"I pick a court",then:"I see only that court's hearings"},{given:"I picked a court",when:"I move to the next week",then:"the calendar keeps my court"}],work:{steps:3,tests:3,screens:1},steps:[{title:"Use the court filter from SK-146 on the calendar",status:"todo"},{title:"Keep the chosen court when the week changes",status:"todo"},{title:'Tests for every "done when"',status:"todo"}],activity:log2([[1,"squad","SK-152 needs SK-146, which is not merged yet, so the build waits for you to choose how to build"],[2,"you","Approved the design"],[6,"architect","Wrote the design. It uses the court filter that SK-146 adds"]])}),base("SK-151","Print the hearing schedule",{stage:"tickets",live:!0,status:"Analyst is reading the ticket",clock:{who:"analyst",step:"questions",since:ago2(.6),limit:10}}),base("SK-153","Upload limit message",{stage:"implement",live:!0,progress:50,status:"Editing web/src/app/upload/upload.component.ts",specApproved:{by:"Momen",at:ago2(30)},work:{steps:6,tests:5,screens:2},steps:[{title:"Read the upload code",status:"done",minutes:3},{title:"Keep the limit in one setting",status:"done",minutes:4},{title:"Check the size before uploading",status:"done",minutes:3},{title:"Show the limit in plain words",status:"doing",live:"Editing web/src/app/upload/upload.component.ts"},{title:"Same check in the API",status:"todo"},{title:'Tests for every "done when"',status:"todo"}],clock:{who:"builder",step:"build",part:4,since:ago2(14),doing:"Editing web/src/app/upload/upload.component.ts",doingAt:ago2(.6),limit:60},stageTimes:{stage:"implement",since:ago2(24),spent:{tickets:40,specs:290}},activity:log2([[14,"builder","Started step 4: Show the limit in plain words"],[14,"builder","Finished step 3: Check the size before uploading"],[17,"builder","Finished step 2: Keep the limit in one setting"],[21,"builder","Finished step 1: Read the upload code"],[24,"builder","Started building"]])})]}function demoReleases(){let addons={changelog:!0,tag:!0,pipeline:!0,whatsapp:!0};return[{version:"v2.2.0",status:"released",from:"v2.1.0",date:daysAgo(49),tickets:["SK-118","SK-121","SK-124","SK-126"],titles:{"SK-118":"Hearing reminders by SMS","SK-121":"Case list paging","SK-124":"Clerk profile page","SK-126":"Faster case search"},notes:`### What's new in 2.2.0
1858
+ config: web/package.json`,options:[{do:"retry",name:"Send it back to the Builder",note:"It puts the tests back as they were, then the checks run again."},{do:"yourself",name:"Fix it yourself",note:"Open the ticket's copy in your editor and fix it. Then the checks, review and proof run again."},{do:"continue",name:"Continue anyway",note:"Go on with the tests as they are now; the checks still run. Write why: it shows on the ticket, the pull request and the release notes."},{do:"plan",name:"Send it back to the plan",note:"The Spec writer rewrites the plan with what went wrong."}]},story:"As a clerk, I want hearing reminders sent by email too, so people without SMS still come.",specApproved:{by:"Momen",at:ago2(70)},steps:[{title:"Add email to the reminder service",status:"done",minutes:6},{title:"Send at the same times as SMS",status:"done",minutes:4},{title:'Tests for every "done when"',status:"done",minutes:3}],progress:100,activity:log2([[2,"tester","Stopped before the checks: the tests were made weaker."],[4,"builder","Finished all steps"]])}),base("SK-135","Case search on a phone",{ask:"Clerks in court use their phones. The case search must work on a phone screen.",stage:"review",status:"The screenshots show it is not working yet",waiting:{kind:"stuck",summary:"The screenshots show it is not working yet: needs one decision"},retryPhase:"proof-fix",proofRound:2,story:"As a clerk in court, I want to search cases on my phone, so I find a case without a laptop.",criteria:[{given:"I'm on the case list on a phone",when:"I open it",then:"the search box shows above the list"},{given:"I'm on the case list on a phone",when:"I scroll the results",then:"the page fits the screen and does not scroll sideways"},{given:"I searched on a phone",when:"I open a case",then:"the back button returns to my results"}],specApproved:{by:"Momen",at:ago2(90)},steps:[{title:"Search box on small screens",status:"done",minutes:4},{title:"Results list on small screens",status:"done",minutes:5},{title:'Tests for every "done when"',status:"done",minutes:3}],work:{steps:3,tests:6,screens:3},gates:{checks:"pass",checksText:"14 of 14 checks pass",review:"pass",reviewBy:"Claude",proof:"fail"},proof:[{criterion:"The search box shows above the list",before:"sk139-before.png",after:"sk139-after.png",beforeLabel:"Before",afterLabel:"After",note:"The search box sits above the case list on a phone screen.",page:"/cases",shows:"met"},{criterion:"The page fits the screen and does not scroll sideways",before:"sk139-before.png",after:"sk139-after.png",beforeLabel:"Before",afterLabel:"After",note:"The results list is wider than the phone screen.",page:"/cases",shows:"broken",why:"At phone width the results list runs off the right edge, so the page scrolls sideways."},{criterion:"The back button returns to my results",before:"sk139-before.png",after:"sk139-after.png",beforeLabel:"Before",afterLabel:"After",note:"The case page is open.",page:"/cases/4417",more:[{after:"sk148-light.png",page:"/cases",note:"The results before opening the case."}],shows:"not-shown",why:"The pictures show the case page and the results before it, but not the results after pressing the back button."}],stuck:{gate:"proof",kind:"broken",question:'The screenshots show "done when" 2 is not working yet: at phone width the results list runs off the right edge, so the page scrolls sideways.',points:['"Done when" 2: The page fits the screen and does not scroll sideways','"Done when" 3, The back button returns to my results: The pictures show the case page and the results before it, but not the results after pressing the back button.'],options:[{do:"retry",name:"Let the Builder try again",note:"It gets what the screenshots show. Then the checks, the review and the screenshots run again."},{do:"continue",name:"Continue anyway",note:'Go on with a "done when" not working yet. Write why: it shows on the ticket, the pull request and the release notes.'},{do:"plan",name:"Send it back to the plan",note:"The Spec writer rewrites the plan with what went wrong."}]},activity:log2([[3,"tester",'Still not working after 2 rounds: "done when" 2'],[4,"reviewer","Checked the screenshots: 1 met, 1 not working yet, 1 not shown yet"],[12,"builder","Fixed what the screenshots showed"],[20,"tester",'The screenshots show one "done when" not working yet, so the Builder fixes it: "done when" 2: At phone width the results list runs off the right edge.']])}),base("SK-155","Hearing room on the case page",{ask:"Clerks want to see the hearing room on the case page, with the room number.",stage:"review",status:"The screenshots do not show it yet",waiting:{kind:"stuck",summary:"The screenshots do not show it yet: needs one decision"},retryPhase:"proof",story:"As a clerk, I want to see the hearing room on the case page, so I know where to go.",criteria:[{given:"a case with a hearing",when:"I open the case page",then:"the hearing room shows with its number"},{given:"a case with no hearing yet",when:"I open the case page",then:"it says no room yet"}],specApproved:{by:"Momen",at:ago2(70)},steps:[{title:"Add the hearing room to the case page",status:"done",minutes:3},{title:'Tests for every "done when"',status:"done",minutes:2}],work:{steps:2,tests:3,screens:2},gates:{checks:"pass",checksText:"9 of 9 checks pass",review:"pass",reviewBy:"Claude",proof:"fail"},proof:[{criterion:"The hearing room shows with its number",before:"sk139-before.png",after:"sk139-after.png",beforeLabel:"Before",afterLabel:"After",note:"Room 4 shows under the hearing date.",page:"/cases/4417",shows:"met"},{criterion:"It says no room yet",before:"sk139-before.png",after:"sk139-after.png",beforeLabel:"Before",afterLabel:"After",note:"The case page is open.",page:"/cases/4502",shows:"not-shown",why:"The picture shows a case with a hearing, so the words for a case with no hearing are not in it."}],stuck:{gate:"proof",kind:"not-shown",question:'The squad could not show "done when" 2 on a screen. Try it yourself, or ask a question.',points:['"Done when" 2, It says no room yet: The picture shows a case with a hearing, so the words for a case with no hearing are not in it.'],options:[{do:"retry",name:"Take the screenshots again",note:"Only the proof runs again. Nothing is rebuilt. You can add a note for the Tester."},{do:"continue",name:"Continue anyway",note:'Go on with a "done when" not shown yet. Write why: it shows on the ticket, the pull request and the release notes.'},{do:"plan",name:"Send it back to the plan",note:"The Spec writer rewrites the plan with what went wrong."}]},activity:log2([[3,"tester",'Could not show "done when" 2 on a screen after 2 more tries'],[4,"reviewer","Checked the screenshots: 1 met, 1 not shown yet"],[8,"tester",'The screenshots do not show "done when" 2 clearly yet, so the Tester takes it again (try 2 of 2)'],[11,"tester",'The screenshots do not show "done when" 2 clearly yet, so the Tester takes it again (try 1 of 2)']])}),base("SK-152","Court filter on the hearing calendar",{ask:"Clerks want the court filter from the case list on the hearing calendar too.",stage:"implement",status:"SK-152 needs SK-146, which is not merged yet",waiting:{kind:"depends",summary:"SK-152 needs SK-146, which is not merged yet"},dependsOn:["SK-146"],specApproved:{by:"Momen",at:ago2(22)},story:"As a clerk, I want to filter the hearing calendar by court, so I see only my court's hearings.",criteria:[{given:"I am on the hearing calendar",when:"I pick a court",then:"I see only that court's hearings"},{given:"I picked a court",when:"I move to the next week",then:"the calendar keeps my court"}],work:{steps:3,tests:3,screens:1},steps:[{title:"Use the court filter from SK-146 on the calendar",status:"todo"},{title:"Keep the chosen court when the week changes",status:"todo"},{title:'Tests for every "done when"',status:"todo"}],activity:log2([[1,"squad","SK-152 needs SK-146, which is not merged yet, so the build waits for you to choose how to build"],[2,"you","Approved the design"],[6,"architect","Wrote the design. It uses the court filter that SK-146 adds"]])}),base("SK-151","Print the hearing schedule",{stage:"tickets",live:!0,status:"Analyst is reading the ticket",clock:{who:"analyst",step:"questions",since:ago2(.6),limit:10}}),base("SK-153","Upload limit message",{stage:"implement",live:!0,progress:80,status:"Fixing a failed test",specApproved:{by:"Momen",at:ago2(40)},work:{steps:6,tests:5,screens:2},steps:[{title:"Read the upload code",status:"done",minutes:3},{title:"Keep the limit in one setting",status:"done",minutes:4},{title:"Check the size before uploading",status:"done",minutes:3},{title:"Show the limit in plain words",status:"done",minutes:4},{title:"Same check in the API",status:"done",minutes:3},{title:'Tests for every "done when"',status:"doing",live:"Fixing a failed test"}],clock:{who:"builder",step:"build",part:6,since:ago2(14),began:ago2(31),doing:"Fixing a failed test",doingAt:ago2(.6),limit:60,failed:{check:"test",times:1,at:ago2(5)}},stageTimes:{stage:"implement",since:ago2(31),spent:{tickets:40,specs:290}},activity:log2([[5,"builder","squad-kit ran the tests for the Builder: did not pass"],[14,"builder",'Started step 6: Tests for every "done when"'],[14,"builder","Finished step 5: Same check in the API"],[17,"builder","Finished step 4: Show the limit in plain words"],[21,"builder","Finished step 3: Check the size before uploading"],[24,"builder","Finished step 2: Keep the limit in one setting"],[28,"builder","Finished step 1: Read the upload code"],[31,"builder","Started building"]])})]}function demoReleases(){let addons={changelog:!0,tag:!0,pipeline:!0,whatsapp:!0};return[{version:"v2.2.0",status:"released",from:"v2.1.0",date:daysAgo(49),tickets:["SK-118","SK-121","SK-124","SK-126"],titles:{"SK-118":"Hearing reminders by SMS","SK-121":"Case list paging","SK-124":"Clerk profile page","SK-126":"Faster case search"},notes:`### What's new in 2.2.0
1859
1859
 
1860
1860
  - **Hearing reminders by SMS.**
1861
1861
  - **Case list paging.**`,addons},{version:"v2.3.0",status:"released",from:"v2.2.0",date:daysAgo(31),tickets:["SK-129","SK-131","SK-133","SK-134","SK-136"],titles:{"SK-129":"Upload case documents","SK-131":"Court calendar view","SK-133":"Judge notes","SK-134":"Arabic date picker","SK-136":"Audit log for case changes"},notes:`### What's new in 2.3.0
@@ -1863,7 +1863,7 @@ config: web/package.json`,options:[{do:"retry",name:"Send it back to the Builder
1863
1863
  - **Upload case documents.**
1864
1864
  - **Court calendar view.**`,addons},{version:"v2.3.1",status:"released",from:"v2.3.0",date:daysAgo(17),tickets:["SK-137","SK-138","SK-141"],titles:{"SK-137":"Fix: upload stops at 5 MB","SK-138":"Fix: calendar week starts on Sunday","SK-141":"Fix: logout button on small screens"},bugs:["SK-140"],notes:`### What's new in 2.3.1
1865
1865
 
1866
- - **Fixed:** uploads stopped at 5 MB.`,addons},{version:"v2.4.0",status:"draft",from:"v2.3.1",tickets:["SK-147","SK-143"],notes:"",addons}]}function demoDecisions(user){return[{id:"ADR-011",title:"Use Angular Material for buttons and menus",why:"The client requires it across all ministry portals.",options:["Angular Material","Our own components"],chosen:"Angular Material",from:"Client's choice",approvedBy:user,date:daysAgo(20),checkedBy:{kind:"lint rule",name:"no-custom-buttons",passing:!0},tickets:["SK-131","SK-134"]},{id:"ADR-010",title:"Every list page can export to PDF",why:"Clerks print lists for court sessions every day.",options:["Export on some pages","Export on every list page"],chosen:"Export on every list page",from:"SK-147",approvedBy:user,date:daysAgo(2),checkedBy:{kind:"test",name:"list-pages-export.spec.ts",passing:!0},tickets:["SK-147"]},{id:"ADR-007",title:"Dates always show as day/month/year",why:"That is how dates are read in Saudi courts. A bug proved it matters.",options:["Follow the browser","Always day/month/year"],chosen:"Always day/month/year",from:"SK-112",approvedBy:user,date:daysAgo(40),checkedBy:{kind:"test",name:"date-format.spec.ts",passing:!0},tickets:["SK-112","SK-134"]},{id:"ADR-003",title:"The back end is a .NET 8 API",why:"Found by the Architect when it first read the project.",options:[".NET 8 API"],chosen:".NET 8 API",from:"Project scan",approvedBy:user,date:daysAgo(60),checkedBy:null,tickets:[]},{id:"ADR-002",title:"The front end is Angular 17",why:"Found by the Architect when it first read the project.",options:["Angular 17"],chosen:"Angular 17",from:"Project scan",approvedBy:user,date:daysAgo(60),checkedBy:null,tickets:[]}]}function demoMeta(user,speed,project){return{project,key:"SK",nextNumber:154,engine:"demo",user:{name:user,role:"Project manager"},roles:{analyst:"Claude",architect:"Claude",builder:"Codex",tester:"Codex",reviewer:"Claude"},tools:[{name:"Claude Code",ready:!0},{name:"Codex",ready:!0}],speed,map:[{area:"Front end",detail:"Angular 17 with Angular Material, in web/"},{area:"Back end",detail:".NET 8 API, in api/"},{area:"Database",detail:"SQL Server, migrations in api/Migrations/"},{area:"Tests",detail:"Jest for logic, Playwright for screens"},{area:"Code style",detail:"ESLint and Prettier rules in web/"},{area:"How to run",detail:"npm start in web/, dotnet run in api/"}],lessons:[{id:"L-003",text:"Dates must show as day/month/year. A test now guards it.",from:"SK-112",date:daysAgo(40)},{id:"L-002",text:"Big case lists need paging, or the page freezes.",from:"SK-121",date:daysAgo(50)},{id:"L-001",text:"Always check the Arabic layout, right to left, before the proof.",from:"SK-104",date:daysAgo(58)}]}}function demoAssetsDir(){let candidates=[fileURLToPath5(new URL("../templates/demo/proof/",import.meta.url)),fileURLToPath5(new URL("../../templates/demo/proof/",import.meta.url))];return candidates.find(c=>fs151.existsSync(c))??candidates[0]}var SAMPLE_SIZE={"SK-138":.8,"SK-141":.45,"SK-147":1.3};function demoFolder(){return path142.join(squadHome(),"demo")}function seedDemoBoard(bp,opts={}){clearBoard(bp),clearDemoRequirements(bp);let user=gitUserName(),speed=opts.speed??1,meta=demoMeta(user,speed,opts.project??"Case portal");if(opts.firstRun){writeMeta(bp,{...meta,nextNumber:1,lessons:[]}),seedFreshSetup(bp);return}writeMeta(bp,meta);for(let t of demoTickets(user))t.live&&t.stage!=="done"&&!t.waiting&&scheduleSquad(t,"ambient",3+Math.random()*4,speed,0),t.history?.length&&countSampleHistory(bp,t,SAMPLE_SIZE[t.id]??1),writeTicket(bp,{...t,owner:user});for(let r of demoReleases())writeRelease(bp,r);for(let d of demoDecisions(user))writeDecision(bp,d);seedDemoRequirements(bp,user);let assets=demoAssetsDir();if(fs151.mkdirSync(bp.proofDir,{recursive:!0}),fs151.existsSync(assets))for(let f of fs151.readdirSync(assets))fs151.copyFileSync(path142.join(assets,f),path142.join(bp.proofDir,f));seedDoneSetup(bp,user,daysAgo(60))}import fs152 from"fs";import path143 from"path";function demoStep(t){let s=t.squad;if(!s||t.waiting)return null;let doing=(t.steps??[]).findIndex(x=>x.status==="doing"),open3=(t.steps??[]).findIndex(x=>x.status!=="done"),part=doing>=0?doing+1:open3>=0?open3+1:void 0;switch(s.phase){case"analyst-read":return t.track==="express"||t.track==="auto"?null:{who:"analyst",step:"questions"};case"plan-write":case"spec-rewrite":case"plan-rewrite":return s.phase==="spec-rewrite"||s.phase==="plan-write"&&needsSpec(t)?{who:"analyst",step:"spec"}:{who:"architect",step:"design"};case"chat-spec":return{who:"analyst",step:"spec"};case"chat-design":case"answered-architect":return{who:"architect",step:"design"};case"build":case"unstick":case"answered-builder":case"chat-carry-builder":return t.stage==="implement"&&part?{who:"builder",step:"build",part}:{who:"builder",step:"build"};case"fix":return{who:"builder",step:"fix"};case"test":return/^(Recording|Checking the change)/.test(t.status)?{who:"tester",step:"proof"}:{who:"squad",step:"checks"};case"review":case"answered-reviewer":case"chat-carry-reviewer":return/^Builder/.test(t.status)?{who:"builder",step:"fix"}:{who:"reviewer",step:"review"};case"chat-carry-tester":return{who:"tester",step:"proof"};case"ambient":return t.stage==="tickets"?{who:"analyst",step:"questions"}:t.stage==="implement"?{who:"builder",step:"build",...part?{part}:{}}:t.stage==="test"?{who:"squad",step:"checks"}:{who:"reviewer",step:"review"};default:return null}}function endDemoStep(bp,t){let c=t.clock;c&&countSampleStep(bp,t,c.who,c.step,readMeta(bp).quality),endStep(bp,t)}function keepDemoClock(bp,t){let next=demoStep(t),c=t.clock;if(!next){c&&endDemoStep(bp,t);return}c&&(c.who!==next.who||c.step!==next.step)&&endDemoStep(bp,t),t.clock&&next.part&&nextPart(bp,t,next.part),t.clock||startStep(t,next);let line3=/^Step \d+ of \d+/.test(t.status)?t.steps?.find(s=>s.status==="doing")?.live??"":t.status;line3&&line3!==t.clock?.doing&&stepDoing(t,line3)}function stopDemoStep(bp,t,speed){if(t.stopRequested=void 0,delete t.quiet,t.retryOnStop){t.retryOnStop=void 0;let limit=t.clock?.limit;delete t.clock,t.live=!0,addActivity(t,"squad","Stopped the step and started it again, with the work so far"),t.squad&&(t.squad.nextAt=new Date(Date.now()+1e3/Math.max(speed,.1)).toISOString()),keepDemoClock(bp,t);let fresh=t.clock;fresh&&limit&&(fresh.limit=limit),t.status="Trying the step again";return}delete t.clock,t.live=!1,t.retryPhase=t.squad?.phase,t.squad=null,t.status="Stopped, needs you",t.stuck={question:"You stopped the squad. Nothing was lost; the work so far is kept.",options:[{do:"retry",name:"Carry on",note:"Run the stopped step again."},...wayOptions({}).slice(1)]},t.waiting={kind:"stuck",summary:"The squad stopped and needs you"},addActivity(t,"squad","Stopped: you stopped the squad. Nothing was lost; the work so far is kept.")}function watchDemoSlow(bp,tickets,at3=Date.now()){let usual=readUsual(bp);for(let t of tickets){if(!t.squad||t.waiting||t.stopRequested)continue;let q5=t.quietSnoozedUntil&&Date.parse(t.quietSnoozedUntil)>at3?null:slowStep(t,usual,at3);(q5?.detail??null)!==(t.quiet?.detail??null)&&updateTicket(bp,t.id,x=>{q5&&x.quiet?.kind!==q5.kind&&addActivity(x,"squad",`Taking long: ${q5.detail}`),q5?x.quiet=q5:delete x.quiet})}}function askedBefore(t){return t.activity.some(a=>a.who==="architect"&&a.text.startsWith("Asked you:"))}function cancelTicket(bp,t){let b=t.breakdown?readBreakdown(bp,t.breakdown.id):null,s=b?allStories(b).find(({story:story2})=>/cancel/i.test(story2.title)&&story2.key!==t.breakdown.item&&story2.ticket&&readTicket(bp,story2.ticket)):void 0;return s?{id:s.story.ticket,title:s.story.title}:null}function refundAnswer(bp,t){let choice=(t.breakdown?readBreakdown(bp,t.breakdown.id)?.questions.find(x=>x.id==="q1"&&/refund/i.test(x.text)):void 0)?.answer?.choice;return!choice||choice==="Other"?null:/48 hours/.test(choice)?"late-kept":"full"}function demoClash(bp,t){if(t.track!=="auto"||!/refund/i.test(t.title)||askedBefore(t))return!1;let earlier=cancelTicket(bp,t),answer2=refundAnswer(bp,t),text3=`Cancelling a hearing today keeps the court fee as credit for the lawyer's next booking. ${earlier?`That was built in ${earlier.id} (${earlier.title})`:"That was built in an earlier ticket"}. The BRD says the fee ${answer2==="late-kept"?"goes back to the card that paid, unless a party cancels less than 48 hours before":"goes back in full to the card that paid"}. Which one is right from now on?`,sources=[{text:"BRD 4.4 Refunds, page 3",section:"4.4",page:3},...answer2?[{text:"Your answer to question 1"}]:[],...earlier?[{text:`${earlier.id} ${earlier.title}`,ticket:earlier.id}]:[]];t.agentQuestion={from:"architect",text:text3,options:[`Refund to the card that paid, as the BRD says. Change how the portal refunds today ${SUGGESTED}`,"Keep the credit for the next booking. Update this story and the breakdown","Refund to the card for hearings booked from this release on. Keep the credit for the ones booked before"],why:"This changes where lawyers and parties get their money back, so the squad does not guess.",kind:"clash",sources,at:now()};let words2=askWords("Architect","clash");return t.live=!1,t.squad=null,t.status=words2.status,t.waiting={kind:"ask",summary:words2.summary},addActivity(t,"architect",`Read the code and found that cancelling a hearing keeps the fee as credit, as ${earlier?earlier.id:"an earlier ticket"} built it`),addActivity(t,"architect",`Asked you: ${text3}`),!0}function demoDecided(t,by2){let text3=/refund/i.test(t.title)?by2==="architect"?"The refund goes back to the card that paid.":"The lawyer gets an email and an SMS when the fee is refunded.":by2==="architect"?"It goes on the screens people already use, not on a new page.":"Each message on the screen says in one line what to do next.";addDecided(t,by2,[text3])}function startDemoSquad(bp,intervalMs=500){let busy=!1,timer=setInterval(()=>{if(!busy){busy=!0;try{tick(bp,{sends:!0})}catch{}finally{busy=!1}}},intervalMs);return()=>clearInterval(timer)}var lastSlowLook=new Map;function tick(bp,opts={}){if(!boardExists(bp))return;let meta=readMeta(bp);if(meta.engine!=="demo")return;playDemoSetup(bp,meta),playDemoBreakdowns(bp,meta),opts.sends&&startSends(bp,meta,demoSendJobs(bp)),playDemoQueue(bp);let at3=Date.now(),tickets=listTickets(bp);at3-(lastSlowLook.get(bp.root)??0)>=5e3&&(lastSlowLook.set(bp.root,at3),watchDemoSlow(bp,tickets,at3));for(let t of tickets){if(t.deleting){t.deleting.stopped||updateTicket(bp,t.id,x=>x.deleting&&(x.deleting.stopped=now()));continue}if(t.stopRequested){updateTicket(bp,t.id,x=>stopDemoStep(bp,x,meta.speed));continue}if(wakeIfReady(bp,t)||resumeAccept(bp,t))continue;let q5=t.questions?.find(x=>x.thinking);q5&&at3-lastMessageAt(q5)>=2200/meta.speed&&reply(bp,t.id,q5.id);let c=t.chat;c?.stopping?chatStopped(bp,t.id):c?.open&&c.thinking&&at3-Date.parse(c.messages.at(-1)?.at??t.activity[0]?.at??new Date(0).toISOString())>=2200/meta.speed&&chatReply(bp,t.id);let ch=openChange(t);ch?.status==="writing"&&at3-Date.parse(ch.at)>=2200/meta.speed&&writeChange(bp,t.id),t.squad&&Date.parse(t.squad.nextAt)<=at3&&(advance(bp,t.id,meta),updateTicket(bp,t.id,x=>keepDemoClock(bp,x)))}}function lastMessageAt(q5){let last=q5.thread?.[q5.thread.length-1];return last?Date.parse(last.at):0}function reply(bp,id2,qid){updateTicket(bp,id2,t=>{let q5=t.questions?.find(x=>x.id===qid);if(!q5||!q5.thinking)return;let script=scriptFor(t),text3=(q5.thread?.filter(m=>m.who==="you").length??0)<=1?script.replies[qid]??"Good question. If you are not sure, pick the closest choice and I will mark it in the plan.":"Then pick the choice closest to what you mean. I will show my reasoning in the plan, and you can change it there.";q5.thread=[...q5.thread??[],{who:q5.from,text:text3,at:now()}],q5.thinking=!1})}function chatStopped(bp,id2){updateTicket(bp,id2,t=>{let c=t.chat;c&&(c.thinking&&c.open&&c.messages.push({who:"squad",text:"You stopped this answer. Ask again, or carry on.",at:now(),note:"stopped"}),c.thinking=!1,c.stopping=void 0)})}function chatReply(bp,id2){updateTicket(bp,id2,t=>{let c=t.chat;if(!c?.open||!c.thinking)return;let said=c.messages.filter(m=>m.who==="you"),last=c.messages.at(-1),opening=last?.who!=="you",name=c.with[0].toUpperCase()+c.with.slice(1),released=t.stage==="release"||t.stage==="done",pages=proofPages(t),where=released?`${t.id} is ${t.stage==="done"?"released":"accepted"}, so I can only answer questions. To change it, add a new linked ticket or report a bug.`:waitsAtReview(t)?`${whereItStands(t)}. Nothing changes while we talk.${pages.length?` Where to look: ${pages.map(p=>`\`${p}\``).join(", ")}.`:""} ${t.waiting?.kind==="proof"?"To ask for a change, press **Something's wrong**.":"To go on, pick a way out on its card."}`:squadWorks(t)?`${t.id} keeps going while we talk. Right now: ${t.status}.`:`${whereItStands(t)}. Nothing changes while we talk.${pages.length?` Where to look: ${pages.map(p=>`\`${p}\``).join(", ")}.`:""} To change something, use its own buttons.`,text3=c.beside?opening?`I am the ${name} on ${t.id}. ${where} What would you like to know?`:`About **${last.text}**: ${where}`:opening?c.with==="analyst"?`I read "${t.title}". As I understand it: ${t.ask||"the ticket asks for a change"}.
1866
+ - **Fixed:** uploads stopped at 5 MB.`,addons},{version:"v2.4.0",status:"draft",from:"v2.3.1",tickets:["SK-147","SK-143"],notes:"",addons}]}function demoDecisions(user){return[{id:"ADR-011",title:"Use Angular Material for buttons and menus",why:"The client requires it across all ministry portals.",options:["Angular Material","Our own components"],chosen:"Angular Material",from:"Client's choice",approvedBy:user,date:daysAgo(20),checkedBy:{kind:"lint rule",name:"no-custom-buttons",passing:!0},tickets:["SK-131","SK-134"]},{id:"ADR-010",title:"Every list page can export to PDF",why:"Clerks print lists for court sessions every day.",options:["Export on some pages","Export on every list page"],chosen:"Export on every list page",from:"SK-147",approvedBy:user,date:daysAgo(2),checkedBy:{kind:"test",name:"list-pages-export.spec.ts",passing:!0},tickets:["SK-147"]},{id:"ADR-007",title:"Dates always show as day/month/year",why:"That is how dates are read in Saudi courts. A bug proved it matters.",options:["Follow the browser","Always day/month/year"],chosen:"Always day/month/year",from:"SK-112",approvedBy:user,date:daysAgo(40),checkedBy:{kind:"test",name:"date-format.spec.ts",passing:!0},tickets:["SK-112","SK-134"]},{id:"ADR-003",title:"The back end is a .NET 8 API",why:"Found by the Architect when it first read the project.",options:[".NET 8 API"],chosen:".NET 8 API",from:"Project scan",approvedBy:user,date:daysAgo(60),checkedBy:null,tickets:[]},{id:"ADR-002",title:"The front end is Angular 17",why:"Found by the Architect when it first read the project.",options:["Angular 17"],chosen:"Angular 17",from:"Project scan",approvedBy:user,date:daysAgo(60),checkedBy:null,tickets:[]}]}function demoMeta(user,speed,project){return{project,key:"SK",nextNumber:154,engine:"demo",user:{name:user,role:"Project manager"},roles:{analyst:"Claude",architect:"Claude",builder:"Codex",tester:"Codex",reviewer:"Claude"},tools:[{name:"Claude Code",ready:!0},{name:"Codex",ready:!0}],speed,map:[{area:"Front end",detail:"Angular 17 with Angular Material, in web/"},{area:"Back end",detail:".NET 8 API, in api/"},{area:"Database",detail:"SQL Server, migrations in api/Migrations/"},{area:"Tests",detail:"Jest for logic, Playwright for screens"},{area:"Code style",detail:"ESLint and Prettier rules in web/"},{area:"How to run",detail:"npm start in web/, dotnet run in api/"}],lessons:[{id:"L-003",text:"Dates must show as day/month/year. A test now guards it.",from:"SK-112",date:daysAgo(40)},{id:"L-002",text:"Big case lists need paging, or the page freezes.",from:"SK-121",date:daysAgo(50)},{id:"L-001",text:"Always check the Arabic layout, right to left, before the proof.",from:"SK-104",date:daysAgo(58)}]}}function demoAssetsDir(){let candidates=[fileURLToPath5(new URL("../templates/demo/proof/",import.meta.url)),fileURLToPath5(new URL("../../templates/demo/proof/",import.meta.url))];return candidates.find(c=>fs151.existsSync(c))??candidates[0]}var SAMPLE_SIZE={"SK-138":.8,"SK-141":.45,"SK-147":1.3};function demoFolder(){return path142.join(squadHome(),"demo")}function seedDemoBoard(bp,opts={}){clearBoard(bp),clearDemoRequirements(bp);let user=gitUserName(),speed=opts.speed??1,meta=demoMeta(user,speed,opts.project??"Case portal");if(opts.firstRun){writeMeta(bp,{...meta,nextNumber:1,lessons:[]}),seedFreshSetup(bp);return}writeMeta(bp,meta);for(let t of demoTickets(user))t.live&&t.stage!=="done"&&!t.waiting&&scheduleSquad(t,"ambient",3+Math.random()*4,speed,0),t.history?.length&&countSampleHistory(bp,t,SAMPLE_SIZE[t.id]??1),writeTicket(bp,{...t,owner:user});for(let r of demoReleases())writeRelease(bp,r);for(let d of demoDecisions(user))writeDecision(bp,d);seedDemoRequirements(bp,user);let assets=demoAssetsDir();if(fs151.mkdirSync(bp.proofDir,{recursive:!0}),fs151.existsSync(assets))for(let f of fs151.readdirSync(assets))fs151.copyFileSync(path142.join(assets,f),path142.join(bp.proofDir,f));seedDoneSetup(bp,user,daysAgo(60))}import fs152 from"fs";import path143 from"path";function demoStep(t){let s=t.squad;if(!s||t.waiting)return null;let doing=(t.steps??[]).findIndex(x=>x.status==="doing"),open3=(t.steps??[]).findIndex(x=>x.status!=="done"),part=doing>=0?doing+1:open3>=0?open3+1:void 0;switch(s.phase){case"analyst-read":return t.track==="express"||t.track==="auto"?null:{who:"analyst",step:"questions"};case"plan-write":case"spec-rewrite":case"plan-rewrite":return s.phase==="spec-rewrite"||s.phase==="plan-write"&&needsSpec(t)?{who:"analyst",step:"spec"}:{who:"architect",step:"design"};case"chat-spec":return{who:"analyst",step:"spec"};case"chat-design":case"answered-architect":return{who:"architect",step:"design"};case"build":case"unstick":case"answered-builder":case"chat-carry-builder":return t.stage==="implement"&&part?{who:"builder",step:"build",part}:{who:"builder",step:"build"};case"fix":return{who:"builder",step:"fix"};case"test":return/^(Recording|Checking the change)/.test(t.status)?{who:"tester",step:"proof"}:{who:"squad",step:"checks"};case"review":case"answered-reviewer":case"chat-carry-reviewer":return/^Builder/.test(t.status)?{who:"builder",step:"fix"}:{who:"reviewer",step:"review"};case"chat-carry-tester":return{who:"tester",step:"proof"};case"ambient":return t.stage==="tickets"?{who:"analyst",step:"questions"}:t.stage==="implement"?{who:"builder",step:"build",...part?{part}:{}}:t.stage==="test"?{who:"squad",step:"checks"}:{who:"reviewer",step:"review"};default:return null}}function endDemoStep(bp,t){let c=t.clock;c&&countSampleStep(bp,t,c.who,c.step,readMeta(bp).quality),endStep(bp,t)}function keepDemoClock(bp,t){let next=demoStep(t),c=t.clock;if(!next){c&&endDemoStep(bp,t);return}c&&(c.who!==next.who||c.step!==next.step)&&endDemoStep(bp,t),t.clock&&next.part&&nextPart(bp,t,next.part),t.clock||startStep(t,next);let line3=/^Step \d+ of \d+/.test(t.status)?t.steps?.find(s=>s.status==="doing")?.live??"":t.status;line3&&line3!==t.clock?.doing&&stepDoing(t,line3)}function stopDemoStep(bp,t,speed){if(t.stopRequested=void 0,delete t.quiet,t.retryOnStop){t.retryOnStop=void 0;let limit=t.clock?.limit;delete t.clock,t.live=!0,addActivity(t,"squad","Stopped the step and started it again, with the work so far"),t.squad&&(t.squad.nextAt=new Date(Date.now()+1e3/Math.max(speed,.1)).toISOString()),keepDemoClock(bp,t);let fresh=t.clock;fresh&&limit&&(fresh.limit=limit),t.status="Trying the step again";return}delete t.clock,t.live=!1,t.retryPhase=t.squad?.phase,t.squad=null,t.status="Stopped, needs you",t.stuck={question:"You stopped the squad. Nothing was lost; the work so far is kept.",options:[{do:"retry",name:"Carry on",note:"Run the stopped step again."},...wayOptions({}).slice(1)]},t.waiting={kind:"stuck",summary:"The squad stopped and needs you"},addActivity(t,"squad","Stopped: you stopped the squad. Nothing was lost; the work so far is kept.")}function watchDemoSlow(bp,tickets,at3=Date.now()){let usual=readUsual(bp);for(let t of tickets){if(!t.squad||t.waiting||t.stopRequested)continue;let q5=t.quietSnoozedUntil&&Date.parse(t.quietSnoozedUntil)>at3?null:slowStep(t,usual,at3);(q5?.detail??null)!==(t.quiet?.detail??null)&&updateTicket(bp,t.id,x=>{q5&&x.quiet?.kind!==q5.kind&&addActivity(x,"squad",slowActivity(x,q5,at3)),q5?x.quiet=q5:delete x.quiet})}}function askedBefore(t){return t.activity.some(a=>a.who==="architect"&&a.text.startsWith("Asked you:"))}function cancelTicket(bp,t){let b=t.breakdown?readBreakdown(bp,t.breakdown.id):null,s=b?allStories(b).find(({story:story2})=>/cancel/i.test(story2.title)&&story2.key!==t.breakdown.item&&story2.ticket&&readTicket(bp,story2.ticket)):void 0;return s?{id:s.story.ticket,title:s.story.title}:null}function refundAnswer(bp,t){let choice=(t.breakdown?readBreakdown(bp,t.breakdown.id)?.questions.find(x=>x.id==="q1"&&/refund/i.test(x.text)):void 0)?.answer?.choice;return!choice||choice==="Other"?null:/48 hours/.test(choice)?"late-kept":"full"}function demoClash(bp,t){if(t.track!=="auto"||!/refund/i.test(t.title)||askedBefore(t))return!1;let earlier=cancelTicket(bp,t),answer2=refundAnswer(bp,t),text3=`Cancelling a hearing today keeps the court fee as credit for the lawyer's next booking. ${earlier?`That was built in ${earlier.id} (${earlier.title})`:"That was built in an earlier ticket"}. The BRD says the fee ${answer2==="late-kept"?"goes back to the card that paid, unless a party cancels less than 48 hours before":"goes back in full to the card that paid"}. Which one is right from now on?`,sources=[{text:"BRD 4.4 Refunds, page 3",section:"4.4",page:3},...answer2?[{text:"Your answer to question 1"}]:[],...earlier?[{text:`${earlier.id} ${earlier.title}`,ticket:earlier.id}]:[]];t.agentQuestion={from:"architect",text:text3,options:[`Refund to the card that paid, as the BRD says. Change how the portal refunds today ${SUGGESTED}`,"Keep the credit for the next booking. Update this story and the breakdown","Refund to the card for hearings booked from this release on. Keep the credit for the ones booked before"],why:"This changes where lawyers and parties get their money back, so the squad does not guess.",kind:"clash",sources,at:now()};let words2=askWords("Architect","clash");return t.live=!1,t.squad=null,t.status=words2.status,t.waiting={kind:"ask",summary:words2.summary},addActivity(t,"architect",`Read the code and found that cancelling a hearing keeps the fee as credit, as ${earlier?earlier.id:"an earlier ticket"} built it`),addActivity(t,"architect",`Asked you: ${text3}`),!0}function demoDecided(t,by2){let text3=/refund/i.test(t.title)?by2==="architect"?"The refund goes back to the card that paid.":"The lawyer gets an email and an SMS when the fee is refunded.":by2==="architect"?"It goes on the screens people already use, not on a new page.":"Each message on the screen says in one line what to do next.";addDecided(t,by2,[text3])}function startDemoSquad(bp,intervalMs=500){let busy=!1,timer=setInterval(()=>{if(!busy){busy=!0;try{tick(bp,{sends:!0})}catch{}finally{busy=!1}}},intervalMs);return()=>clearInterval(timer)}var lastSlowLook=new Map;function tick(bp,opts={}){if(!boardExists(bp))return;let meta=readMeta(bp);if(meta.engine!=="demo")return;playDemoSetup(bp,meta),playDemoBreakdowns(bp,meta),opts.sends&&startSends(bp,meta,demoSendJobs(bp)),playDemoQueue(bp);let at3=Date.now(),tickets=listTickets(bp);at3-(lastSlowLook.get(bp.root)??0)>=5e3&&(lastSlowLook.set(bp.root,at3),watchDemoSlow(bp,tickets,at3));for(let t of tickets){if(t.deleting){t.deleting.stopped||updateTicket(bp,t.id,x=>x.deleting&&(x.deleting.stopped=now()));continue}if(t.stopRequested){updateTicket(bp,t.id,x=>stopDemoStep(bp,x,meta.speed));continue}if(wakeIfReady(bp,t)||resumeAccept(bp,t))continue;let q5=t.questions?.find(x=>x.thinking);q5&&at3-lastMessageAt(q5)>=2200/meta.speed&&reply(bp,t.id,q5.id);let c=t.chat;c?.stopping?chatStopped(bp,t.id):c?.open&&c.thinking&&at3-Date.parse(c.messages.at(-1)?.at??t.activity[0]?.at??new Date(0).toISOString())>=2200/meta.speed&&chatReply(bp,t.id);let ch=openChange(t);ch?.status==="writing"&&at3-Date.parse(ch.at)>=2200/meta.speed&&writeChange(bp,t.id),t.squad&&Date.parse(t.squad.nextAt)<=at3&&(advance(bp,t.id,meta),updateTicket(bp,t.id,x=>keepDemoClock(bp,x)))}}function lastMessageAt(q5){let last=q5.thread?.[q5.thread.length-1];return last?Date.parse(last.at):0}function reply(bp,id2,qid){updateTicket(bp,id2,t=>{let q5=t.questions?.find(x=>x.id===qid);if(!q5||!q5.thinking)return;let script=scriptFor(t),text3=(q5.thread?.filter(m=>m.who==="you").length??0)<=1?script.replies[qid]??"Good question. If you are not sure, pick the closest choice and I will mark it in the plan.":"Then pick the choice closest to what you mean. I will show my reasoning in the plan, and you can change it there.";q5.thread=[...q5.thread??[],{who:q5.from,text:text3,at:now()}],q5.thinking=!1})}function chatStopped(bp,id2){updateTicket(bp,id2,t=>{let c=t.chat;c&&(c.thinking&&c.open&&c.messages.push({who:"squad",text:"You stopped this answer. Ask again, or carry on.",at:now(),note:"stopped"}),c.thinking=!1,c.stopping=void 0)})}function chatReply(bp,id2){updateTicket(bp,id2,t=>{let c=t.chat;if(!c?.open||!c.thinking)return;let said=c.messages.filter(m=>m.who==="you"),last=c.messages.at(-1),opening=last?.who!=="you",name=c.with[0].toUpperCase()+c.with.slice(1),released=t.stage==="release"||t.stage==="done",pages=proofPages(t),where=released?`${t.id} is ${t.stage==="done"?"released":"accepted"}, so I can only answer questions. To change it, add a new linked ticket or report a bug.`:waitsAtReview(t)?`${whereItStands(t)}. Nothing changes while we talk.${pages.length?` Where to look: ${pages.map(p=>`\`${p}\``).join(", ")}.`:""} ${t.waiting?.kind==="proof"?"To ask for a change, press **Something's wrong**.":"To go on, pick a way out on its card."}`:squadWorks(t)?`${t.id} keeps going while we talk. Right now: ${t.status}.`:`${whereItStands(t)}. Nothing changes while we talk.${pages.length?` Where to look: ${pages.map(p=>`\`${p}\``).join(", ")}.`:""} To change something, use its own buttons.`,text3=c.beside?opening?`I am the ${name} on ${t.id}. ${where} What would you like to know?`:`About **${last.text}**: ${where}`:opening?c.with==="analyst"?`I read "${t.title}". As I understand it: ${t.ask||"the ticket asks for a change"}.
1867
1867
 
1868
1868
  Two things are not clear to me yet:
1869
1869
  1. **Who** uses this most?
@@ -1884,8 +1884,8 @@ body.dark ul{background:#0004}
1884
1884
  <main><div class="card"><small>Open cases</small><h3>128</h3></div><div class="card"><small>Hearings today</small><h3>14</h3></div><div class="card"><small>Waiting on documents</small><h3>9</h3></div></main>
1885
1885
  <ul>${rows2}</ul>
1886
1886
  </body></html>`),file3}import fs153 from"fs";import{z as z6}from"zod";import path144 from"path";function isInside3(abs,parent){let a=path144.resolve(abs)+path144.sep,p=path144.resolve(parent)+path144.sep;return a.startsWith(p)}var CreateBody=z6.object({feature:z6.string().min(1).max(80),title:z6.string().max(200).optional(),trackerId:z6.string().max(80).optional()}),PatchBody=z6.object({intakeContent:z6.string().min(1).max(1e6)});function safeFeatureParam(feature){return feature.length>0&&!feature.includes("/")&&!feature.includes("..")&&slugify(feature)===feature}function mountStoriesApi(app,opts){app.get("/api/stories",c=>{let feature=c.req.query("feature")??void 0,stories=listStories(opts.paths,{feature});return c.json(stories.map(s=>({feature:s.feature,id:s.id,intakePath:s.intakePath,storyDir:s.storyDir,planFile:s.planFile??null,titleHint:s.titleHint??null})))}),app.get("/api/stories/:feature/:id",c=>{let{feature,id:id2}=c.req.param();if(!safeFeatureParam(feature))return c.json({error:"forbidden"},403);let hit=listStories(opts.paths,{feature}).find(s=>s.id===id2);if(!hit)return c.json({error:"not_found"},404);if(!isInside3(hit.intakePath,opts.paths.storiesDir))return c.json({error:"forbidden"},403);let intakeContent="";try{intakeContent=fs153.readFileSync(hit.intakePath,"utf8")}catch{}return c.json({feature:hit.feature,id:hit.id,intakePath:hit.intakePath,storyDir:hit.storyDir,planFile:hit.planFile??null,titleHint:hit.titleHint??null,intakeContent})}),app.post("/api/stories",async c=>{let body10=CreateBody.safeParse(await c.req.json());if(!body10.success)return c.json({error:"invalid_body",issues:body10.error.issues},400);let cfg=loadConfig(opts.paths.configFile),created;try{created=createStoryRecord({paths:opts.paths,config:cfg,feature:body10.data.feature,title:body10.data.title,trackerId:body10.data.trackerId})}catch(e){let msg=e instanceof Error?e.message:String(e);return/already exists|Intake already exists/.test(msg)?c.json({error:"conflict",message:msg},409):c.json({error:"invalid_request",message:msg},400)}return c.json(created,201)}),app.patch("/api/stories/:feature/:id",async c=>{let body10=PatchBody.safeParse(await c.req.json());if(!body10.success)return c.json({error:"invalid_body",issues:body10.error.issues},400);let{feature,id:id2}=c.req.param();if(!safeFeatureParam(feature))return c.json({error:"forbidden"},403);let hit=listStories(opts.paths,{feature}).find(s=>s.id===id2);return hit?isInside3(hit.intakePath,opts.paths.storiesDir)?(fs153.writeFileSync(hit.intakePath,body10.data.intakeContent,"utf8"),c.json({ok:!0})):c.json({error:"forbidden"},403):c.json({error:"not_found"},404)}),app.delete("/api/stories/:feature/:id",c=>{let trash=c.req.query("trash")==="1",{feature,id:id2}=c.req.param();if(!safeFeatureParam(feature))return c.json({error:"forbidden"},403);let hit=listStories(opts.paths,{feature}).find(s=>s.id===id2);if(!hit)return c.json({error:"not_found"},404);if(!isInside3(hit.storyDir,opts.paths.storiesDir))return c.json({error:"forbidden"},403);let result=deleteStoryRecord({paths:opts.paths,story:hit,trash});return c.json(result)})}import fs154 from"fs";import path145 from"path";import{z as z7}from"zod";import{diffLines}from"diff";var DiffQuery=z7.object({feature:z7.string().min(1).max(80),a:z7.string().min(1).max(200),b:z7.string().min(1).max(200)});function safeFeatureParam2(feature){return feature.length>0&&!feature.includes("/")&&!feature.includes("..")&&slugify(feature)===feature}function mountPlansApi(app,opts){app.get("/api/plans",c=>{let out20=[];for(let feature of listPlanFeatureNames(opts.paths)){let featureDir=path145.join(opts.paths.plansDir,feature);if(fs154.existsSync(featureDir))for(let entry of fs154.readdirSync(featureDir)){if(!entry.endsWith(".md")||entry==="00-index.md")continue;let abs=path145.join(featureDir,entry);out20.push({feature,planFile:entry,metadata:readPlanMetadata(abs)})}}return c.json(out20)}),app.get("/api/plans/:feature/:planFile",c=>{let{feature,planFile}=c.req.param();if(!safeFeatureParam2(feature))return c.json({error:"forbidden"},403);let abs=path145.resolve(path145.join(opts.paths.plansDir,feature,planFile));if(!isInside3(abs,opts.paths.plansDir))return c.json({error:"forbidden"},403);if(!fs154.existsSync(abs))return c.json({error:"not_found"},404);let content=fs154.readFileSync(abs,"utf8");return c.json({feature,planFile,content,absPath:abs,metadata:readPlanMetadata(abs)})}),app.get("/api/plans/diff",c=>{let parse=DiffQuery.safeParse({feature:c.req.query("feature"),a:c.req.query("a"),b:c.req.query("b")});if(!parse.success)return c.json({error:"invalid_query",issues:parse.error.issues},400);let{feature,a,b}=parse.data;if(!safeFeatureParam2(feature))return c.json({error:"forbidden"},403);let featureDir=path145.join(opts.paths.plansDir,feature),aAbs=path145.join(featureDir,a),bAbs=path145.join(featureDir,b);if(!isInside3(aAbs,opts.paths.plansDir)||!isInside3(bAbs,opts.paths.plansDir))return c.json({error:"forbidden"},403);if(!fs154.existsSync(aAbs)||!fs154.existsSync(bAbs))return c.json({error:"not_found"},404);let aText=fs154.readFileSync(aAbs,"utf8"),bText=fs154.readFileSync(bAbs,"utf8"),changes=diffLines(aText,bText);return c.json({feature,a,b,changes})}),app.delete("/api/plans/:feature/:planFile",c=>{let trash=c.req.query("trash")==="1",{feature,planFile}=c.req.param();if(!safeFeatureParam2(feature))return c.json({error:"forbidden"},403);let abs=path145.join(opts.paths.plansDir,feature,planFile);if(!isInside3(abs,opts.paths.plansDir))return c.json({error:"forbidden"},403);if(!fs154.existsSync(abs))return c.json({error:"not_found"},404);if(trash){let result=trashOrDelete([abs],opts.paths.trashDir,!0);return removeOverviewRowForPlanFile(path145.join(opts.paths.plansDir,feature),planFile),c.json({ok:!0,trashed:result.trashed})}return fs154.rmSync(abs),removeOverviewRowForPlanFile(path145.join(opts.paths.plansDir,feature),planFile),c.json({ok:!0})})}import{z as z8}from"zod";var ConfigBody=z8.object({}).passthrough();function mountConfigApi(app,opts){app.get("/api/config",c=>{let config2=loadConfig(opts.paths.configFile);return c.json(config2)}),app.put("/api/config",async c=>{let body10=ConfigBody.safeParse(await c.req.json());if(!body10.success)return c.json({error:"invalid_body",issues:body10.error.issues},400);try{return saveConfig(opts.paths.configFile,body10.data),c.json({ok:!0})}catch(err2){return c.json({error:"invalid_config",detail:err2.message},400)}})}function mountMetaApi(app,opts){app.get("/api/meta",async c=>{let config2=loadConfig(opts.paths.configFile),lastRun=await readLastRun(opts.paths);return c.json({version:readInstalledPackage().version,root:opts.paths.root,project:config2.project,planner:config2.planner?{provider:config2.planner.provider,enabled:config2.planner.enabled}:null,tracker:{type:config2.tracker.type},lastRun})}),app.get("/api/last-run",async c=>{let lastRun=await readLastRun(opts.paths);return c.json(lastRun??null)})}import{streamSSE}from"hono/streaming";import{z as z9}from"zod";import path146 from"path";import fs155 from"fs";var active=new Map,EVENT_BUF_CAP=4096,StartBody=z9.object({feature:z9.string().min(1),storyId:z9.string().min(1)});function emptyStats(durationMs){return{turns:0,inputTokens:0,outputTokens:0,cacheCreationTokens:0,cacheReadTokens:0,cacheHitRatio:0,durationMs}}function mountRunsApi(app,opts){app.get("/api/runs",async c=>c.json(await listRuns(opts.paths))),app.get("/api/runs/active",c=>{let rows2=[...active.values()].map(r=>({runId:r.runId,feature:r.feature,storyId:r.storyId,startedAt:r.startedAt}));return c.json(rows2)}),app.post("/api/runs",async c=>{let body10=StartBody.safeParse(await c.req.json());if(!body10.success)return c.json({error:"invalid_body",issues:body10.error.issues},400);let cfg=loadConfig(opts.paths.configFile);if(!cfg.planner?.enabled)return c.json({error:"planner_disabled",detail:"Run `squad config set planner` first."},400);let auth2;try{auth2=resolvePlannerAuthForPaths(opts.paths,cfg.planner.provider,cfg.planner)}catch(err2){if(err2 instanceof PlannerAuthUnavailableError)return c.json({error:"auth_unavailable",mode:cfg.planner.auth?.anthropic??"auto",detail:err2.message},400);throw err2}let story2=listStories(opts.paths,{feature:body10.data.feature}).find(s=>s.id===body10.data.storyId);if(!story2)return c.json({error:"not_found"},404);let runId=newRunId(),eventBuffer=[],bus=new PlannerEventBus;bus.subscribe(e=>{eventBuffer.push(e),eventBuffer.length>EVENT_BUF_CAP&&eventBuffer.splice(0,eventBuffer.length-EVENT_BUF_CAP)});let controller=new AbortController,startedAt=Date.now(),done2=(async()=>{let cfgFresh=loadConfig(opts.paths.configFile),planner=cfgFresh.planner,modelId=modelFor(planner.provider,"plan",planner.modelOverride),anthropicRuntimeChoice=planner.runtime?.anthropic??"agent-sdk",draftRuntime=resolveRuntime({provider:planner.provider,modelId,auth:auth2,anthropicRuntime:planner.provider==="anthropic"?anthropicRuntimeChoice:void 0}),anthropicProviderSpecific=planner.provider==="anthropic"?{draft:extractAnthropicProviderSpecific(planner,"draft"),scout:extractAnthropicProviderSpecific(planner,"scout")}:void 0,budget=new Budget(planner.budget),cacheEnabled=planner.cache?.enabled??!0,scoutEnabled=planner.stages?.scout?.enabled!==!1,validationEnabled=planner.validation?.enabled!==!1,strictValidation=planner.validation?.strict===!0,maxScoutFiles=planner.stages?.scout?.maxFiles??12,scoutModelId="",scoutRuntime,scoutSystemPrompt;scoutEnabled&&(scoutModelId=modelFor(planner.provider,"scout",planner.modelOverride,planner.stages?.scout?.modelOverride),scoutRuntime=resolveRuntime({provider:planner.provider,modelId:scoutModelId,auth:auth2,anthropicRuntime:planner.provider==="anthropic"?anthropicRuntimeChoice:void 0}));let repoMap=buildRepoMap(opts.paths.root,{format:"tree"});scoutEnabled&&(scoutSystemPrompt=composeScoutSystemPrompt({projectRoots:cfgFresh.project.projectRoots??["."],primaryLanguage:cfgFresh.project.primaryLanguage??"",trackerType:cfgFresh.tracker.type,repoMap,plansIndex:buildPlansIndex(opts.paths)}));let systemPrompt=composeSystemPrompt({projectRoots:cfgFresh.project.projectRoots??["."],primaryLanguage:cfgFresh.project.primaryLanguage??"",trackerType:cfgFresh.tracker.type,repoMap}),userPrompt=composeUserPrompt({intakeContent:fs155.readFileSync(story2.intakePath,"utf8")}),result;try{try{result=await runPlanner({root:opts.paths.root,runtime:draftRuntime,provider:planner.provider,modelId,auth:auth2,anthropicProviderSpecific,systemPrompt,userPrompt,budget,maxOutputTokens:planner.maxOutputTokens,cacheEnabled,events:bus,runId,paths:opts.paths,abort:controller.signal,stages:{scout:{enabled:scoutEnabled,runtime:scoutRuntime,modelId:scoutModelId,maxFiles:maxScoutFiles,maxOutputTokens:planner.stages?.scout?.maxOutputTokens??2048}},validation:{enabled:validationEnabled,strict:strictValidation},toolsEnabled:planner.tools,scoutSystemPrompt})}catch(err2){let durationMs=Date.now()-startedAt,last=eventBuffer[eventBuffer.length-1];last?.kind==="rate_limit"&&last.phase==="aborted"||bus.emit({kind:"error",runId,message:err2.message}),bus.emit({kind:"done",runId,success:!1,planFile:null,partial:!0,stats:emptyStats(durationMs),durationMs});return}let issueCount=result.validation?.issues.length??0,issuesByKind=summariseIssuesByKind(result.validation?.issues??[]),validationBlocks=strictValidation&&validationEnabled&&issueCount>0,runSuccess=result.finishedNormally&&!result.timedOut&&!result.userCancelled&&!validationBlocks,planFile=null;if(result.planText.trim()){let snap=budget.snapshot(),header=buildMetadataHeader({provider:planner.provider,model:modelId,reads:snap.reads,bytes:snap.bytes,inputTokens:snap.usage.inputTokens,outputTokens:snap.usage.outputTokens,durationMs:Date.now()-startedAt,planStatus:runSuccess?void 0:"partial",scoutEnabled,validationEnabled,validationIssueCount:issueCount}),out20=writePlanFile({paths:opts.paths,config:cfgFresh,story:story2,planBodyMarkdown:result.planText,metadataHeader:header,partial:!runSuccess});planFile=path146.relative(opts.paths.root,out20.planFile)}try{await writeLastRun(opts.paths,{stats:result.stats,completedAt:new Date().toISOString(),provider:planner.provider,model:modelId}),await appendRun(opts.paths,{runId,provider:planner.provider,model:modelId,feature:story2.feature,storyId:story2.id,startedAt:new Date(startedAt).toISOString(),completedAt:new Date().toISOString(),success:runSuccess,partial:!runSuccess,planFile,stats:result.stats,cacheEnabled,durationMs:Date.now()-startedAt,scout:{enabled:scoutEnabled,selectedCount:result.scout?.selected.length,tokensUsed:result.scout?.tokensUsed,durationMs:result.scout?.durationMs},validation:{enabled:validationEnabled,issuesCount:issueCount,issuesByKind,durationMs:result.validation?.durationMs},plannerRuntime:{kind:draftRuntime.kind,provider:planner.provider},authMode:describeAuth(auth2).mode,providerOptionsSnapshot:anthropicProviderSpecific?{anthropic:anthropicProviderSpecific}:void 0})}catch{}bus.emit({kind:"done",runId,success:runSuccess,planFile,partial:!runSuccess,stats:result.stats,durationMs:Date.now()-startedAt})}finally{await bus.finalizeEventPersistence?.()}})().finally(()=>{active.delete(runId)});return active.set(runId,{runId,bus,controller,feature:story2.feature,storyId:story2.id,startedAt,done:done2,eventBuffer}),c.json({runId,eventStream:`/api/runs/${runId}/stream`},202)}),app.get("/api/runs/:runId/events",async c=>{let runId=c.req.param("runId"),fromQ=Number(c.req.query("from")??0),fromIndex=Number.isFinite(fromQ)?Math.max(0,Math.floor(fromQ)):0,limQ=Number(c.req.query("limit")??1e3),limit=Math.min(2e3,Math.max(1,Number.isFinite(limQ)?Math.floor(limQ):1e3)),reader=await openRunEventsReader(opts.paths,runId);if(!reader)return c.json({error:"not_found"},404);let events=[];for await(let e of reader.iterate({fromIndex,limit}))events.push(e);let total=await reader.count();return c.json({runId,fromIndex,limit,total,events})}),app.get("/api/runs/:runId",async c=>{let runId=c.req.param("runId"),rec=(await listRuns(opts.paths)).find(r=>r.runId===runId);return rec?c.json(rec):c.json({error:"not_found"},404)}),app.get("/api/runs/:runId/stream",c=>{let runId=c.req.param("runId"),run7=active.get(runId);return run7?streamSSE(c,async stream=>{let idx=0,resolveWaiter=null,notify=()=>resolveWaiter?.(),unsubscribe=run7.bus.subscribe(()=>notify()),ka=setInterval(()=>stream.writeSSE({event:"ping",data:"{}"}).catch(()=>{}),15e3),closed=!1;stream.onAbort(()=>{closed=!0,unsubscribe(),clearInterval(ka)});try{for(;!closed;){for(;idx<run7.eventBuffer.length;){let e=run7.eventBuffer[idx++];if(await stream.writeSSE({event:e.kind,data:JSON.stringify(e)}),e.kind==="done"){await stream.writeSSE({event:"closed",data:JSON.stringify({ok:!0})}),clearInterval(ka),unsubscribe();return}}await new Promise(resolve2=>{resolveWaiter=resolve2}),resolveWaiter=null}}finally{clearInterval(ka),unsubscribe()}}):streamSSE(c,async stream=>{let reader=await openRunEventsReader(opts.paths,runId);if(!reader){await stream.writeSSE({event:"error",data:JSON.stringify({message:"unknown run"})}),await stream.writeSSE({event:"closed",data:"{}"});return}try{for await(let e of reader.iterate())await stream.writeSSE({event:e.kind,data:JSON.stringify(e)});await stream.writeSSE({event:"closed",data:"{}"})}catch{await stream.writeSSE({event:"error",data:JSON.stringify({message:"replay_failed"})}),await stream.writeSSE({event:"closed",data:"{}"})}})}),app.delete("/api/runs/:runId",c=>{let runId=c.req.param("runId"),run7=active.get(runId);return run7?(run7.controller.abort(),c.json({ok:!0})):c.json({error:"not_found"},404)})}import{z as z10}from"zod";function maskToken(t){return t?t.length<=8?"\u2022\u2022\u2022\u2022"+t.slice(-2):t.slice(0,4)+"\u2026"+t.slice(-4):null}var PROVIDERS=["anthropic","openai","google"];function mountSecretsApi(app,opts){app.get("/api/secrets",c=>{let s=loadSecrets(opts.paths.secretsFile);return c.json({planner:{anthropic:maskToken(s.planner?.anthropic),openai:maskToken(s.planner?.openai),google:maskToken(s.planner?.google),anthropicOauthToken:maskToken(s.planner?.anthropicOauthToken)},tracker:{jira:{host:s.tracker?.jira?.host??null,email:s.tracker?.jira?.email??null,token:maskToken(s.tracker?.jira?.token)},azure:{organization:s.tracker?.azure?.organization??null,project:s.tracker?.azure?.project??null,pat:maskToken(s.tracker?.azure?.pat)},github:{host:s.tracker?.github?.host??null,pat:maskToken(s.tracker?.github?.pat)}}})});let PutBody=z10.object({planner:z10.object({anthropic:z10.string().optional(),openai:z10.string().optional(),google:z10.string().optional(),anthropicOauthToken:z10.string().optional()}).optional(),tracker:z10.object({jira:z10.object({host:z10.string().optional(),email:z10.string().optional(),token:z10.string().optional()}).optional(),azure:z10.object({organization:z10.string().optional(),project:z10.string().optional(),pat:z10.string().optional()}).optional(),github:z10.object({host:z10.string().optional(),pat:z10.string().optional()}).optional()}).optional()});app.put("/api/secrets",async c=>{let parse=PutBody.safeParse(await c.req.json());if(!parse.success)return c.json({error:"invalid_body",issues:parse.error.issues},400);let current=loadSecrets(opts.paths.secretsFile),merged2=mergeSecrets(current,parse.data);return parse.data.planner?.anthropicOauthToken===""&&merged2.planner&&delete merged2.planner.anthropicOauthToken,saveSecrets(opts.paths.secretsFile,merged2),c.json({ok:!0})});for(let p of PROVIDERS)app.post(`/api/secrets/test/${p}`,async c=>{let key=loadSecrets(opts.paths.secretsFile).planner?.[p];if(!key)return c.json({ok:!1,detail:`${p} key not saved`},400);let result=await fetchProviderModelIds(p,key);return result.ok?c.json({ok:!0,modelCount:result.ids.size}):c.json({ok:!1,status:result.status,detail:result.body.slice(0,200)},200)});app.post("/api/secrets/test/jira",async c=>{let cfg=loadConfig(opts.paths.configFile),s=loadSecrets(opts.paths.secretsFile);return c.json(await probeJiraConnectivity(s,cfg))}),app.post("/api/secrets/test/azure",async c=>{let cfg=loadConfig(opts.paths.configFile),s=loadSecrets(opts.paths.secretsFile);return c.json(await probeAzureConnectivity(s,cfg))}),app.post("/api/secrets/test/github",async c=>{let cfg=loadConfig(opts.paths.configFile),s=loadSecrets(opts.paths.secretsFile);return c.json(await probeGitHubConnectivity(s,cfg))})}import{z as z11}from"zod";async function runAuthStatus(opts={}){let paths=buildPaths(requireSquadRoot()),planner=loadConfig(paths.configFile).planner,provider=planner?.provider??"anthropic",auth2=resolvePlannerAuthForCwd(provider,planner),described=describeAuth(auth2),probe=shouldProbe(opts,provider)?await probeClaudeAuth(auth2):void 0,payload=buildPayload2({auth:described,provider,planner,probe,fallbackLoggedIn:credentialPresent(auth2)});if(opts.json){process.stdout.write(`${JSON.stringify(payload,null,2)}
1887
- `);return}let kw2=15;if(step("squad-kit auth"),blank(),kv("auth mode",`${payload.mode} (${payload.reason})`,kw2),kv("credential",payload.credentialHint,kw2),payload.account){let account=[payload.account.email,payload.account.organization,payload.account.subscriptionType].filter(Boolean).join(" \xB7 ");account&&kv("account",account,kw2)}payload.apiKeySource&&kv("api key source",payload.apiKeySource,kw2),kv("runtime",payload.runtime,kw2),probe&&!probe.ok?(blank(),warning(`Live check failed (${probe.kind}).`),info(probe.detail),(probe.kind==="not-logged-in"||probe.kind==="expired")&&info("Run `squad auth login` to sign in again.")):probe||(blank(),info("Account details skipped (offline). Re-run without --offline to check the live login."))}function shouldProbe(opts,provider){return opts.offline||skipExternalProbesInAutomation()?!1:provider==="anthropic"}function buildPayload2(input7){let{auth:auth2,provider,planner,probe}=input7,payload={mode:auth2.mode,reason:authReasonText(auth2.reason,provider),credentialHint:auth2.credentialHint,runtime:runtimeNameFor(provider,planner),loggedIn:probe?probe.ok:input7.fallbackLoggedIn};return probe?.ok&&(probe.account&&(payload.account=probe.account),probe.apiKeySource&&(payload.apiKeySource=probe.apiKeySource)),payload}var PLANNER_AUTH_LOGIN_COMMAND="squad auth login",PROBE_CACHE_TTL_MS=6e4,probeCache;function probeCacheKey(auth2){let described=describeAuth(auth2);return`${described.mode}:${described.credentialHint}`}async function probeWithCache(auth2,now3){let key=probeCacheKey(auth2);if(probeCache&&probeCache.key===key&&now3-probeCache.at<PROBE_CACHE_TTL_MS)return probeCache.result;let result=await probeClaudeAuth(auth2);return result.ok&&(probeCache={key,at:now3,result}),result}function resetPlannerAuthProbeCache(){probeCache=void 0}function subscriptionRuntimeConflict(provider,planner){return provider!=="anthropic"?plannerAuthRuntimeMismatchMessage(provider):runtimeNameFor(provider,planner)==="vercel"?plannerAuthRuntimeMismatchMessage(provider):null}function mountPlannerAuthApi(app,opts){app.get("/api/planner-auth",async c=>{let planner=loadConfig(opts.paths.configFile).planner,provider=planner?.provider??"anthropic",configuredMode=planner?.auth?.anthropic??"auto",loginProbe=detectClaudeLogin(loadSecrets(opts.paths.secretsFile)),binary=resolveClaudeBinary(),auth2,error2=null;try{auth2=resolvePlannerAuthForPaths(opts.paths,provider,planner)}catch(err2){if(!(err2 instanceof PlannerAuthUnavailableError))throw err2;error2=err2.message}let probe=c.req.query("probe")==="1"&&provider==="anthropic"&&auth2!==void 0?await probeWithCache(auth2,Date.now()):void 0,status=auth2?buildPayload2({auth:describeAuth(auth2),provider,planner,probe,fallbackLoggedIn:credentialPresent(auth2)}):void 0,payload={provider,configuredMode,resolved:status?{mode:status.mode,reason:status.reason,credentialHint:status.credentialHint}:null,error:error2,login:{present:loginProbe.present,hint:loginProbe.hint,detail:loginProbe.detail},account:status?.account??null,apiKeySource:status?.apiKeySource??null,runtime:runtimeNameFor(provider,planner)==="vercel"?"vercel":"agent-sdk",binary:{found:!!binary,source:binary?.source??null},loginCommand:PLANNER_AUTH_LOGIN_COMMAND,loggedIn:status?.loggedIn??!1,probe:probe?{ran:!0,ok:probe.ok,...probe.ok?{}:{kind:probe.kind,detail:probe.detail}}:null,runtimeConflict:subscriptionRuntimeConflict(provider,planner)};return c.json(payload)});let ModeBody=z11.object({mode:z11.string()});app.post("/api/planner-auth/mode",async c=>{let parsed=ModeBody.safeParse(await c.req.json().catch(()=>null));if(!parsed.success)return c.json({error:"invalid_body",issues:parsed.error.issues},400);let config2=loadConfig(opts.paths.configFile),next={...config2,planner:{...config2.planner??{},auth:{...config2.planner?.auth??{},anthropic:parsed.data.mode}}};try{saveConfig(opts.paths.configFile,next)}catch(err2){return c.json({error:"invalid_auth_mode",detail:err2.message},400)}return resetPlannerAuthProbeCache(),c.json({ok:!0,mode:parsed.data.mode})})}import{z as z12}from"zod";var SearchQuery=z12.object({q:z12.string().max(500)}),ImportBody=z12.object({issueId:z12.string().min(1),feature:z12.string().min(1).max(80),withAttachments:z12.boolean().optional()});function mountTrackerApi(app,opts){app.get("/api/tracker/search",async c=>{let q5=SearchQuery.safeParse({q:c.req.query("q")});if(!q5.success)return c.json({error:"invalid_query"},400);let cfg=loadConfig(opts.paths.configFile),secrets=overlayTrackerEnv(loadSecrets(opts.paths.secretsFile)),{client,error:error2}=clientFor(cfg,secrets);if(!client)return c.json({error:error2?.kind??"unsupported",detail:error2?.detail??""},400);try{let results=await client.searchIssues(q5.data.q);return c.json({ok:!0,results})}catch(err2){return err2 instanceof TrackerError?c.json({ok:!1,kind:err2.kind,status:err2.statusCode,detail:err2.message},200):c.json({ok:!1,detail:err2.message},200)}}),app.post("/api/tracker/import",async c=>{let body10=ImportBody.safeParse(await c.req.json());if(!body10.success)return c.json({error:"invalid_body",issues:body10.error.issues},400);let cfg=loadConfig(opts.paths.configFile),secrets=overlayTrackerEnv(loadSecrets(opts.paths.secretsFile)),{client,error:error2}=clientFor(cfg,secrets);if(!client)return c.json({error:error2?.kind??"unsupported",detail:error2?.detail??""},400);try{let issue=await client.fetchIssue(body10.data.issueId),created=await createStoryRecordFromIssue({paths:opts.paths,feature:body10.data.feature,issue,withAttachments:body10.data.withAttachments??!0,client,config:cfg});return c.json(created,201)}catch(err2){return err2 instanceof TrackerError?c.json({error:err2.kind,detail:err2.message},502):c.json({error:"fetch_failed",detail:err2.message},500)}})}async function gatherContextForApi(paths){return gatherContext(paths)}async function runAllChecksForApi(paths,ctx,fix){return runAllChecks(paths,ctx,fix)}function mountDoctorApi(app,opts){app.get("/api/doctor",async c=>{let ctx=await gatherContextForApi(opts.paths),checks=await runAllChecksForApi(opts.paths,ctx,!1);return c.json({root:opts.paths.root,checks,summary:summarise(checks)})}),app.post("/api/doctor/fix",async c=>{let ctx=await gatherContextForApi(opts.paths),checks=await runAllChecksForApi(opts.paths,ctx,!0);return c.json({root:opts.paths.root,checks,summary:summarise(checks)})})}function mountCopyPlanPromptApi(app,opts){app.get("/api/copy-plan-prompt",c=>{let feature=(c.req.query("feature")??"").trim(),storyId=(c.req.query("storyId")??"").trim();if(!feature||!storyId)return c.json({error:"bad_request",detail:"Query params `feature` and `storyId` are required."},400);let hit=listStories(opts.paths,{feature}).find(s=>s.feature===feature&&s.id===storyId);if(!hit)return c.json({error:"not_found",detail:`No story intake for feature "${feature}" and id "${storyId}".`},404);let config2=loadConfig(opts.paths.configFile),intakeContent=readFile(hit.intakePath),prompt=buildCopyPlanPromptMarkdown(config2,intakeContent),bytes=Buffer.byteLength(prompt,"utf8");return c.json({prompt,feature:hit.feature,storyId:hit.id,bytes,estTokensApprox:Math.round(bytes/4)})})}function mountDashboardApi(app,opts){app.get("/api/dashboard",async c=>{let cfg=loadConfig(opts.paths.configFile),lastRun=await readLastRun(opts.paths)??null,runs2=await listRuns(opts.paths),stories=listStories(opts.paths);return c.json({version:readInstalledPackage().version,root:opts.paths.root,project:cfg.project,planner:cfg.planner?{provider:cfg.planner.provider,enabled:cfg.planner.enabled}:null,tracker:{type:cfg.tracker.type},lastRun,runs:runs2.map(r=>({runId:r.runId,startedAt:r.startedAt,durationMs:r.durationMs,success:r.success,partial:r.partial,inputTokens:r.stats.inputTokens,outputTokens:r.stats.outputTokens,cacheHitRatio:r.stats.cacheHitRatio})),storyCounts:countStories(stories),stories:stories.map(s=>({feature:s.feature,id:s.id,intakePath:s.intakePath,storyDir:s.storyDir,planFile:s.planFile??null,titleHint:s.titleHint??null}))})})}function countStories(stories){let total=stories.length,planned=stories.filter(s=>!!s.planFile).length;return{total,planned,unplanned:total-planned}}import fs156 from"fs/promises";import path147 from"path";import os26 from"os";var MAX=10;function recentFilePath(){return path147.join(os26.homedir(),".squad","recent-projects.json")}async function loadRecent(){let RECENT_FILE=recentFilePath();try{let raw=await fs156.readFile(RECENT_FILE,"utf8"),parsed=JSON.parse(raw);return Array.isArray(parsed)?parsed.slice(0,MAX):[]}catch{return[]}}async function saveRecent(list6){let RECENT_FILE=recentFilePath();await fs156.mkdir(path147.dirname(RECENT_FILE),{recursive:!0}),await fs156.writeFile(RECENT_FILE,JSON.stringify(list6.slice(0,MAX),null,2)+`
1888
- `,"utf8")}function mountProjectsApi(app,opts){app.get("/api/projects/recent",async c=>c.json(await loadRecent())),app.post("/api/projects/touch",async c=>{let filtered=(await loadRecent()).filter(p=>p.root!==opts.paths.root);return filtered.unshift({root:opts.paths.root,lastOpenedAt:new Date().toISOString()}),await saveRecent(filtered),c.json({ok:!0})})}import fs162 from"fs";import{createHmac as createHmac2,randomBytes as randomBytes9}from"crypto";import path154 from"path";function architecture(bp){let decisions=listDecisions(bp).sort((a,b)=>num3(a.id)-num3(b.id)).map(withMentions),shipped2=new Set(listReleases(bp).filter(r=>r.status==="released").flatMap(r=>r.tickets)),tickets=listTickets(bp).sort((a,b)=>num3(a.id)-num3(b.id)).map(t=>({id:t.id,title:t.title,kind:t.kind,state:stateOf2(t,shipped2),made:t.decision?.adr??decisions.find(d=>d.from===t.id)?.id,follows:t.follows??decisions.filter(d=>d.from!==t.id&&d.tickets.includes(t.id)).map(d=>d.id),...t.release?{release:t.release}:{}})),meta=readMeta(bp),notes=meta.blueprint?.stack??[],item=t=>{let note2=notes.find(n=>n.name.toLowerCase()===t.name.toLowerCase()),chosen2=[...new Set([...note2?.decision?[note2.decision]:[],...decidedIn(t,decisions)])],draft=meta.techDrafts?.find(d=>d.tech===t.name);return{...t,...note2?.why?{why:note2.why}:{},decisions:chosen2,...draft?{draft}:{}}},inPackages=detectStack(bp.root,meta),named=new Set(inPackages.map(t=>t.name.toLowerCase())),repo=reposOf(meta)[0]?.path??".",seen=notes.filter(n=>n.role&&!named.has(n.name.toLowerCase())).map(n=>({name:n.name,role:n.role,icon:n.name.toLowerCase().replace(/[^a-z0-9]/g,""),repo})),found2=[...inPackages,...seen].map(item),stack=found2.map(t=>{if(t.decisions.length)return t;let parent=found2.find(p=>t.comesWith?.includes(p.name)&&p.decisions.length);return parent?{...t,via:{name:parent.name,decisions:parent.decisions}}:t}),{stack:_s,patterns=[],...reading}=meta.blueprint??{stack:[],patterns:[]},broken=brokenDecisions(bp);return{decisions,tickets,map:meta.map,lessons:meta.lessons,stack,patterns,impact:meta.impactChecks??[],...meta.blueprint?{reading}:{},...broken.length?{broken}:{}}}function stateOf2(t,shipped2){return shipped2.has(t.id)||t.stage==="done"?"shipped":t.mergedAt?"merged":t.decision?.adr||["implement","test","review","release"].includes(t.stage)?"building":t.work||t.steps?.length?"designed":"coming"}function withMentions(d){if(d.buildsOn)return d;let named=[...new Set(d.why.match(/ADR-\d+/g)??[])].filter(x=>x!==d.id);return named.length?{...d,buildsOn:named}:d}function num3(id2){return Number(id2.replace(/\D/g,""))||0}import fs157 from"fs";import os27 from"os";import path148 from"path";import{randomUUID as randomUUID8}from"crypto";var DraftCancelled=class extends BoardActionError{constructor(){super("You stopped the Analyst. Your text is kept.")}};function checkInput(input7){if(!input7.text?.trim()&&!input7.files?.length)throw new BoardActionError("Paste the idea or message first, or add a file.")}async function draftTicket(bp,input7,opts={}){checkInput(input7);let text3=input7.text?.trim()??"",meta=readMeta(bp);if(meta.engine!=="live")return roughDraft(text3);let dir=fs157.mkdtempSync(path148.join(os27.tmpdir(),"squad-draft-")),progress2={step:"read",looked:[]},report3=(step2,looked3)=>{step2===progress2.step&&(!looked3||progress2.looked.includes(looked3))||(progress2.step=step2,looked3&&!progress2.looked.includes(looked3)&&progress2.looked.push(looked3),opts.onProgress?.({step:step2,looked:[...progress2.looked]}))};try{let saved=saveUploads(dir,input7.files??[]),files=[...saved,...(await prepareMedia(bp,saved)).added];if(opts.signal?.aborted)throw new DraftCancelled;let root=projectRoot(bp),r=await runAnalyst(bp,{cwd:root,prompt:draftPrompt(meta,text3,files,{rules:rulesFiles(root)}),permissions:["read"],schema:DRAFT_SCHEMA,addDirs:[dir],timeoutMs:DRAFT_MS,...opts.signal?{signal:opts.signal}:{},onActivity:()=>{progress2.step==="read"&&report3("look")},onTool:e=>report3("look",lookedAt(bp,e,root,dir)),onText:()=>report3("write")}).catch(err2=>{throw opts.signal?.aborted?new DraftCancelled:err2});if(opts.signal?.aborted)throw new DraftCancelled;return r.structured}finally{fs157.rmSync(dir,{recursive:!0,force:!0})}}function lookedAt(bp,e,root,uploads){let file3=[e.input.file_path,e.input.path,e.input.notebook_path].find(p=>typeof p=="string"&&p.length>0);if(!file3)return describeToolEvent(e,root)?"the code":null;let abs=path148.resolve(root,file3),inside5=dir=>abs===dir||abs.startsWith(dir+path148.sep);return inside5(uploads)?`your file ${path148.basename(abs)}`:inside5(bp.decisionsDir)?`saved decision ${path148.basename(abs,".md")}`:"the code"}var DRAFT_MS=4*6e4,drafts=new Map,KEEP_MS2=24*36e5;function held3(bp){let h=drafts.get(bp.squadDir);if(h?.job.endedAt&&Date.now()-Date.parse(h.job.endedAt)>KEEP_MS2){drafts.delete(bp.squadDir);return}return h}function heldJob(bp,id2){let h=held3(bp);if(!h||h.job.id!==id2)throw new BoardActionError("This draft is gone. Paste the text again.");return h}var copy=job=>({...job,looked:[...job.looked],files:[...job.files]});function startDraft(bp,input7){checkInput(input7),held3(bp)?.stop.abort();let stop=new AbortController,job={id:randomUUID8(),status:"working",step:"read",looked:[],text:input7.text??"",files:(input7.files??[]).map(f=>({name:f.name,size:Buffer.byteLength(f.data??"","base64")})),startedAt:new Date().toISOString()};drafts.set(bp.squadDir,{job,files:input7.files??[],stop});let end=fields=>{job.status==="working"&&Object.assign(job,fields,{endedAt:new Date().toISOString()})},onProgress=p=>{job.status==="working"&&Object.assign(job,p)};return draftTicket(bp,input7,{signal:stop.signal,onProgress}).then(draft=>end({status:"done",draft})).catch(err2=>end(err2 instanceof DraftCancelled?{status:"cancelled"}:{status:"failed",error:err2 instanceof Error?err2.message:String(err2)})),copy(job)}function currentDraft(bp){let h=held3(bp);return h?copy(h.job):null}function draftJob(bp,id2){return copy(heldJob(bp,id2).job)}function cancelDraft(bp,id2){let h=heldJob(bp,id2);return h.job.status==="working"&&(Object.assign(h.job,{status:"cancelled",endedAt:new Date().toISOString()}),h.stop.abort()),copy(h.job)}function dismissDraft(bp,id2){let h=held3(bp);return h?.job.id===id2&&(h.stop.abort(),drafts.delete(bp.squadDir)),{ok:!0}}function draftFiles(bp,id2){return{files:heldJob(bp,id2).files}}async function runAnalyst(bp,opts){let agent=readAgent(bp,"analyst"),tool5=aiTool(agent.tool);if(!tool5?.canRun){let can=AI_TOOLS.filter(t=>t.canRun).map(t=>t.label).join(" or ");throw new BoardActionError(`The Analyst 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),rulesFor(bp,"analyst")].filter(Boolean).join(`
1887
+ `);return}let kw2=15;if(step("squad-kit auth"),blank(),kv("auth mode",`${payload.mode} (${payload.reason})`,kw2),kv("credential",payload.credentialHint,kw2),payload.account){let account=[payload.account.email,payload.account.organization,payload.account.subscriptionType].filter(Boolean).join(" \xB7 ");account&&kv("account",account,kw2)}payload.apiKeySource&&kv("api key source",payload.apiKeySource,kw2),kv("runtime",payload.runtime,kw2),probe&&!probe.ok?(blank(),warning(`Live check failed (${probe.kind}).`),info(probe.detail),(probe.kind==="not-logged-in"||probe.kind==="expired")&&info("Run `squad auth login` to sign in again.")):probe||(blank(),info("Account details skipped (offline). Re-run without --offline to check the live login."))}function shouldProbe(opts,provider){return opts.offline||skipExternalProbesInAutomation()?!1:provider==="anthropic"}function buildPayload2(input7){let{auth:auth2,provider,planner,probe}=input7,payload={mode:auth2.mode,reason:authReasonText(auth2.reason,provider),credentialHint:auth2.credentialHint,runtime:runtimeNameFor(provider,planner),loggedIn:probe?probe.ok:input7.fallbackLoggedIn};return probe?.ok&&(probe.account&&(payload.account=probe.account),probe.apiKeySource&&(payload.apiKeySource=probe.apiKeySource)),payload}var PLANNER_AUTH_LOGIN_COMMAND="squad auth login",PROBE_CACHE_TTL_MS=6e4,probeCache;function probeCacheKey(auth2){let described=describeAuth(auth2);return`${described.mode}:${described.credentialHint}`}async function probeWithCache(auth2,now3){let key=probeCacheKey(auth2);if(probeCache&&probeCache.key===key&&now3-probeCache.at<PROBE_CACHE_TTL_MS)return probeCache.result;let result=await probeClaudeAuth(auth2);return result.ok&&(probeCache={key,at:now3,result}),result}function resetPlannerAuthProbeCache(){probeCache=void 0}function subscriptionRuntimeConflict(provider,planner){return provider!=="anthropic"?plannerAuthRuntimeMismatchMessage(provider):runtimeNameFor(provider,planner)==="vercel"?plannerAuthRuntimeMismatchMessage(provider):null}function mountPlannerAuthApi(app,opts){app.get("/api/planner-auth",async c=>{let planner=loadConfig(opts.paths.configFile).planner,provider=planner?.provider??"anthropic",configuredMode=planner?.auth?.anthropic??"auto",loginProbe=detectClaudeLogin(loadSecrets(opts.paths.secretsFile)),binary=resolveClaudeBinary(),auth2,error2=null;try{auth2=resolvePlannerAuthForPaths(opts.paths,provider,planner)}catch(err2){if(!(err2 instanceof PlannerAuthUnavailableError))throw err2;error2=err2.message}let probe=c.req.query("probe")==="1"&&provider==="anthropic"&&auth2!==void 0?await probeWithCache(auth2,Date.now()):void 0,status=auth2?buildPayload2({auth:describeAuth(auth2),provider,planner,probe,fallbackLoggedIn:credentialPresent(auth2)}):void 0,payload={provider,configuredMode,resolved:status?{mode:status.mode,reason:status.reason,credentialHint:status.credentialHint}:null,error:error2,login:{present:loginProbe.present,hint:loginProbe.hint,detail:loginProbe.detail},account:status?.account??null,apiKeySource:status?.apiKeySource??null,runtime:runtimeNameFor(provider,planner)==="vercel"?"vercel":"agent-sdk",binary:{found:!!binary,source:binary?.source??null},loginCommand:PLANNER_AUTH_LOGIN_COMMAND,loggedIn:status?.loggedIn??!1,probe:probe?{ran:!0,ok:probe.ok,...probe.ok?{}:{kind:probe.kind,detail:probe.detail}}:null,runtimeConflict:subscriptionRuntimeConflict(provider,planner)};return c.json(payload)});let ModeBody=z11.object({mode:z11.string()});app.post("/api/planner-auth/mode",async c=>{let parsed=ModeBody.safeParse(await c.req.json().catch(()=>null));if(!parsed.success)return c.json({error:"invalid_body",issues:parsed.error.issues},400);let config2=loadConfig(opts.paths.configFile),next={...config2,planner:{...config2.planner??{},auth:{...config2.planner?.auth??{},anthropic:parsed.data.mode}}};try{saveConfig(opts.paths.configFile,next)}catch(err2){return c.json({error:"invalid_auth_mode",detail:err2.message},400)}return resetPlannerAuthProbeCache(),c.json({ok:!0,mode:parsed.data.mode})})}import{z as z12}from"zod";var SearchQuery=z12.object({q:z12.string().max(500)}),ImportBody=z12.object({issueId:z12.string().min(1),feature:z12.string().min(1).max(80),withAttachments:z12.boolean().optional()});function mountTrackerApi(app,opts){app.get("/api/tracker/search",async c=>{let q5=SearchQuery.safeParse({q:c.req.query("q")});if(!q5.success)return c.json({error:"invalid_query"},400);let cfg=loadConfig(opts.paths.configFile),secrets=overlayTrackerEnv(loadSecrets(opts.paths.secretsFile)),{client,error:error2}=clientFor(cfg,secrets);if(!client)return c.json({error:error2?.kind??"unsupported",detail:error2?.detail??""},400);try{let results=await client.searchIssues(q5.data.q);return c.json({ok:!0,results})}catch(err2){return err2 instanceof TrackerError?c.json({ok:!1,kind:err2.kind,status:err2.statusCode,detail:err2.message},200):c.json({ok:!1,detail:err2.message},200)}}),app.post("/api/tracker/import",async c=>{let body10=ImportBody.safeParse(await c.req.json());if(!body10.success)return c.json({error:"invalid_body",issues:body10.error.issues},400);let cfg=loadConfig(opts.paths.configFile),secrets=overlayTrackerEnv(loadSecrets(opts.paths.secretsFile)),{client,error:error2}=clientFor(cfg,secrets);if(!client)return c.json({error:error2?.kind??"unsupported",detail:error2?.detail??""},400);try{let issue=await client.fetchIssue(body10.data.issueId),created=await createStoryRecordFromIssue({paths:opts.paths,feature:body10.data.feature,issue,withAttachments:body10.data.withAttachments??!0,client,config:cfg});return c.json(created,201)}catch(err2){return err2 instanceof TrackerError?c.json({error:err2.kind,detail:err2.message},502):c.json({error:"fetch_failed",detail:err2.message},500)}})}async function gatherContextForApi(paths){return gatherContext(paths)}async function runAllChecksForApi(paths,ctx,fix){return runAllChecks(paths,ctx,fix)}function mountDoctorApi(app,opts){app.get("/api/doctor",async c=>{let ctx=await gatherContextForApi(opts.paths),checks=await runAllChecksForApi(opts.paths,ctx,!1);return c.json({root:opts.paths.root,checks,summary:summarise(checks)})}),app.post("/api/doctor/fix",async c=>{let ctx=await gatherContextForApi(opts.paths),checks=await runAllChecksForApi(opts.paths,ctx,!0);return c.json({root:opts.paths.root,checks,summary:summarise(checks)})})}function mountCopyPlanPromptApi(app,opts){app.get("/api/copy-plan-prompt",c=>{let feature=(c.req.query("feature")??"").trim(),storyId=(c.req.query("storyId")??"").trim();if(!feature||!storyId)return c.json({error:"bad_request",detail:"Query params `feature` and `storyId` are required."},400);let hit=listStories(opts.paths,{feature}).find(s=>s.feature===feature&&s.id===storyId);if(!hit)return c.json({error:"not_found",detail:`No story intake for feature "${feature}" and id "${storyId}".`},404);let config2=loadConfig(opts.paths.configFile),intakeContent=readFile(hit.intakePath),prompt=buildCopyPlanPromptMarkdown(config2,intakeContent),bytes=Buffer.byteLength(prompt,"utf8");return c.json({prompt,feature:hit.feature,storyId:hit.id,bytes,estTokensApprox:Math.round(bytes/4)})})}function mountDashboardApi(app,opts){app.get("/api/dashboard",async c=>{let cfg=loadConfig(opts.paths.configFile),lastRun=await readLastRun(opts.paths)??null,runs2=await listRuns(opts.paths),stories=listStories(opts.paths);return c.json({version:readInstalledPackage().version,root:opts.paths.root,project:cfg.project,planner:cfg.planner?{provider:cfg.planner.provider,enabled:cfg.planner.enabled}:null,tracker:{type:cfg.tracker.type},lastRun,runs:runs2.map(r=>({runId:r.runId,startedAt:r.startedAt,durationMs:r.durationMs,success:r.success,partial:r.partial,inputTokens:r.stats.inputTokens,outputTokens:r.stats.outputTokens,cacheHitRatio:r.stats.cacheHitRatio})),storyCounts:countStories(stories),stories:stories.map(s=>({feature:s.feature,id:s.id,intakePath:s.intakePath,storyDir:s.storyDir,planFile:s.planFile??null,titleHint:s.titleHint??null}))})})}function countStories(stories){let total=stories.length,planned=stories.filter(s=>!!s.planFile).length;return{total,planned,unplanned:total-planned}}import fs156 from"fs/promises";import path147 from"path";import os27 from"os";var MAX=10;function recentFilePath(){return path147.join(os27.homedir(),".squad","recent-projects.json")}async function loadRecent(){let RECENT_FILE=recentFilePath();try{let raw=await fs156.readFile(RECENT_FILE,"utf8"),parsed=JSON.parse(raw);return Array.isArray(parsed)?parsed.slice(0,MAX):[]}catch{return[]}}async function saveRecent(list6){let RECENT_FILE=recentFilePath();await fs156.mkdir(path147.dirname(RECENT_FILE),{recursive:!0}),await fs156.writeFile(RECENT_FILE,JSON.stringify(list6.slice(0,MAX),null,2)+`
1888
+ `,"utf8")}function mountProjectsApi(app,opts){app.get("/api/projects/recent",async c=>c.json(await loadRecent())),app.post("/api/projects/touch",async c=>{let filtered=(await loadRecent()).filter(p=>p.root!==opts.paths.root);return filtered.unshift({root:opts.paths.root,lastOpenedAt:new Date().toISOString()}),await saveRecent(filtered),c.json({ok:!0})})}import fs162 from"fs";import{createHmac as createHmac2,randomBytes as randomBytes9}from"crypto";import path154 from"path";function architecture(bp){let decisions=listDecisions(bp).sort((a,b)=>num3(a.id)-num3(b.id)).map(withMentions),shipped2=new Set(listReleases(bp).filter(r=>r.status==="released").flatMap(r=>r.tickets)),tickets=listTickets(bp).sort((a,b)=>num3(a.id)-num3(b.id)).map(t=>({id:t.id,title:t.title,kind:t.kind,state:stateOf2(t,shipped2),made:t.decision?.adr??decisions.find(d=>d.from===t.id)?.id,follows:t.follows??decisions.filter(d=>d.from!==t.id&&d.tickets.includes(t.id)).map(d=>d.id),...t.release?{release:t.release}:{}})),meta=readMeta(bp),notes=meta.blueprint?.stack??[],item=t=>{let note2=notes.find(n=>n.name.toLowerCase()===t.name.toLowerCase()),chosen2=[...new Set([...note2?.decision?[note2.decision]:[],...decidedIn(t,decisions)])],draft=meta.techDrafts?.find(d=>d.tech===t.name);return{...t,...note2?.why?{why:note2.why}:{},decisions:chosen2,...draft?{draft}:{}}},inPackages=detectStack(bp.root,meta),named=new Set(inPackages.map(t=>t.name.toLowerCase())),repo=reposOf(meta)[0]?.path??".",seen=notes.filter(n=>n.role&&!named.has(n.name.toLowerCase())).map(n=>({name:n.name,role:n.role,icon:n.name.toLowerCase().replace(/[^a-z0-9]/g,""),repo})),found2=[...inPackages,...seen].map(item),stack=found2.map(t=>{if(t.decisions.length)return t;let parent=found2.find(p=>t.comesWith?.includes(p.name)&&p.decisions.length);return parent?{...t,via:{name:parent.name,decisions:parent.decisions}}:t}),{stack:_s,patterns=[],...reading}=meta.blueprint??{stack:[],patterns:[]},broken=brokenDecisions(bp);return{decisions,tickets,map:meta.map,lessons:meta.lessons,stack,patterns,impact:meta.impactChecks??[],...meta.blueprint?{reading}:{},...broken.length?{broken}:{}}}function stateOf2(t,shipped2){return shipped2.has(t.id)||t.stage==="done"?"shipped":t.mergedAt?"merged":t.decision?.adr||["implement","test","review","release"].includes(t.stage)?"building":t.work||t.steps?.length?"designed":"coming"}function withMentions(d){if(d.buildsOn)return d;let named=[...new Set(d.why.match(/ADR-\d+/g)??[])].filter(x=>x!==d.id);return named.length?{...d,buildsOn:named}:d}function num3(id2){return Number(id2.replace(/\D/g,""))||0}import fs157 from"fs";import os28 from"os";import path148 from"path";import{randomUUID as randomUUID8}from"crypto";var DraftCancelled=class extends BoardActionError{constructor(){super("You stopped the Analyst. Your text is kept.")}};function checkInput(input7){if(!input7.text?.trim()&&!input7.files?.length)throw new BoardActionError("Paste the idea or message first, or add a file.")}async function draftTicket(bp,input7,opts={}){checkInput(input7);let text3=input7.text?.trim()??"",meta=readMeta(bp);if(meta.engine!=="live")return roughDraft(text3);let dir=fs157.mkdtempSync(path148.join(os28.tmpdir(),"squad-draft-")),progress2={step:"read",looked:[]},report3=(step2,looked3)=>{step2===progress2.step&&(!looked3||progress2.looked.includes(looked3))||(progress2.step=step2,looked3&&!progress2.looked.includes(looked3)&&progress2.looked.push(looked3),opts.onProgress?.({step:step2,looked:[...progress2.looked]}))};try{let saved=saveUploads(dir,input7.files??[]),files=[...saved,...(await prepareMedia(bp,saved)).added];if(opts.signal?.aborted)throw new DraftCancelled;let root=projectRoot(bp),r=await runAnalyst(bp,{cwd:root,prompt:draftPrompt(meta,text3,files,{rules:rulesFiles(root)}),permissions:["read"],schema:DRAFT_SCHEMA,addDirs:[dir],timeoutMs:DRAFT_MS,...opts.signal?{signal:opts.signal}:{},onActivity:()=>{progress2.step==="read"&&report3("look")},onTool:e=>report3("look",lookedAt(bp,e,root,dir)),onText:()=>report3("write")}).catch(err2=>{throw opts.signal?.aborted?new DraftCancelled:err2});if(opts.signal?.aborted)throw new DraftCancelled;return r.structured}finally{fs157.rmSync(dir,{recursive:!0,force:!0})}}function lookedAt(bp,e,root,uploads){let file3=[e.input.file_path,e.input.path,e.input.notebook_path].find(p=>typeof p=="string"&&p.length>0);if(!file3)return describeToolEvent(e,root)?"the code":null;let abs=path148.resolve(root,file3),inside5=dir=>abs===dir||abs.startsWith(dir+path148.sep);return inside5(uploads)?`your file ${path148.basename(abs)}`:inside5(bp.decisionsDir)?`saved decision ${path148.basename(abs,".md")}`:"the code"}var DRAFT_MS=4*6e4,drafts=new Map,KEEP_MS2=24*36e5;function held3(bp){let h=drafts.get(bp.squadDir);if(h?.job.endedAt&&Date.now()-Date.parse(h.job.endedAt)>KEEP_MS2){drafts.delete(bp.squadDir);return}return h}function heldJob(bp,id2){let h=held3(bp);if(!h||h.job.id!==id2)throw new BoardActionError("This draft is gone. Paste the text again.");return h}var copy=job=>({...job,looked:[...job.looked],files:[...job.files]});function startDraft(bp,input7){checkInput(input7),held3(bp)?.stop.abort();let stop=new AbortController,job={id:randomUUID8(),status:"working",step:"read",looked:[],text:input7.text??"",files:(input7.files??[]).map(f=>({name:f.name,size:Buffer.byteLength(f.data??"","base64")})),startedAt:new Date().toISOString()};drafts.set(bp.squadDir,{job,files:input7.files??[],stop});let end=fields=>{job.status==="working"&&Object.assign(job,fields,{endedAt:new Date().toISOString()})},onProgress=p=>{job.status==="working"&&Object.assign(job,p)};return draftTicket(bp,input7,{signal:stop.signal,onProgress}).then(draft=>end({status:"done",draft})).catch(err2=>end(err2 instanceof DraftCancelled?{status:"cancelled"}:{status:"failed",error:err2 instanceof Error?err2.message:String(err2)})),copy(job)}function currentDraft(bp){let h=held3(bp);return h?copy(h.job):null}function draftJob(bp,id2){return copy(heldJob(bp,id2).job)}function cancelDraft(bp,id2){let h=heldJob(bp,id2);return h.job.status==="working"&&(Object.assign(h.job,{status:"cancelled",endedAt:new Date().toISOString()}),h.stop.abort()),copy(h.job)}function dismissDraft(bp,id2){let h=held3(bp);return h?.job.id===id2&&(h.stop.abort(),drafts.delete(bp.squadDir)),{ok:!0}}function draftFiles(bp,id2){return{files:heldJob(bp,id2).files}}async function runAnalyst(bp,opts){let agent=readAgent(bp,"analyst"),tool5=aiTool(agent.tool);if(!tool5?.canRun){let can=AI_TOOLS.filter(t=>t.canRun).map(t=>t.label).join(" or ");throw new BoardActionError(`The Analyst 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),rulesFor(bp,"analyst")].filter(Boolean).join(`
1889
1889
 
1890
1890
  `),lv=resolveLevel({role:"analyst",step:"draft",quality:readMeta(bp).quality},agent.levels,personLevels()),effort=effortOn(tool5,lv.effort),r=await tool5.run({...opts,secretFiles:await secretFiles(bp),privatePlaces:agentPrivatePlaces(),...extra?{instructions:extra}:{},level:lv.level,...effort?{effort}:{},...lv.model?{model:lv.model}:{},logDir:bp.chatsDir,direct:readMeta(bp).runner==="direct"});if(recordUsage(bp,void 0,{role:"analyst",step:"draft",level:lv.level,model:lv.model||tool5.modelFor(lv.level)},r.usage),!r.ok){let project=readMeta(bp).project;r.limit&&startPause(r.limit,{project}),log.warn(`The Analyst could not fill in a new ticket: ${r.detail??r.error??"no answer"}`,{project})}if(!r.ok||!r.structured)throw new BoardActionError(r.error&&ownRunWords(r.error,tool5)?r.error:"The Analyst did not fill in the ticket. Try again, or write it yourself.");return r}function roughDraft(text3){let lines3=text3.split(`
1891
1891
  `).map(l=>l.trim()).filter(Boolean),title2=(lines3[0]??"New ticket").replace(/[.!?]+$/,"").slice(0,80);return{title:title2,ask:lines3.slice(1).join(" ")||title2,kind:/\b(bug|broken|error|crash|fails?)\b/i.test(text3)?"bug":"feature",track:/\b(typo|label|spelling|rename)\b/i.test(text3)?"express":"full",priority:/\b(urgent|asap|blocker|today)\b/i.test(text3)?"high":"normal",criteria:[],rules:[],edgeCases:[],gaps:["The demo board fills in only the title. Connect a real project to let the Analyst write the rest."]}}import fs158 from"fs";import path149 from"path";import{spawn as spawn16,execFileSync as execFileSync15}from"child_process";var MAX_PATCH=600*1024,MAX_DIFF_READ=64*1024*1024;async function diffOrNames(dir,range){try{return{files:splitDiff(await gitLater(dir,["diff","--no-color","--find-renames",range],{allowFail:!0,mostBytes:MAX_DIFF_READ})),cut:!1}}catch(err2){if(!(err2 instanceof GitSaidTooMuch))throw err2}let fields=(await gitLater(dir,["diff","--name-status","-z","--find-renames",range],{allowFail:!0})).split("\0").filter(Boolean),files=[];for(let i=0;i<fields.length;){let status=fields[i++][0],from=status==="R"||status==="C"?fields[i++]:"",to=fields[i++];files.push({path:from?`${from} \u2192 ${to}`:to,status:status==="A"?"added":status==="D"?"deleted":from?"renamed":"changed",added:0,removed:0,patch:""})}return{files,cut:!0}}async function ticketChanges(bp,id2){let t=requireTicket(bp,id2);if(!t.baseCommit)return{from:"none",files:[]};if(t.worktree&&fs158.existsSync(t.worktree)){let cut=!1,inCopy=async(dir2,base2)=>{let diff=await diffOrNames(dir2,base2);cut||=diff.cut;let{files:files2}=diff;for(let f of(await gitLater(dir2,["ls-files","--others","--exclude-standard"],{allowFail:!0})).split(`
@@ -1904,7 +1904,7 @@ body.dark ul{background:#0004}
1904
1904
  \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1905
1905
 
1906
1906
  `),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 fs159 from"fs";import path150 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 textOf2(file3){if(fs159.statSync(file3).size>MAX_FOLDER_FILE_BYTES)return null;let data=fs159.readFileSync(file3);if(data.includes(0))return null;let text3=data.toString("utf8");return text3.includes("\uFFFD")?null:text3}function sectionsOf2(text3,title2){let out20=[],at3={name:title2,body:""};for(let line3 of text3.split(/\r?\n/)){let h=/^#{1,6}\s+(.+?)\s*#*$/.exec(line3);h?(at3.body.trim()&&out20.push(at3),at3={name:h[1],body:""}):at3.body+=`${line3}
1907
- `}return at3.body.trim()&&out20.push(at3),out20}var boxesOf=body10=>[...body10.matchAll(/^\s*[-*]\s+\[[ xX]\]\s+(.+)$/gm)].map(m=>m[1].trim()),namedFrom=file3=>path150.basename(file3).replace(/\.[^.]+$/,"");function fromMarkdown(source2,text3,asRules){return sectionsOf2(text3,namedFrom(source2)).flatMap((s,i)=>{let key=`${source2}#${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(q5=>scrubSecrets(q5).slice(0,MAX_TYPED_CHARS));return[{key,kind:"checklist",name,source:source2,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:source2,text:body10,...tooLong,...ours}]})}function fromOurFormat(source2,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=`${source2}#${i}`;return name?a.kind==="rule"&&typeof a.text=="string"?[{key,kind:"rule",name,source:source2,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:source2,questions:a.questions.filter(q5=>typeof q5=="string").slice(0,MAX_CHECKLIST_QUESTIONS).map(q5=>scrubSecrets(q5))}]:a.kind==="gate"&&typeof a.command=="string"?[{key,kind:"gate",name,source:source2,command:a.command.trim(),when:["checks","merge","release"].includes(a.when)?a.when:"checks"}]:[]:[]}):[]}function runnerFor(root){return fs159.existsSync(path150.join(root,"pnpm-lock.yaml"))?s=>`pnpm run ${s}`:fs159.existsSync(path150.join(root,"yarn.lock"))?s=>`yarn ${s}`:s=>`npm run ${s}`}function fromPackage(root,source2,text3,commands){let scripts;try{scripts=JSON.parse(text3).scripts??{}}catch{return[]}let run7=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:`${source2}#${s}`,kind:"gate",name:s,source:source2,command:run7(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=fs159.realpathSync(path150.resolve(folder)),!fs159.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=[],left2=0,seen=0,looked3=0,more3=!1,walk3=(dir,depth)=>{let entries;try{entries=fs159.readdirSync(dir,{withFileTypes:!0})}catch{left2+=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=path150.join(dir,e.name),source2=path150.relative(root,at3).split(path150.sep).join("/");if(HOLDS_KEYS.test(e.name)||NOT_LOOKED_IN.has(e.name)){left2+=1;continue}let real2=at3;if(e.isSymbolicLink()){try{real2=fs159.realpathSync(at3)}catch{real2=""}if(!real2||!`${real2}${path150.sep}`.startsWith(`${root}${path150.sep}`)){left2+=1;continue}}let stat;try{stat=fs159.statSync(real2)}catch{left2+=1;continue}if(stat.isDirectory()){depth<DEEPEST&&!e.isSymbolicLink()&&walk3(at3,depth+1);continue}if(!stat.isFile())continue;let isScriptCheck=source2==="scripts/check",isMarkdown=/\.(md|mdc|markdown)$/i.test(e.name),isOurs=source2==="harness.yaml"||source2===".squad/harness.yaml";if(!isMarkdown&&!isOurs&&!isScriptCheck&&source2!=="package.json"&&!(/(^|\/)rules\//.test(source2)&&/\.txt$/i.test(e.name)))continue;if((seen+=1)>MAX_FOLDER_FILES){more3=!0;return}let text3=null;try{text3=textOf2(real2)}catch{}if(text3===null){left2+=1;continue}isOurs?finds.push(...fromOurFormat(source2,text3)):source2==="package.json"?finds.push(...fromPackage(root,source2,text3,commands)):isScriptCheck?finds.push({key:source2,kind:"gate",name:"scripts/check",source:source2,command:"./scripts/check",when:"checks"}):finds.push(...fromMarkdown(source2,text3,/^(AGENTS|CLAUDE)\.md$/.test(e.name)||/(^|\/)rules\//.test(source2)))}};walk3(root,0);let named=new Set(have.map(a=>`${a.kind}\0${a.name.trim().toLowerCase()}`));return{name:path150.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:left2,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,now3=Date.now()){let from=now3-SEEN_DAYS*ONE_DAY,week=now3-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,about2,when2,gates)=>({id:id2,name,about:about2,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(q5=>QUALITY_PRESETS[q5].reviewerFloor).map(q5=>q5.charAt(0).toUpperCase()+q5.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("flutter-stamps","Flutter's own version stamps","Flutter writes its version in a few small files of its own folder on every run. Those files, and nothing else of Flutter, are open to the squad's commands."),g("xcode","iPhone builds",OUTSIDE_THE_GUARD_WORDS),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("phone-pictures","iPhone apps on a real iPhone","On a Mac, an iPhone app's pictures come from squad-kit's own hidden iPhone and iPad, each named with its device. With its tools missing, pictures of the web version stand in, and the story says so.","With no web version either, it waits for the tools and goes on once they are there. An app that does not build or a phone that does not start: a card, and you decide."),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):[],run7=(...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=list6=>list6?.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",run7("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"]),"flutter-stamps":code("engine/flutter-sdk.ts",...FLUTTER_STAMPS.map(f=>[f.file,f.why]),["In",flutterOnPath()?.root??"No Flutter on this computer"]),xcode:code("engine/checks.ts",["Runs outside the guard","xcodebuild build and test, only as squad-kit writes them for a native iPhone app (outsideTheGuard)"],["Keeps","Xcode's own sandboxes: for a package's manifest and plugins, and for build scripts when the project turns that on"],["Builds in","build/squad-kit-ios in the story's copy, with the guard's caches"]),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",run7("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",run7("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",run7("setup")],["Looks at","Package files and database changes, saved or not"]),"test-change":code("engine/test-change.ts",["Runs",run7("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",run7("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`]),"phone-pictures":code("engine/phone/session.ts",["Devices","squad-kit iPhone and squad-kit iPad, its own, one started at a time"],["Taps with","Maestro"]),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",run7("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&&!partsOf2(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)?partsOf2(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(),q6=s2.replace(AR_POLITE,"");return q6!==s2?{topic:AR.find(([,re])=>re.test(q6))?.[0]??"other",lang:"ar"}:AR_DO.test(q6)?{topic:"change",lang:"ar"}:{topic:AR.find(([,re])=>re.test(q6))?.[0]??"other",lang:"ar"}}let s=text3.toLowerCase().replace(/[’`]/g,"'").replace(/[?!.,;:]+/g," ").replace(/\s+/g," ").trim(),q5=s.replace(EN_POLITE,"");if(q5===s&&EN_DO.test(q5))return{topic:"change",lang:"en"};let found2=EN.find(([,re])=>re.test(q5))?.[0];return found2?{topic:found2,lang:"en"}:{topic:q5===s&&EN_ASK.test(q5)?"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,now3){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=now3.part;switch(now3.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(now3){return now3.usual?Math.max(60,middle(now3.usual)-(now3.seconds??0)):60}function noteWhenAr(t,now3){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(now3.state){case"working":return now3.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(now3))}.`:`\u064A\u0642\u0631\u0624\u0647\u0627 ${WHO_AR[now3.role??"builder"]} \u0639\u0646\u062F \u0628\u062F\u0621 \u062E\u0637\u0648\u062A\u0647 \u0627\u0644\u062A\u0627\u0644\u064A\u0629\u060C ${arIn(noteLeft(now3))}.`;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(now3){return now3.role&&now3.role!=="squad"?now3.role:"analyst"}function slowNow(now3,book,t){return now3.slow?!0:!now3.step||!now3.role||now3.seconds===void 0?!1:now3.seconds>=slowAfter(usualOf(book,now3.role,now3.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,now3,book,lang2){let ar=lang2==="ar",secs=now3.seconds;if(now3.state==="working"){let who4=ar?WHO_AR[now3.role??"builder"]:`The ${now3.who}`;if(now3.slow?.kind==="stuck"){let quiet=now3.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(now3,book,t))return!now3.slow&&!ar?[`It is slower than usual, but still working. ${now3.role==="squad"?"squad-kit":who4} has been on this step for ${minutesWords(secs??0)}. It usually takes ${rangeEn(now3.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(now3.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. ${now3.slow?.text??""}`.trim(),"You can give it 10 more minutes, stop and try again, or stop it from the card above."];let p=now3.part,u=now3.usual,checks=now3.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,now3)} \u0633\u062A\u0638\u0647\u0631 \u0641\u064A \xABWaiting for you\xBB \u0639\u0646\u062F\u0645\u0627 \u062A\u062D\u062A\u0627\u062C\u0643.`:`Next: ${lowerFirst3(now3.then??"")} I will put it in Waiting for you when it needs you.`;return[first2,tries,then].filter(Boolean)}return now3.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. ${now3.sentence}`]:ar?[stateAr(t,now3,"\u0644\u0627 \u062A\u0648\u062C\u062F \u0645\u0634\u0643\u0644\u0629. ")]:[`No problem. ${now3.sentence}`]}function stateAr(t,now3,lead=""){switch(now3.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,now3,book,lang2){let ar=lang2==="ar";if(now3.state==="working"&&now3.readyIn){if(slowNow(now3,book,t)){let who4=ar?WHO_AR[now3.role??"builder"]:now3.role==="squad"?"squad-kit":`The ${now3.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(now3.seconds??0)}.`]:[`It is taking longer than usual, so it is hard to say. ${now3.slow?.text??`${who4} has been on this step for ${minutesWords(now3.seconds??0)}.`}`]}let what=now3.readyIn.what;return t.track==="auto"?ar?[`\u063A\u0627\u0644\u0628\u064B\u0627 ${arIn(now3.readyIn.seconds)}. \u0628\u0639\u062F\u0647\u0627 ${MERGES_AR}`]:[`Probably ${inAbout(now3.readyIn.seconds)}. Then it is accepted and merged by itself.`]:ar?[`\u063A\u0627\u0644\u0628\u064B\u0627 ${arIn(now3.readyIn.seconds)}. \u0628\u0639\u062F\u0647\u0627 \u0633\u0646\u0637\u0644\u0628 \u0645\u0646\u0643 ${NEED_AR[what]??what}.`]:[`Probably ${inAbout(now3.readyIn.seconds)}. Then you will be asked to ${what}.`]}return now3.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")}.`]:now3.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."]:now3.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"}.`]:now3.state==="done"?ar?[`\u0635\u062F\u0631\u062A${t.release?` \u0641\u064A ${t.release}`:""}.`]:[now3.sentence]:now3.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,now3)]:[now3.sentence]}function doingLines(t,now3,lang2){if(lang2==="en")return[now3.sentence,...now3.state==="working"&&now3.doing?[`Right now it is ${now3.doing}.`]:[]];if(now3.state!=="working")return[stateAr(t,now3)];let who4=WHO_AR[now3.role??"builder"],title2=now3.part?t.steps?.[now3.part.n-1]?.title:void 0;return[now3.part&&title2?`${who4} \u0641\u064A \u0627\u0644\u062E\u0637\u0648\u0629 ${now3.part.n} \u0645\u0646 ${now3.part.of}: ${title2}.`:SENTENCE_AR[now3.step??"build"],...now3.doing?[`\u0627\u0644\u0622\u0646 ${doingAr(now3.doing)}.`]:[]]}function nextLines(t,now3,lang2){let ar=lang2==="ar";return now3.state==="working"&&now3.readyIn?ar?t.track==="auto"?[thenAr(t,now3),`\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(now3.readyIn.seconds)}. \u0644\u0627 \u0634\u064A\u0621 \u0645\u0637\u0644\u0648\u0628 \u0645\u0646\u0643 \u0627\u0644\u0622\u0646.`]:[thenAr(t,now3),`\u0628\u0639\u062F\u0647\u0627 \u0633\u0646\u0637\u0644\u0628 \u0645\u0646\u0643 ${NEED_AR[now3.readyIn.what]??now3.readyIn.what} ${arIn(now3.readyIn.seconds)}. \u0644\u0627 \u0634\u064A\u0621 \u0645\u0637\u0644\u0648\u0628 \u0645\u0646\u0643 \u0627\u0644\u0622\u0646.`]:[now3.next??""]:now3.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}.`]:[now3.next??now3.sentence]:ar?[stateAr(t,now3)]:[now3.next??now3.sentence]}function answerAsk(t,now3,book,question){let{topic,lang:lang2}=askTopic(question),ar=lang2==="ar",who4=agentOf(now3),to=now3.state==="working"&&now3.role&&now3.role!=="squad"?now3.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,now3):now3.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,now3,book,lang2),now3.state==="working"&&!now3.slow?[stop]:[]);case"when":return answer2(whenLines(t,now3,book,lang2));case"doing":return answer2(doingLines(t,now3,lang2));case"next":return answer2(nextLines(t,now3,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 path151 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))}var READING=/^Reading the changes \(round (\d+) of/;function reviewRound(log3,run7){if(!run7.from)return;let lo=ms(run7.from)-6e4,hi=ms(run7.to),m=log3.find(a=>a.who==="reviewer"&&ms(a.at)>=lo&&ms(a.at)<=hi&&READING.test(a.text))?.text.match(READING);return m?Number(m[1]):void 0}function plural4(n,one2,many2=`${one2}s`){return`${n} ${n===1?one2:many2}`}function analystSummary(log3,run7){let end=ms(run7.line?.at??run7.to);return log3.filter(a=>a.who==="analyst"&&Math.abs(ms(a.at)-end)<=5e3&&a!==run7.line&&!TOOLISH.test(a.text)).at(-1)?.text}function summaryOf(t,run7,log3){let line3=run7.line?.text??"";switch(run7.step){case"questions":{let n=t.questions?.length??Number(/^Asked (\d+)/.exec(line3)?.[1]??0),read7=analystSummary(log3,run7),asked=n?`asked you ${plural4(n,"question")}`:"had no questions";return read7?`${read7.replace(/\.?$/,".")} Then ${asked}.`:`Read the ticket and ${asked}.`}case"spec":{if(/^Updated the spec: /.test(line3))return line3.replace(/\.?$/,".");let answers2=(t.questions??[]).filter(q5=>q5.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):path151.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,run7,files,conv){switch(run7.step){case"questions":return t.questions?.length?{kind:"questions",items:t.questions.map(q5=>({from:q5.from,text:q5.text,...q5.answer?{answer:q5.answer.value?`${q5.answer.choice.replace(/:$/,"")}: ${q5.answer.value}`:q5.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=run7.step==="fix"||!files?filesFrom(conv,conv?.cwd,run7.from,run7.to):null,got=mine2?{...mine2,removed:[]}:files,steps=run7.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=run7.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 now3=src.now??Date.now(),log3=oldestFirst(t),runs2=runsOf2(t,log3),read7=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,read7(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((run7,i)=>{let next=runs2[i+1],session=SESSION[run7.step],c=conv(session),isLatest=latest.get(run7.step)===i,seconds2=run7.from?Math.max(0,(ms(run7.to)-ms(run7.from))/1e3):void 0,notes=[],until=next?ms(next.from??next.to):1/0;for(let a of AFTER[run7.step]??[])log3.some(l=>a.who.includes(l.who)&&a.re.test(l.text)&&ms(l.at)>=ms(run7.to)-1e3&&ms(l.at)<=until)&&notes.push(a.note);if(run7.step==="review"){let k=runs2.slice(0,i+1).filter(r=>r.step==="review").length,round=reviewRound(log3,run7),fresh=round!==void 0?round===1:k===1;notes.push(reviews>1?`round ${k} of ${reviews}, ${fresh?"a fresh session":"the same Reviewer as the round before"}`:"a second, separate session")}let got=isLatest?handedIn(t,run7,run7.step==="build"?ticketFiles():null,c):void 0,changed=got?.kind==="files"?got.added.length+got.changed.length+got.removed.length:0,n=counts2(c,run7.from,run7.to),tab=TAB[run7.step];return{who:run7.who,step:run7.step,name:`${WHO[run7.who]} \xB7 ${STEP_NAME[run7.step]}`,...run7.from?{from:run7.from}:{},to:run7.to,...seconds2!==void 0?{seconds:seconds2}:{},summary:run7.step==="build"&&changed?summaryOf(t,run7,log3).replace(/\.$/,` and changed ${plural4(changed,"file")}.`):summaryOf(t,run7,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,(now3-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 createHash13}from"crypto";import fs160 from"fs";import path152 from"path";function docsHash(bp,id2){let said=["spec.md","design.md","questions.md","proof.md"].flatMap(f=>{try{return[f,fs160.readFileSync(path152.join(ticketDir(bp,id2),f),"utf8")]}catch{return[]}});return said.length?createHash13("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=createHash13("sha256"),built=!1;for(let c of copiesOf(meta,t)){if(!c.baseCommit||!fs160.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=path152.join(c.dir,file3),seen=await fs160.promises.stat(full);hash2.update(seen.size>MOST_HASHED?`${seen.size}:${seen.mtimeMs}`:await fs160.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(q5=>q5.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,left2){t.stage=left2.stage,t.status=left2.status,t.waiting=left2.waiting,t.live=!1,left2.phase&&!left2.waiting&&(t.live=!0,scheduleSquad(t,left2.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,left2=taken?.free;if(!taken||!left2)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===left2.docs&&work.hash===left2.work&&!left2.approved)return asItWas(t,left2);t.outside=t.outside?{...t.outside,checked:void 0}:{...left2.from?{from:left2.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?(left2.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"),left2.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 left2=t.takenOver?.free;if(!left2)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()},left2.approved=!0,what==="design"&&(left2.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 fs161 from"fs";import path153 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
1907
+ `}return at3.body.trim()&&out20.push(at3),out20}var boxesOf=body10=>[...body10.matchAll(/^\s*[-*]\s+\[[ xX]\]\s+(.+)$/gm)].map(m=>m[1].trim()),namedFrom=file3=>path150.basename(file3).replace(/\.[^.]+$/,"");function fromMarkdown(source2,text3,asRules){return sectionsOf2(text3,namedFrom(source2)).flatMap((s,i)=>{let key=`${source2}#${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(q5=>scrubSecrets(q5).slice(0,MAX_TYPED_CHARS));return[{key,kind:"checklist",name,source:source2,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:source2,text:body10,...tooLong,...ours}]})}function fromOurFormat(source2,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=`${source2}#${i}`;return name?a.kind==="rule"&&typeof a.text=="string"?[{key,kind:"rule",name,source:source2,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:source2,questions:a.questions.filter(q5=>typeof q5=="string").slice(0,MAX_CHECKLIST_QUESTIONS).map(q5=>scrubSecrets(q5))}]:a.kind==="gate"&&typeof a.command=="string"?[{key,kind:"gate",name,source:source2,command:a.command.trim(),when:["checks","merge","release"].includes(a.when)?a.when:"checks"}]:[]:[]}):[]}function runnerFor(root){return fs159.existsSync(path150.join(root,"pnpm-lock.yaml"))?s=>`pnpm run ${s}`:fs159.existsSync(path150.join(root,"yarn.lock"))?s=>`yarn ${s}`:s=>`npm run ${s}`}function fromPackage(root,source2,text3,commands){let scripts;try{scripts=JSON.parse(text3).scripts??{}}catch{return[]}let run7=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:`${source2}#${s}`,kind:"gate",name:s,source:source2,command:run7(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=fs159.realpathSync(path150.resolve(folder)),!fs159.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=[],left2=0,seen=0,looked3=0,more3=!1,walk3=(dir,depth)=>{let entries;try{entries=fs159.readdirSync(dir,{withFileTypes:!0})}catch{left2+=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=path150.join(dir,e.name),source2=path150.relative(root,at3).split(path150.sep).join("/");if(HOLDS_KEYS.test(e.name)||NOT_LOOKED_IN.has(e.name)){left2+=1;continue}let real2=at3;if(e.isSymbolicLink()){try{real2=fs159.realpathSync(at3)}catch{real2=""}if(!real2||!`${real2}${path150.sep}`.startsWith(`${root}${path150.sep}`)){left2+=1;continue}}let stat;try{stat=fs159.statSync(real2)}catch{left2+=1;continue}if(stat.isDirectory()){depth<DEEPEST&&!e.isSymbolicLink()&&walk3(at3,depth+1);continue}if(!stat.isFile())continue;let isScriptCheck=source2==="scripts/check",isMarkdown=/\.(md|mdc|markdown)$/i.test(e.name),isOurs=source2==="harness.yaml"||source2===".squad/harness.yaml";if(!isMarkdown&&!isOurs&&!isScriptCheck&&source2!=="package.json"&&!(/(^|\/)rules\//.test(source2)&&/\.txt$/i.test(e.name)))continue;if((seen+=1)>MAX_FOLDER_FILES){more3=!0;return}let text3=null;try{text3=textOf2(real2)}catch{}if(text3===null){left2+=1;continue}isOurs?finds.push(...fromOurFormat(source2,text3)):source2==="package.json"?finds.push(...fromPackage(root,source2,text3,commands)):isScriptCheck?finds.push({key:source2,kind:"gate",name:"scripts/check",source:source2,command:"./scripts/check",when:"checks"}):finds.push(...fromMarkdown(source2,text3,/^(AGENTS|CLAUDE)\.md$/.test(e.name)||/(^|\/)rules\//.test(source2)))}};walk3(root,0);let named=new Set(have.map(a=>`${a.kind}\0${a.name.trim().toLowerCase()}`));return{name:path150.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:left2,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/},PASSED2={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,now3=Date.now()){let from=now3-SEEN_DAYS*ONE_DAY,week=now3-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(PASSED2).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(PASSED2))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,about2,when2,gates)=>({id:id2,name,about:about2,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(q5=>QUALITY_PRESETS[q5].reviewerFloor).map(q5=>q5.charAt(0).toUpperCase()+q5.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("flutter-stamps","Flutter's own version stamps","Flutter writes its version in a few small files of its own folder on every run. Those files, and nothing else of Flutter, are open to the squad's commands."),g("xcode","iPhone builds",OUTSIDE_THE_GUARD_WORDS),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("phone-pictures","iPhone apps on a real iPhone","On a Mac, an iPhone app's pictures come from squad-kit's own hidden iPhone and iPad, each named with its device. With its tools missing, pictures of the web version stand in, and the story says so.","With no web version either, it waits for the tools and goes on once they are there. An app that does not build or a phone that does not start: a card, and you decide."),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):[],run7=(...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=list6=>list6?.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",run7("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"]),"flutter-stamps":code("engine/flutter-sdk.ts",...FLUTTER_STAMPS.map(f=>[f.file,f.why]),["In",flutterOnPath()?.root??"No Flutter on this computer"],["On Linux","Each command writes them in a throwaway bin/cache that goes when it ends, so Flutter's own folder stays as it was"]),xcode:code("engine/checks.ts",["Runs outside the guard","xcodebuild build and test, only as squad-kit writes them for a native iPhone app (outsideTheGuard)"],["Keeps","Xcode's own sandboxes: for a package's manifest and plugins, and for build scripts when the project turns that on"],["Builds in","build/squad-kit-ios in the story's copy, with the guard's caches"]),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",run7("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",run7("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",run7("setup")],["Looks at","Package files and database changes, saved or not"]),"test-change":code("engine/test-change.ts",["Runs",run7("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",run7("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`]),"phone-pictures":code("engine/phone/session.ts",["Devices","squad-kit iPhone and squad-kit iPad, its own, one started at a time"],["Taps with","Maestro"]),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",run7("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&&!partsOf2(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)?partsOf2(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(),q6=s2.replace(AR_POLITE,"");return q6!==s2?{topic:AR.find(([,re])=>re.test(q6))?.[0]??"other",lang:"ar"}:AR_DO.test(q6)?{topic:"change",lang:"ar"}:{topic:AR.find(([,re])=>re.test(q6))?.[0]??"other",lang:"ar"}}let s=text3.toLowerCase().replace(/[’`]/g,"'").replace(/[?!.,;:]+/g," ").replace(/\s+/g," ").trim(),q5=s.replace(EN_POLITE,"");if(q5===s&&EN_DO.test(q5))return{topic:"change",lang:"en"};let found2=EN.find(([,re])=>re.test(q5))?.[0];return found2?{topic:found2,lang:"en"}:{topic:q5===s&&EN_ASK.test(q5)?"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,now3){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=now3.part;switch(now3.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(now3){return now3.usual?Math.max(60,middle(now3.usual)-(now3.seconds??0)):60}function noteWhenAr(t,now3){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(now3.state){case"working":return now3.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(now3))}.`:`\u064A\u0642\u0631\u0624\u0647\u0627 ${WHO_AR[now3.role??"builder"]} \u0639\u0646\u062F \u0628\u062F\u0621 \u062E\u0637\u0648\u062A\u0647 \u0627\u0644\u062A\u0627\u0644\u064A\u0629\u060C ${arIn(noteLeft(now3))}.`;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(now3){return now3.role&&now3.role!=="squad"?now3.role:"analyst"}function slowNow(now3,book,t){return now3.slow?!0:!now3.step||!now3.role||now3.seconds===void 0?!1:now3.seconds>=slowAfter(usualOf(book,now3.role,now3.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,now3,book,lang2){let ar=lang2==="ar",secs=now3.seconds;if(now3.state==="working"){let who4=ar?WHO_AR[now3.role??"builder"]:`The ${now3.who}`;if(now3.slow?.kind==="stuck"){let quiet=now3.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(now3,book,t)){if(!now3.slow&&!ar)return[`It is slower than usual, but still working. ${now3.role==="squad"?"squad-kit":who4} has been on this step for ${minutesWords(secs??0)}. It usually takes ${rangeEn(now3.usual)}.`,"You can give it more time, stop and try again, or stop it."];if(ar)return[`\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(now3.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."];let s=now3.slow;return[`${s.calm?"It is taking longer than usual, but it is still working.":"It is taking longer than usual, with no update for a few minutes."} ${s.why?.sentence??s.text}`,s.limit??"",s.calm?"You don't need to do anything. If you want, the card above can give it 10 more minutes, or stop it.":"You can give it 10 more minutes, stop and try again, or stop it from the card above."]}let p=now3.part,u=now3.usual,checks=now3.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,now3)} \u0633\u062A\u0638\u0647\u0631 \u0641\u064A \xABWaiting for you\xBB \u0639\u0646\u062F\u0645\u0627 \u062A\u062D\u062A\u0627\u062C\u0643.`:`Next: ${lowerFirst3(now3.then??"")} I will put it in Waiting for you when it needs you.`;return[first2,tries,then].filter(Boolean)}return now3.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. ${now3.sentence}`]:ar?[stateAr(t,now3,"\u0644\u0627 \u062A\u0648\u062C\u062F \u0645\u0634\u0643\u0644\u0629. ")]:[`No problem. ${now3.sentence}`]}function stateAr(t,now3,lead=""){switch(now3.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,now3,book,lang2){let ar=lang2==="ar";if(now3.state==="working"&&now3.readyIn){if(slowNow(now3,book,t)){let who4=ar?WHO_AR[now3.role??"builder"]:now3.role==="squad"?"squad-kit":`The ${now3.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(now3.seconds??0)}.`]:[`It is taking longer than usual, so it is hard to say. ${now3.slow?.why?.sentence??now3.slow?.text??`${who4} has been on this step for ${minutesWords(now3.seconds??0)}.`}`,now3.slow?.limit??""].filter(Boolean)}let what=now3.readyIn.what;return t.track==="auto"?ar?[`\u063A\u0627\u0644\u0628\u064B\u0627 ${arIn(now3.readyIn.seconds)}. \u0628\u0639\u062F\u0647\u0627 ${MERGES_AR}`]:[`Probably ${inAbout(now3.readyIn.seconds)}. Then it is accepted and merged by itself.`]:ar?[`\u063A\u0627\u0644\u0628\u064B\u0627 ${arIn(now3.readyIn.seconds)}. \u0628\u0639\u062F\u0647\u0627 \u0633\u0646\u0637\u0644\u0628 \u0645\u0646\u0643 ${NEED_AR[what]??what}.`]:[`Probably ${inAbout(now3.readyIn.seconds)}. Then you will be asked to ${what}.`]}return now3.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")}.`]:now3.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."]:now3.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"}.`]:now3.state==="done"?ar?[`\u0635\u062F\u0631\u062A${t.release?` \u0641\u064A ${t.release}`:""}.`]:[now3.sentence]:now3.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,now3)]:[now3.sentence]}function doingLines(t,now3,lang2){if(lang2==="en")return[now3.sentence,...now3.state==="working"&&now3.doing?[`Right now it is ${now3.doing}.`]:[]];if(now3.state!=="working")return[stateAr(t,now3)];let who4=WHO_AR[now3.role??"builder"],title2=now3.part?t.steps?.[now3.part.n-1]?.title:void 0;return[now3.part&&title2?`${who4} \u0641\u064A \u0627\u0644\u062E\u0637\u0648\u0629 ${now3.part.n} \u0645\u0646 ${now3.part.of}: ${title2}.`:SENTENCE_AR[now3.step??"build"],...now3.doing?[`\u0627\u0644\u0622\u0646 ${doingAr(now3.doing)}.`]:[]]}function nextLines(t,now3,lang2){let ar=lang2==="ar";return now3.state==="working"&&now3.readyIn?ar?t.track==="auto"?[thenAr(t,now3),`\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(now3.readyIn.seconds)}. \u0644\u0627 \u0634\u064A\u0621 \u0645\u0637\u0644\u0648\u0628 \u0645\u0646\u0643 \u0627\u0644\u0622\u0646.`]:[thenAr(t,now3),`\u0628\u0639\u062F\u0647\u0627 \u0633\u0646\u0637\u0644\u0628 \u0645\u0646\u0643 ${NEED_AR[now3.readyIn.what]??now3.readyIn.what} ${arIn(now3.readyIn.seconds)}. \u0644\u0627 \u0634\u064A\u0621 \u0645\u0637\u0644\u0648\u0628 \u0645\u0646\u0643 \u0627\u0644\u0622\u0646.`]:[now3.next??""]:now3.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}.`]:[now3.next??now3.sentence]:ar?[stateAr(t,now3)]:[now3.next??now3.sentence]}function answerAsk(t,now3,book,question){let{topic,lang:lang2}=askTopic(question),ar=lang2==="ar",who4=agentOf(now3),to=now3.state==="working"&&now3.role&&now3.role!=="squad"?now3.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,now3):now3.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,now3,book,lang2),now3.state==="working"&&!now3.slow?[stop]:[]);case"when":return answer2(whenLines(t,now3,book,lang2));case"doing":return answer2(doingLines(t,now3,lang2));case"next":return answer2(nextLines(t,now3,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 path151 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))}var READING=/^Reading the changes \(round (\d+) of/;function reviewRound(log3,run7){if(!run7.from)return;let lo=ms(run7.from)-6e4,hi=ms(run7.to),m=log3.find(a=>a.who==="reviewer"&&ms(a.at)>=lo&&ms(a.at)<=hi&&READING.test(a.text))?.text.match(READING);return m?Number(m[1]):void 0}function plural4(n,one2,many2=`${one2}s`){return`${n} ${n===1?one2:many2}`}function analystSummary(log3,run7){let end=ms(run7.line?.at??run7.to);return log3.filter(a=>a.who==="analyst"&&Math.abs(ms(a.at)-end)<=5e3&&a!==run7.line&&!TOOLISH.test(a.text)).at(-1)?.text}function summaryOf(t,run7,log3){let line3=run7.line?.text??"";switch(run7.step){case"questions":{let n=t.questions?.length??Number(/^Asked (\d+)/.exec(line3)?.[1]??0),read7=analystSummary(log3,run7),asked=n?`asked you ${plural4(n,"question")}`:"had no questions";return read7?`${read7.replace(/\.?$/,".")} Then ${asked}.`:`Read the ticket and ${asked}.`}case"spec":{if(/^Updated the spec: /.test(line3))return line3.replace(/\.?$/,".");let answers2=(t.questions??[]).filter(q5=>q5.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):path151.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,run7,files,conv){switch(run7.step){case"questions":return t.questions?.length?{kind:"questions",items:t.questions.map(q5=>({from:q5.from,text:q5.text,...q5.answer?{answer:q5.answer.value?`${q5.answer.choice.replace(/:$/,"")}: ${q5.answer.value}`:q5.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=run7.step==="fix"||!files?filesFrom(conv,conv?.cwd,run7.from,run7.to):null,got=mine2?{...mine2,removed:[]}:files,steps=run7.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=run7.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 now3=src.now??Date.now(),log3=oldestFirst(t),runs2=runsOf2(t,log3),read7=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,read7(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((run7,i)=>{let next=runs2[i+1],session=SESSION[run7.step],c=conv(session),isLatest=latest.get(run7.step)===i,seconds2=run7.from?Math.max(0,(ms(run7.to)-ms(run7.from))/1e3):void 0,notes=[],until=next?ms(next.from??next.to):1/0;for(let a of AFTER[run7.step]??[])log3.some(l=>a.who.includes(l.who)&&a.re.test(l.text)&&ms(l.at)>=ms(run7.to)-1e3&&ms(l.at)<=until)&&notes.push(a.note);if(run7.step==="review"){let k=runs2.slice(0,i+1).filter(r=>r.step==="review").length,round=reviewRound(log3,run7),fresh=round!==void 0?round===1:k===1;notes.push(reviews>1?`round ${k} of ${reviews}, ${fresh?"a fresh session":"the same Reviewer as the round before"}`:"a second, separate session")}let got=isLatest?handedIn(t,run7,run7.step==="build"?ticketFiles():null,c):void 0,changed=got?.kind==="files"?got.added.length+got.changed.length+got.removed.length:0,n=counts2(c,run7.from,run7.to),tab=TAB[run7.step];return{who:run7.who,step:run7.step,name:`${WHO[run7.who]} \xB7 ${STEP_NAME[run7.step]}`,...run7.from?{from:run7.from}:{},to:run7.to,...seconds2!==void 0?{seconds:seconds2}:{},summary:run7.step==="build"&&changed?summaryOf(t,run7,log3).replace(/\.$/,` and changed ${plural4(changed,"file")}.`):summaryOf(t,run7,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,(now3-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 createHash13}from"crypto";import fs160 from"fs";import path152 from"path";function docsHash(bp,id2){let said=["spec.md","design.md","questions.md","proof.md"].flatMap(f=>{try{return[f,fs160.readFileSync(path152.join(ticketDir(bp,id2),f),"utf8")]}catch{return[]}});return said.length?createHash13("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=createHash13("sha256"),built=!1;for(let c of copiesOf(meta,t)){if(!c.baseCommit||!fs160.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=path152.join(c.dir,file3),seen=await fs160.promises.stat(full);hash2.update(seen.size>MOST_HASHED?`${seen.size}:${seen.mtimeMs}`:await fs160.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(q5=>q5.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,left2){t.stage=left2.stage,t.status=left2.status,t.waiting=left2.waiting,t.live=!1,left2.phase&&!left2.waiting&&(t.live=!0,scheduleSquad(t,left2.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,left2=taken?.free;if(!taken||!left2)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===left2.docs&&work.hash===left2.work&&!left2.approved)return asItWas(t,left2);t.outside=t.outside?{...t.outside,checked:void 0}:{...left2.from?{from:left2.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?(left2.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"),left2.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 left2=t.takenOver?.free;if(!left2)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()},left2.approved=!0,what==="design"&&(left2.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 fs161 from"fs";import path153 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
1908
1908
  ${SAMPLE_DOCS[file3].trimEnd()}
1909
1909
  \`\`\`\``}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(`
1910
1910
  `)}function guideText(bp){return`# How to work on this project in a conversation
@@ -2019,12 +2019,12 @@ Next: ${next}
2019
2019
 
2020
2020
  Go on through the steps from there. Stop and ask me where the guide says to. When we are done, or when I tell you, hand it back: squad outside back ${t.id}`}function lookAtFiles(bp,id2){let folder=ticketDir(bp,id2),known=["ticket.md","spec.md","questions.md","design.md","notes.md","proof.md"],docs={},problems=[];if(!fs161.existsSync(folder))return{ticket:null,problems:[{file:id2.toUpperCase(),problem:"There is no story with that id."}]};for(let e of fs161.readdirSync(folder))known.includes(e)?docs[e]=fs161.readFileSync(path153.join(folder,e),"utf8").replace(/\r\n/g,`
2021
2021
  `):e.endsWith(".md")&&problems.push({file:e,problem:"squad-kit does not read this file, so what it says will not show on the board. Its files are spec.md, questions.md and design.md."});let ticket=null;try{ticket=readTicket(bp,id2)}catch(err2){if(!(err2 instanceof UnreadableTicket))throw err2;problems.push({file:err2.file,problem:err2.detail})}return problems.push(...shapeTroubles(docs)),{ticket,problems}}var DAY3=864e5,SESSION_ROLE={intake:"analyst",build:"builder",review:"reviewer",tester:"tester"},MOCKUP_SECRET=randomBytes9(32);function mockupKey(id2,file3){return createHmac2("sha256",MOCKUP_SECRET).update(`${id2}\0${file3}`).digest("hex")}function mockupFile(bp,id2,asked){let t=readTicket(bp,id2);if(!t||!/\.html?$/i.test(asked))return null;let file3=allowedFile(asked.replace(/^file:\/\//,""),ticketFolders(t,bp));return file3&&fs162.statSync(file3).size<=5*1024*1024?file3:null}function withNotes(bp,r){return r.status==="draft"&&!r.notes.trim()?{...r,notes:composeNotes(bp,r)}:r}function checkWhereCodeGoes(dir,base2,remote){if(!isRepo(dir))return;let remotesHere=remotes(dir);if(remote&&!remotesHere.includes(remote))throw new Error(`There is no remote called "${remote}" here. ${remotesHere.length?`It has: ${remotesHere.join(", ")}.`:"It has none yet."} Nothing was saved.`);let branches=git(dir,["for-each-ref","--format=%(refname:short)","refs/heads",...remote?[`refs/remotes/${remote}`]:[]],{allowFail:!0}).split(`
2022
- `).map(b=>b.replace(`${remote}/`,"")).filter(b=>b&&b!=="HEAD"&&b!==remote);if(branches.length&&!branches.includes(base2))throw new Error(`There is no branch called "${base2}" here. It has: ${[...new Set(branches)].slice(0,8).join(", ")}. Nothing was saved.`)}async function body3(c){try{return await c.req.json()}catch{return{}}}function mountBoardApi(app,opts){let bp=boardPaths(opts.paths.squadDir),tried=t=>(lookAgain(bp),t),nowOf=(t,usual)=>liveNow(t,usual,Date.now(),isSaving(bp,t.id)),connection=null;async function trackerStatus(){let tracker2=trackerFor(bp);if(!tracker2)return{tracker:null};let key=`${tracker2.name}/${tracker2.identity}`;if(!connection||connection.key!==key||Date.now()-connection.at>6e4)try{connection={at:Date.now(),key,user:await tracker2.whoAmI()}}catch(err2){connection={at:Date.now(),key,error:plainWords(err2,"Checking the tracker sign-in")}}return{tracker:connection.error?null:tracker2,user:connection.user,error:connection.error}}app.get("/api/v1/tracker",async c=>{let config2=fs162.existsSync(opts.paths.configFile)?loadConfig(opts.paths.configFile):null,root2=path154.dirname(opts.paths.squadDir),type=config2?.tracker.type??"none",mod=trackerModule(type),status=mod?await trackerStatus():{user:void 0,error:void 0},dirs=[root2,...findNestedRepos(root2).map(r=>path154.join(root2,r.path))],guessed=guessTracker(dirs.map(d=>git(d,["config","--get","remote.origin.url"],{allowFail:!0}))),saved=mod?mod.read(root2):{values:{},hasSecret:!1},fieldsOf=mod??guessed?.module??TRACKERS[0],values=Object.fromEntries(fieldsOf.fields.filter(f=>!f.secret).map(f=>[f.key,saved.values[f.key]||guessed?.values[f.key]||""]));return c.json({type,module:fieldsOf.name,label:fieldsOf.label,fields:fieldsOf.fields,...values,hasToken:saved.hasSecret,user:status.user,error:status.error})}),app.put("/api/v1/tracker",async c=>{let b=await body3(c);return act(c,async()=>{let config2=loadConfig(opts.paths.configFile);(trackerModule(b.module??config2.tracker.type)??TRACKERS[0]).save(path154.dirname(opts.paths.squadDir),b),connection=null;let status=await trackerStatus();if(status.error)throw new Error(status.error);return{ok:!0,user:status.user}})});let root=path154.dirname(opts.paths.squadDir);app.get("/api/v1/speech",c=>c.json({hasKey:speechModule().hasKey(root),label:speechModule().label,video:videoNotesWork()})),app.put("/api/v1/speech",async c=>{let b=await body3(c);return act(c,async()=>{let speech=speechModule();if(b.remove)return speech.removeKey(root),{ok:!0,hasKey:speech.hasKey(root)};let key=(b.key??b.munsit)?.trim();if(!key)throw new Error(`Paste your ${speech.label} key first.`);let said=await speech.checkKey(key);if(said==="wrong")throw new BoardActionError(`${speech.label} did not accept this key, so it was not saved. Copy it again from your ${speech.label} account.`);return speech.saveKey(root,key),{ok:!0,hasKey:!0,...said==="no-answer"?{unchecked:!0}:{}}})}),app.get("/api/v1/backlog",async c=>{let status=await trackerStatus(),tracker2=status.tracker;if(!tracker2)return c.json({connected:!1,error:status.error??null,sprints:[],items:[]});try{let sprints=tracker2.abilities.sprints?await tracker2.sprints():[],asked=c.req.query("sprint"),sprint=asked==="all"?null:sprints.find(s=>s.path===asked)??sprints.find(s=>s.current)??sprints[0]??null,items3=await tracker2.list(sprint?{sprint:sprint.path}:{}),onBoard=new Map(listTickets(bp).filter(t=>t.source.type===tracker2.name).map(t=>[t.source.id??tracker2.parseRef(t.source.ref??""),t]));return c.json({connected:!0,user:status.user,sprints,sprint:sprint?.path??"all",items:items3.map(w=>{let t=onBoard.get(w.id);return{...w,ticket:t?{id:t.id,stage:t.stage,status:t.status}:null}})})}catch(err2){return c.json({connected:!0,error:plainWords(err2,"Reading the backlog"),sprints:[],items:[]})}}),app.post("/api/v1/backlog/:id/start",async c=>act(c,async()=>{let status=await trackerStatus();if(!status.tracker)throw new Error(status.error??"No tracker is connected. Add it in Settings.");return startFromTracker(bp,status.tracker,c.req.param("id"))})),app.get("/api/v1/board",c=>{if(!boardExists(bp)){let settings=settingsProblem(bp);return c.json({ready:!1,...settings?{settings}:{}})}let meta=readMeta(bp),all=listTickets(bp),broken=brokenTickets(bp,all),records=[...brokenDecisions(bp),...brokenReleases(bp),...brokenBreakdowns(bp)],recent=listReleases(bp).filter(r=>r.status==="released"&&r.date&&Date.now()-Date.parse(r.date)<30*DAY3),ctl=opts.control,usual=readUsual(bp);return c.json({ready:!0,meta,console:ctl?{startedAt:ctl.startedAt,outdated:ctl.build>0&&buildStamp()>ctl.build}:null,tickets:all.filter(t=>t.stage!=="done"&&!t.archived).map(t=>({...t,now:nowOf(t,usual),...toolWaitOf(bp,t)})),archived:all.filter(t=>t.stage!=="done"&&t.archived),...broken.length?{broken}:{},...records.length?{brokenRecords:records}:{},stats:{released30:recent.reduce((n,r)=>n+r.tickets.length,0),bugs30:recent.reduce((n,r)=>n+(r.bugs?.length??0),0),open:all.filter(t=>t.stage!=="done"&&!t.mergedAt&&!t.archived).length}})}),app.get("/api/v1/tickets/:id",c=>{let problem2=ticketProblem(bp,c.req.param("id"));if(problem2)return c.json({error:problem2},409);let t=readTicket(bp,c.req.param("id"));return t?c.json({...withProblem(t),now:nowOf(t,readUsual(bp)),...toolWaitOf(bp,t,!0)}):c.json({error:"not found"},404)}),app.get("/api/v1/tickets/:id/status",c=>{let t=readTicket(bp,c.req.param("id"));return t?c.json(nowOf(t,readUsual(bp))):c.json({error:"not found"},404)}),app.post("/api/v1/tickets/:id/status/ask",async c=>{let b=await body3(c);return act(c,()=>{let t=readTicket(bp,c.req.param("id"));if(!t)throw new Error(`No ticket ${c.req.param("id").toUpperCase()} on this board.`);let text3=(b.text??"").trim();if(!text3)throw new Error("Write your question first.");let usual=readUsual(bp);return answerAsk(t,nowOf(t,usual),usual,text3)})}),app.get("/api/v1/tickets/:id/conversations",c=>{let t=readTicket(bp,c.req.param("id"));return t?c.json(ticketConversations(t,void 0,bp.chatsDir)):c.json({error:"not found"},404)}),app.get("/api/v1/tickets/:id/conversations/:key",c=>{let t=readTicket(bp,c.req.param("id")),key=c.req.param("key"),id2=key==="chat"?t?.chat?.side:t?.sessions?.[key];if(!id2)return c.json({error:"not found"},404);let conv=readConversation(id2,Math.min(1e3,Number(c.req.query("limit"))||400),void 0,bp.chatsDir),role=key==="chat"?t?.chat?.with??"analyst":SESSION_ROLE[key]??"builder",tool5=aiTool(readAgent(bp,role).tool),open3=tool5?.resumeCommand?{command:tool5.resumeCommand(id2,conv?.cwd),tool:tool5.label}:{};return conv?c.json({...conv,protocol:"acp/v1",...open3}):c.json({sessionId:id2,updates:[],total:0,...open3})}),app.get("/api/v1/tickets/:id/file",c=>{let t=readTicket(bp,c.req.param("id")),asked=c.req.query("path")??"",file3=t&&asked?allowedFile(asked.replace(/^file:\/\//,""),ticketFolders(t,bp)):null;if(!file3)return c.text("not found",404);let size2=fs162.statSync(file3).size;if(size2>100*1024*1024)return c.text("too large",413);let type=mimeOf(file3),inline2=/^(image|video|audio)\/|^application\/pdf$|^text\//.test(type)&&type!=="image/svg+xml"&&type!=="text/html";return new Response(fs162.readFileSync(file3),{headers:{"content-type":type,"content-length":String(size2),"content-disposition":dispositionOf(inline2?"inline":"attachment",file3),"x-content-type-options":"nosniff","cache-control":"private, max-age=300"}})}),app.get("/api/v1/tickets/:id/mockup-link",c=>{let id2=c.req.param("id"),file3=mockupFile(bp,id2,c.req.query("path")??"");return file3?c.json({url:`/mockup/${encodeURIComponent(id2)}?path=${encodeURIComponent(file3)}&k=${mockupKey(id2,file3)}`}):c.json({error:"not found"},404)}),app.get("/mockup/:id",c=>{let id2=c.req.param("id"),file3=mockupFile(bp,id2,c.req.query("path")??""),k=c.req.query("k")??"";return!file3||!timingSafeEqualHex(k,mockupKey(id2,file3))?c.text("not found",404):new Response(fs162.readFileSync(file3),{headers:{"content-type":"text/html; charset=utf-8","cache-control":"private, no-store"}})}),app.post("/api/v1/tickets",async c=>{let b=await body3(c);return act(c,()=>createTicket(bp,{title:b.title??"",ask:b.ask,priority:b.priority,kind:b.kind==="bug"?"bug":"feature",severity:b.severity==="stops-work"||b.severity==="annoying"||b.severity==="small"?b.severity:void 0,track:b.track==="express"?"express":"full",criteria:b.criteria,rules:b.rules,edgeCases:b.edgeCases,files:b.files,talkFirst:!!b.talkFirst,...b.noNewTests===!0?{noNewTests:!0}:{},...typeof b.key=="string"&&b.key?{key:b.key.slice(0,80)}:{}}))}),app.post("/api/v1/tickets/draft",async c=>{let b=await body3(c);return act(c,()=>startDraft(bp,{text:b.text??"",files:b.files}))}),app.get("/api/v1/tickets/draft/current",c=>c.json({draft:currentDraft(bp)})),app.get("/api/v1/tickets/draft/:id",c=>{try{return c.json(draftJob(bp,c.req.param("id")))}catch(err2){return c.json({error:plainWords(err2,"Reading a draft")},404)}}),app.get("/api/v1/tickets/draft/:id/files",c=>act(c,()=>draftFiles(bp,c.req.param("id")))),app.post("/api/v1/tickets/draft/:id/cancel",c=>act(c,()=>cancelDraft(bp,c.req.param("id")))),app.post("/api/v1/tickets/draft/:id/dismiss",c=>act(c,()=>dismissDraft(bp,c.req.param("id")))),app.post("/api/v1/pull",async c=>{let b=await body3(c);return act(c,()=>pullTicket(bp,b.ref??""))}),app.post("/api/v1/tickets/:id/answers",async c=>{let b=await body3(c);return act(c,()=>answerQuestions(bp,c.req.param("id"),b.answers??{}))}),app.post("/api/v1/tickets/:id/decision-update",async c=>{let b=await body3(c),id2=c.req.param("id");return act(c,()=>b.do==="keep"?keepPlan(bp,id2,b.adr??"",b.reason??""):updateForDecision(bp,id2,b.adr??""))}),app.post("/api/v1/tickets/:id/local-setup",c=>act(c,()=>setUpLocalCopy(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/skip",c=>act(c,()=>skipQuestions(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/ask",async c=>{let b=await body3(c);return act(c,()=>askAbout(bp,c.req.param("id"),b.questionId??"",b.text??""))}),app.post("/api/v1/tickets/:id/approve",c=>act(c,async()=>tried(await approveBeside(bp,c.req.param("id"))))),app.post("/api/v1/tickets/:id/stop",c=>act(c,()=>stopTicket(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/start",c=>act(c,()=>startTicket(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/unstick",c=>act(c,()=>endStuckCommand(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/free-port",c=>act(c,()=>freePortAndRetry(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/keep-waiting",async c=>{let b=await body3(c);return act(c,()=>keepWaiting(bp,c.req.param("id"),b.minutes??10))}),app.post("/api/v1/tickets/:id/restart",c=>act(c,()=>restartStep(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/change",async c=>{let b=await body3(c);return act(c,()=>tried(requestChange(bp,c.req.param("id"),b.text??"")))}),app.get("/api/v1/tickets/:id/changes",c=>act(c,()=>ticketChanges(bp,c.req.param("id")))),app.get("/api/v1/editors",c=>c.json({editors:installedEditors()})),app.post("/api/v1/tickets/:id/open-editor",async c=>{let b=await body3(c);return act(c,()=>openInEditor(bp,c.req.param("id"),b.editor))}),app.post("/api/v1/tickets/:id/reply",async c=>{let b=await body3(c);return act(c,()=>answerAgent(bp,c.req.param("id"),b.text??""))}),app.post("/api/v1/tickets/:id/notes",async c=>{let b=await body3(c);return act(c,()=>addNote(bp,c.req.param("id"),{text:b.text??"",kind:b.kind,to:b.to}))}),app.post("/api/v1/tickets/:id/plan/edit",async c=>{let b=await body3(c);return act(c,()=>editPlan(bp,c.req.param("id"),b))}),app.post("/api/v1/tickets/:id/plan/send-back",async c=>{let b=await body3(c);return act(c,()=>sendBack(bp,c.req.param("id"),b.comments??[]))}),app.post("/api/v1/tickets/:id/plan/start-over",async c=>{let b=await body3(c);return act(c,()=>startOver(bp,c.req.param("id"),b.direction??""))}),app.post("/api/v1/tickets/:id/chat/start",async c=>{let b=await body3(c);return act(c,()=>b.ask?askQuestion(bp,c.req.param("id"),b.with):startChat(bp,c.req.param("id"),b.with??"analyst"))}),app.post("/api/v1/tickets/:id/chat/send",async c=>{let b=await body3(c);return act(c,()=>sendChat(bp,c.req.param("id"),b.text??"",b.files??[]))}),app.post("/api/v1/tickets/:id/chat/write",c=>act(c,()=>chatWrite(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/chat/done",c=>act(c,()=>chatDone(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/spec-change",async c=>{let b=await body3(c);try{return c.json(proposeChange(bp,c.req.param("id"),b.text??"",b.from))}catch(err2){return err2 instanceof ReleasedError?c.json({error:err2.message,released:!0,choices:["ticket","bug"]},409):act(c,()=>{throw err2})}}),app.post("/api/v1/tickets/:id/spec-change/approve",c=>act(c,()=>approveChange(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/spec-change/decline",async c=>{let b=await body3(c);return act(c,()=>declineChange(bp,c.req.param("id"),b.reason))}),app.post("/api/v1/tickets/:id/spec-change/retry",c=>act(c,()=>retryChange(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/spec-change/withdraw",async c=>{let b=await body3(c);return act(c,()=>withdrawChange(bp,c.req.param("id"),b.reason))}),app.post("/api/v1/tickets/:id/spec-change/new-ticket",async c=>{let b=await body3(c);return act(c,()=>changeAfterRelease(bp,c.req.param("id"),b.text??"",b.as==="bug"?"bug":"ticket"))}),app.post("/api/v1/tickets/:id/decided/:did",async c=>{let b=await body3(c);return act(c,()=>changeDecided(bp,c.req.param("id"),c.req.param("did"),b.text??""))}),app.post("/api/v1/tickets/:id/chat/stop",c=>act(c,()=>chatStop(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/chat/retry",c=>act(c,()=>chatRetry(bp,c.req.param("id")))),app.get("/api/v1/conversations",c=>c.json(boardExists(bp)?chatSummaries(listTickets(bp),c.req.query("q")??""):[])),app.post("/api/v1/tickets/:id/take-over",c=>act(c,()=>takeOver(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/give-back",async c=>{let b=await body3(c);return act(c,()=>giveBack(bp,c.req.param("id"),b.note))}),app.get("/api/v1/outside-prompt",c=>act(c,()=>({prompt:outsidePrompt(bp)}))),app.get("/api/v1/tickets/:id/outside-prompt",c=>act(c,()=>({prompt:outsidePrompt(bp,requireTicket(bp,c.req.param("id")))}))),app.post("/api/v1/tickets/:id/outside",c=>act(c,async()=>{let t=await workOutside(bp,c.req.param("id"));return{...t,prompt:outsidePrompt(bp,t)}})),app.post("/api/v1/tickets/:id/bring-back",c=>act(c,()=>bringBack2(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/check-outside",c=>act(c,()=>checkOutside(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/hand-over",async c=>{let b=await body3(c);return act(c,()=>handOver(bp,c.req.param("id"),{to:b.to??"",note:b.note}))}),app.post("/api/v1/tickets/:id/take-back",async c=>{let b=await body3(c);return act(c,()=>takeBack(bp,c.req.param("id"),b.reason??""))}),app.post("/api/v1/tickets/:id/decide",async c=>{let b=await body3(c);return act(c,()=>decideStuckBeside(bp,c.req.param("id"),Number(b.choice),{note:b.note,reason:b.reason}))}),app.post("/api/v1/tickets/:id/test-command",async c=>{let b=await body3(c);return act(c,()=>{if(typeof b.use!="boolean")throw new BoardActionError('Say { "use": true } to use the new test command, or { "use": false } to keep the current one.');return decideTestCommand(bp,c.req.param("id"),b.use)})}),app.get("/api/v1/releases",c=>{let tickets=listTickets(bp),meta=readMeta(bp),titleOf=(id2,r)=>tickets.find(t=>t.id===id2)?.title??r.titles?.[id2]??id2,targets=r=>{if(meta.engine!=="live")return[];let seen=new Map;for(let t of tickets.filter(x=>r.tickets.includes(x.id)))for(let view of copyViews(meta,t)){let repo=repoFor(meta,view);seen.set(repo.path,{repo:repo.path,branch:repo.base,remote:repo.remote})}return[...seen.values()]};return c.json(listReleases(bp).map(r=>({...withNotes(bp,r),targets:targets(r),items:r.tickets.map(id2=>{let t=tickets.find(x=>x.id===id2);return{id:id2,title:titleOf(id2,r),gates:t?.gates??null,kind:t?.kind??"feature",...t?.outside?{outside:t.outside}:{},...t?.noNewTests?{noNewTests:!0}:{}}})})))}),app.put("/api/v1/releases/:v/notes",async c=>{let b=await body3(c);return act(c,()=>saveReleaseNotes(bp,c.req.param("v"),b.notes??""))}),app.post("/api/v1/releases/:v/release",async c=>{let{goOn:goOn3}=await body3(c);return act(c,()=>releaseVersion(bp,c.req.param("v"),goOn3))}),app.post("/api/v1/releases/:v/push",c=>act(c,()=>pushReleaseAgain(bp,c.req.param("v")))),app.post("/api/v1/releases/:v/mark-sent",async c=>{let b=await c.req.json().catch(()=>({}));try{return c.json(await markReleaseSent(bp,c.req.param("v"),{...b.reason?{reason:b.reason}:{},...b.anyway===!0?{anyway:!0}:{}}))}catch(err2){return err2 instanceof SentNotSure?c.json({error:err2.message},409):act(c,()=>{throw err2})}}),app.get("/api/v1/releases/:v",c=>{let r=readRelease(bp,c.req.param("v"));return r?c.json(withNotes(bp,r)):c.json({error:"not found"},404)}),app.post("/api/v1/bugs",async c=>{let b=await body3(c);return act(c,()=>reportBug(bp,{what:b.what??"",how:b.how,expected:b.expected,severity:b.severity,foundIn:b.foundIn}))}),app.get("/api/v1/decisions",c=>{if(!boardExists(bp))return c.json({decisions:[],map:[],lessons:[]});let meta=readMeta(bp),broken=brokenDecisions(bp);return c.json({decisions:listDecisions(bp),map:meta.map,lessons:meta.lessons,...broken.length?{broken}:{}})}),app.post("/api/v1/architecture/read",c=>act(c,()=>readCodeAgain(bp))),app.post("/api/v1/architecture/impact",async c=>{let b=await body3(c);return act(c,()=>checkTicketsAgainst(bp,b.adr??""))}),app.post("/api/v1/architecture/decide",async c=>{let b=await body3(c),tech=b.tech??"";return b.do==="approve"?act(c,()=>approveTechDecision(bp,tech)):b.do==="discard"?act(c,()=>(discardTechDecision(bp,tech),{ok:!0})):act(c,()=>draftTechDecision(bp,tech))}),app.get("/api/v1/architecture",c=>boardExists(bp)?c.json(architecture(bp)):c.json({decisions:[],tickets:[],map:[],lessons:[],stack:[],patterns:[],impact:[]})),app.get("/api/v1/proof/:file",c=>{let name=path154.basename(c.req.param("file")),abs=path154.join(bp.proofDir,name);return fs162.existsSync(abs)?isApiProof(name)?new Response(fs162.readFileSync(abs),{headers:{"content-type":"application/json"}}):/-api-\d+-answer\.[a-z]+$/.test(name)?new Response(fs162.readFileSync(abs),{headers:{"content-type":"application/octet-stream","content-disposition":`attachment; filename="${name}"`}}):name.endsWith(".png")?new Response(fs162.readFileSync(abs),{headers:{"content-type":"image/png","cache-control":"private, max-age=3600"}}):c.text("not found",404):c.text("not found",404)}),app.put("/api/v1/settings",async c=>{let b=await body3(c);return act(c,()=>{let meta=readMeta(bp);if(b.allowedHosts!==void 0){let parsed=parseHosts(Array.isArray(b.allowedHosts)?b.allowedHosts:[]),problem2=hostsProblem(parsed);if(problem2)throw new Error(problem2);meta.allowedHosts=parsed.hosts}if(b.network!==void 0){let level=parseNetwork(b.network);if(!level)throw new Error(`"${b.network}" is not a network level. Use standard, strict or off.`);meta.network=level}if(b.guard!==void 0){let clean4=list6=>[...new Set((Array.isArray(list6)?list6:[]).map(p=>String(p).trim()).filter(Boolean).map(p=>sharedPath(p)))],read7=clean4(b.guard.allowRead),write2=clean4(b.guard.allowWrite);meta.guard=read7.length||write2.length?{...read7.length?{allowRead:read7}:{},...write2.length?{allowWrite:write2}:{}}:void 0}if(b.quality!==void 0){let q5=parseQuality(b.quality);if(!q5)throw new Error(`"${b.quality}" is not a Quality setting. Use best, balanced or thrifty.`);meta.quality=q5}if(b.commands&&(meta.commands=Object.fromEntries(Object.entries(b.commands).map(([k,v])=>[k,String(v??"").trim()]).filter(([,v])=>v))),b.git){let next={base:b.git.base?.trim()||meta.git?.base||"main",remote:b.git.remote?.trim()??meta.git?.remote??""};checkWhereCodeGoes(root,next.base,next.remote),meta.git=next}return typeof b.parallel=="number"&&b.parallel>=1&&b.parallel<=5&&(meta.parallel=Math.round(b.parallel)),b.delivery===null?delete meta.delivery:(b.delivery==="pr"||b.delivery==="push")&&(meta.delivery=b.delivery),b.atOnce===null?delete meta.atOnce:typeof b.atOnce=="number"&&b.atOnce>=1&&b.atOnce<=5&&(meta.atOnce=Math.round(b.atOnce)),(b.runner==="acp"||b.runner==="direct")&&(meta.runner=b.runner),typeof b.documentsInGit=="boolean"&&(meta.documentsInGit=b.documentsInGit),b.alwaysNewTests===!0?meta.alwaysNewTests=!0:b.alwaysNewTests===!1&&delete meta.alwaysNewTests,b.repos&&meta.repos&&(meta.repos=meta.repos.map(r=>{let u=b.repos.find(x=>x.path===r.path);if(!u)return r;let commands=Object.fromEntries(Object.entries(u.commands??r.commands).map(([k,v])=>[k,String(v??"").trim()]).filter(([,v])=>v)),next={base:u.base?.trim()||r.base,remote:u.remote?.trim()??r.remote};return checkWhereCodeGoes(path154.join(root,r.path),next.base,next.remote),{...r,...next,commands}})),writeMeta(bp,meta),meta})});let partChange=b=>({...typeof b.name=="string"?{name:b.name}:{},...typeof b.shared=="boolean"?{shared:b.shared}:{},...typeof b.repo=="string"?{repo:b.repo}:{},...typeof b.folder=="string"?{folder:b.folder}:{},...Array.isArray(b.needs)?{needs:b.needs.map(String)}:{},...typeof b.url=="string"?{url:b.url}:{},...typeof b.off=="boolean"?{off:b.off}:{}});app.get("/api/v1/parts",c=>{if(!boardExists(bp))return c.json({parts:[],repos:[],several:!1});let meta=readMeta(bp);return c.json({parts:partsOf2(meta),repos:repoPaths(meta),several:severalParts(meta)})}),app.post("/api/v1/parts",async c=>{let b=await body3(c);return act(c,()=>addPart(bp,partChange(b)))}),app.put("/api/v1/parts/:id",async c=>{let b=await body3(c);return act(c,()=>changePart(bp,c.req.param("id"),partChange(b)))}),app.post("/api/v1/parts/:id/remove",c=>act(c,()=>(removePart(bp,c.req.param("id")),{ok:!0}))),app.get("/api/v1/project-files",c=>boardExists(bp)?c.json(projectFilesView(bp)):c.json({local:[],locked:[],repos:[],docker:[]}));let fileRoute=(route,run7)=>app.post(`/api/v1/project-files/${route}`,async c=>{let b=await body3(c);return act(c,()=>run7(typeof b.repo=="string"?b.repo:void 0,typeof b.path=="string"?b.path:void 0))});fileRoute("local",(repo,file3)=>addLocalFile(bp,repo,file3)),fileRoute("locked",(repo,file3)=>addLockedFile(bp,repo,file3)),fileRoute("use-mine",(repo,file3)=>useMyFile(bp,repo,file3)),fileRoute("remove",(repo,file3)=>removeProjectFile(bp,repo,file3)),app.post("/api/v1/project-files/lock-docker",c=>act(c,()=>({locked:lockDockerFiles(bp)})));let noDatabase={database:null,template:null,stories:[],notes:[],missing:[],words:"squad-kit has not found a database in this project."};app.get("/api/v1/database",c=>boardExists(bp)?c.json(databaseView(bp)):c.json(noDatabase)),app.post("/api/v1/database/find",c=>act(c,()=>(learnDatabase(bp,{again:!0}),databaseView(bp)))),app.post("/api/v1/database/confirm",async c=>{let b=await body3(c),change=Object.fromEntries(["kind","name","repo","env","file","key","address"].filter(k=>typeof b[k]=="string").map(k=>[k,b[k]]));return act(c,()=>(confirmDatabase(bp,readMeta(bp).user.name,change),databaseView(bp)))}),app.post("/api/v1/database/template",c=>act(c,()=>(makeTemplateBeside(bp,readMeta(bp).user.name),databaseView(bp)))),app.put("/api/v1/database",async c=>{let b=await body3(c);return act(c,()=>(setDatabaseOptions(bp,{...typeof b.hidePersonal=="boolean"?{hidePersonal:b.hidePersonal}:{},...typeof b.sampleRows=="number"||b.sampleRows===null?{sampleRows:b.sampleRows}:{}}),databaseView(bp)))}),app.get("/api/v1/sandbox",c=>{let tools4=sandboxStatus(),note2=guardNote(process.platform,tools4),g=guardSupport(),allow=boardExists(bp)?readMeta(bp).guard:void 0,guard={on:g.on,why:g.why,...g.fix?{fix:g.fix}:{},allowRead:allow?.allowRead??[],allowWrite:allow?.allowWrite??[]},level=(boardExists(bp)?parseNetwork(readMeta(bp).network):null)??DEFAULT_NETWORK,network={level,commands:networkPolicy(level).commands,blocked:boardExists(bp)?blockedSites(bp).slice(0,20):[]};return c.json({tools:tools4,registries:PACKAGE_HOSTS,allowedHosts:boardExists(bp)?readMeta(bp).allowedHosts??[]:[],guard,network,...note2?{note:note2}:{}})}),app.get("/api/v1/agents",c=>{if(!boardExists(bp))return c.json([]);readMeta(bp).engine==="live"&&ensureAgents(bp);let tickets=listTickets(bp).filter(t=>t.live&&t.squad),mine2=personLevels(),quality=readMeta(bp).quality,STEP_NAME2={chat:"Talking it through",draft:"Filling in a new ticket"};return c.json(AGENTS.map(a=>{let agent=readAgent(bp,a.role),tool5=aiTool(agent.tool),floor=a.role==="reviewer"?{builder:builderLevel({quality},readAgent(bp,"builder").levels,mine2)}:{},levels=levelSteps(a.role).map(step2=>{let now3=resolveLevel({role:a.role,step:step2,quality,...floor},agent.levels,mine2),effort=tool5?effortOn(tool5,now3.effort):now3.effort;return{step:step2,name:STEPS[a.role].find(x=>x.id===step2)?.name??STEP_NAME2[step2]??step2,standard:standardLevel({role:a.role,step:step2,quality}),team:agent.levels[`${a.role}.${step2}`]??null,you:mine2[`${a.role}.${step2}`]??null,now:{...now3,effort,modelName:now3.model||tool5?.modelFor(now3.level)||""}}});return{...a,...agent,levels,teamRole:agent.levels[a.role]??null,youRole:mine2[a.role]??null,modelOf:Object.fromEntries(MODEL_LEVELS.map(l=>[l,tool5?.modelFor(l)??""])),supportsEffort:tool5?.supportsEffort??!1,efforts:tool5?toolEfforts(tool5):[],file:`.squad/board/agents/${a.role}.md`,stepList:STEPS[a.role],working:tickets.filter(t=>roleForPhase(t.squad?.phase)===a.role).map(t=>({id:t.id,title:t.title,status:t.status}))}}))});let addOne=(input7,from)=>{let meta=readMeta(bp),by2=meta.user.name;if(input7.kind!=="gate"){let added3=addAddition(bp,by2,input7,from);return{added:added3,...added3.kind==="rule"&&ruleNote(added3.text)?{note:ruleNote(added3.text)}:{}}}if(severalParts(meta)&&!input7.part)throw new Error("Pick which part it runs in.");let{added:added2,failsToday}=addTriedGate(bp,by2,input7,from);return{added:added2,...failsToday?{note:"It fails on today's code, so it stays on Try first until that is fixed."}:{}}};app.post("/api/v1/harness/additions",async c=>{let input7=await body3(c);return act(c,()=>addOne(input7))}),app.post("/api/v1/harness/additions/:id/state",async c=>{let{state}=await body3(c);return act(c,()=>setAdditionState(bp,readMeta(bp).user.name,c.req.param("id"),state))}),app.post("/api/v1/harness/additions/:id/remove",c=>act(c,()=>removeAddition(bp,readMeta(bp).user.name,c.req.param("id")))),app.post("/api/v1/harness/try",async c=>{let input7=await body3(c);return act(c,()=>tryGate(bp,readMeta(bp).user.name,input7))});let lookIn=folder=>{if(!folder?.trim())throw new Error("Choose a folder first.");return lookInFolder(folder,readHarness(bp).additions,reposOf(readMeta(bp))[0]?.commands??{})};app.post("/api/v1/harness/folder",async c=>{let{path:folder}=await body3(c);return act(c,()=>lookIn(folder))}),app.post("/api/v1/harness/folder/add",async c=>{let{path:folder,picks}=await body3(c);return act(c,()=>{let look=lookIn(folder),added2=[],left2=[];for(let pick2 of picks??[]){let f=look.finds.find(x=>x.key===pick2.key);if(!(!f||f.out||f.already))try{added2.push(addOne(additionOf(f,pick2),look.name).added.name)}catch(err2){left2.push({name:f.name,why:plainWords(err2,"Adding from a folder")})}}return{added:added2,left:left2}})}),app.get("/api/v1/harness",c=>c.json(boardExists(bp)?harnessView(readMeta(bp),bp):harnessView())),app.get("/api/v1/usage",c=>boardExists(bp)?c.json(boardUsage(bp)):c.json({total:{runs:0},project:{runs:0},roles:[],tickets:0})),app.put("/api/v1/agents/:role",async c=>{let b=await body3(c);return act(c,()=>writeAgent(bp,c.req.param("role"),{tool:b.tool,model:b.model?.trim(),instructions:b.instructions,steps:b.steps,...b.levels?{levels:b.levels}:{}}))}),app.put("/api/v1/me/levels",async c=>{let b=await body3(c);return act(c,()=>{let key=b.key??"",[role,step2]=key.split(".");if(!AGENTS.some(a=>a.role===role)||step2&&!levelSteps(role).includes(step2))throw new Error(`"${key}" is not a role or one of its steps.`);if(b.level&&!parseLevel(b.level))throw new Error(`"${b.level}" is not a level. Use strongest, balanced or fast.`);if(b.effort&&!parseEffort(b.effort))throw new Error(`"${b.effort}" is not an effort. Use xhigh (extra high), high, medium or low.`);return setPersonLevel(key,{level:parseLevel(b.level),effort:parseEffort(b.effort),model:b.model})})}),app.get("/api/v1/agents/:role/prompts",async c=>{let role=c.req.param("role");return AGENTS.some(a=>a.role===role)?c.json(await corePrompts(bp,role,c.req.query("ticket")||void 0)):c.json({error:"not found"},404)}),app.post("/api/v1/restart",c=>opts.control?(opts.control.restart(),c.json({ok:!0})):c.json({error:"This board cannot restart itself. Press Ctrl-C and run: squad console"},400)),app.post("/api/v1/scan",c=>act(c,()=>{let meta=readMeta(bp);if(meta.engine!=="live")throw new Error("Only a live board reads the project.");return meta.scan={status:"pending"},writeMeta(bp,meta),meta})),mountCostApi(app,bp),app.post("/api/v1/demo/reset",c=>{if(boardExists(bp)&&readMeta(bp).engine!=="demo")return c.json({error:"Only a demo board can be reset."},400);let speed=boardExists(bp)?readMeta(bp).speed:1,project=boardExists(bp)?readMeta(bp).project:void 0;return seedDemoBoard(bp,{speed,project}),c.json({ok:!0})})}import fs163 from"fs";import path155 from"path";function isMerged2(t,meta){return!!t.mergedAt||t.stage==="done"||meta.engine==="demo"&&t.stage==="release"}function branchOf2(t){return t.branch??`squad/${t.id.toLowerCase()}`}function repoDir2(bp,meta,t){return path155.join(bp.root,repoFor(meta,t).path)}function copyOf(dir,t){return t.worktree??path155.join(worktreeRoot(dir),t.id.toLowerCase())}function inside4(dir,file3){let rel=path155.relative(dir,file3);return!!rel&&!rel.startsWith("..")&&!path155.isAbsolute(rel)}function hasBranch(dir,branch){return!!git(dir,["rev-parse","--verify","-q",`refs/heads/${branch}`],{allowFail:!0})}function and4(items3){return items3.length<2?items3[0]??"":`${items3.slice(0,-1).join(", ")} and ${items3[items3.length-1]}`}function decisionsOf(bp,t){let others=listTickets(bp).filter(x=>x.id!==t.id),going=[],kept2=[];for(let d of listDecisions(bp).filter(x=>x.from===t.id)){let by2=others.filter(x=>x.follows?.includes(d.id)||x.replanFor===d.id).map(x=>x.id);by2.length?kept2.push({d,by:by2}):going.push(d)}return{going,kept:kept2}}function pushedTo(bp,dir,t){if(!isRepo(dir))return;let target=repoTarget(bp,t);if(!target)return;let branch=branchOf2(t);if(!git(dir,["rev-parse","--verify","-q",`refs/remotes/${target.remote}/${branch}`],{allowFail:!0})&&!t.handedOver&&!t.pr)return;let why=t.handedOver?"squad-kit pushed it when the ticket was handed over.":t.pr?`squad-kit pushed it for pull request ${t.pr.id}.`:`${target.remote} has it, pushed from this computer or a teammate's.`;return{remote:target.remote,branch,why}}function refusal(bp,meta,t){let base2=repoFor(meta,t).base;if(!isMine(t,meta)&&!t.ownerDeleted)return{why:`${t.id} is ${ownerOf(t)}'s. Only ${ownerOf(t)} can delete it.`};if(isMerged2(t,meta))return{why:`${t.id} is merged already: its code is in ${base2}, so it cannot be deleted. Archive it to hide it from the board. Its history stays.`,archive:!0};let going=releaseGoingOut(bp),draft=listReleases(bp).find(r=>r.status==="draft"&&r.tickets.includes(t.id));if(draft&&going===draft.version)return{why:`${draft.version} is going out right now with ${t.id}. Wait for it to finish.`};if(draft)return{why:`${t.id} is in ${draft.version}, the release being made, so it cannot be deleted. It goes out with that release; after that you can archive it.`};let dir=repoDir2(bp,meta,t);if(isRepo(dir)&&currentBranch(dir)===branchOf2(t))return{why:`Your project folder is on ${branchOf2(t)}. Switch it back to ${base2} first (git switch ${base2}), then delete ${t.id}.`}}function deletePlan(bp,id2,opts={}){let meta=readMeta(bp),t=requireTicket(bp,id2),dir=repoDir2(bp,meta,t),base2=repoFor(meta,t).base,no=refusal(bp,meta,t);if(!no&&!isMine(t,meta)&&t.ownerDeleted)return removePlan(bp,meta,t,dir,base2);let pushed=no?void 0:pushedTo(bp,dir,t),remote=!!(pushed&&(opts.remote??t.deleting?.remote)),removes=["The ticket, its spec, notes and questions"],stays=[`Your ${base2} code and every other ticket`],warnings=[],inGit=isRepo(dir),copy2=inGit&&fs163.existsSync(copyOf(dir,t)),branch=inGit&&hasBranch(dir,branchOf2(t));(copy2||branch)&&removes.push(t.copies?.length?"Its copies of the code":copy2?"Its copy of the code":"Its saved work on this computer"),fs163.existsSync(ticketCacheDir(bp,t.id))&&removes.push("The packages its Builder and Tester downloaded for it");let chats=!!(Object.values(t.sessions??{}).some(Boolean)||t.chat),shots=!!t.proof?.length;(chats||shots)&&removes.push(chats&&shots?"Its agent conversations and screenshots":chats?"Its agent conversations":"Its screenshots"),t.trackerQueue?.length&&removes.push(`${t.trackerQueue.length===1?"An update":`${t.trackerQueue.length} updates`} for the tracker that ${t.trackerQueue.length===1?"was":"were"} not sent yet`);let{going,kept:kept2}=decisionsOf(bp,t),all=listDecisions(bp);for(let d of going){removes.push(`Its decision ${d.id}, "${d.title}". It never reached ${base2}.`);let old=all.find(x=>x.replacedBy===d.id);old&&stays.push(`Decision ${old.id}, "${old.title}". It is a rule again, as it was before ${d.id} replaced it.`)}for(let r of listReleases(bp).filter(x=>x.bugs?.includes(t.id)))removes.push(`Its place among the bugs found in ${r.version}`);pushed&&remote&&removes.push(`Its saved work in your team's shared copy of the code${t.pr?`, and pull request ${t.pr.id} is closed`:""}`);let tracker2=trackerModule(t.source.type);tracker2&&stays.push(`The ${t.kind==="bug"?"bug":"story"}${t.source.ref?` ${t.source.ref}`:""} in ${tracker2.label}. Only the link to it is removed.`),stays.push("The cost it used, still counted in Usage");for(let k of kept2)stays.push(`Its decision ${k.d.id}, "${k.d.title}", because ${and4(k.by)} ${k.by.length===1?"follows":"follow"} it`);pushed&&!remote&&(stays.push(`Its saved work in your team's shared copy of the code, with a note that you deleted ${t.id}, so teammates' boards show it as deleted.`),t.pr&&stays.push(`Pull request ${t.pr.id}, still open. Close it on the code host if you no longer need it.`)),(t.live||t.squad||t.questions?.some(q5=>q5.thinking)||t.chat?.thinking)&&!t.deleting&&warnings.push("The squad is working on it. Delete stops it first and waits for it."),t.chat?.open&&warnings.push("A chat is open on it. Delete ends the chat."),t.takenOver&&warnings.push("You took it over. Close the session you opened in its copy first.");for(let k of kept2)warnings.push(`${and4(k.by)} ${k.by.length===1?"builds":"build"} on its decision ${k.d.id}.`);return{id:t.id,title:t.title,summary:[t.title,t.status,isMerged2(t,meta)?t.stage==="done"?`released${t.release?` in ${t.release}`:""}`:`merged into ${base2}`:"never merged"].filter(Boolean).join(" \xB7 "),...no?{refused:no.why,...no.archive?{archive:!0}:{}}:{},removes,stays,warnings,...pushed?{pushed}:{},...t.deleting?.error?{unfinished:t.deleting.error}:{},remote,base:base2,command:`squad delete ${t.id}${remote?" --remote":""}`}}function removePlan(bp,meta,t,dir,base2){let by2=t.ownerDeleted.by,local=localCopy(dir,t,base2,copyOf(dir,t)),removes=[`${t.id} from your board: its files, notes and questions on this computer`];(local.copy||local.branch)&&removes.push(local.copy?"Its copy of the code on this computer":"Its saved work on this computer");let chats=!!(Object.values(t.sessions??{}).some(Boolean)||t.chat),shots=!!t.proof?.length;(chats||shots)&&removes.push(chats&&shots?"Its agent conversations and screenshots on this computer":chats?"Its agent conversations on this computer":"Its screenshots on this computer");let{going}=decisionsOf(bp,t);for(let d of going)removes.push(`Its decision ${d.id}, "${d.title}", on this computer`);let stays=[`Everything on the remote: squad-kit pushes and deletes nothing there. ${by2}'s delete is theirs.`,`Your ${base2} code and every other ticket`,...local.kept,...t.usage?.length?["The cost it used on this computer, still counted in Usage"]:[]],warnings=t.chat?.open?["A chat is open on it. Removing it ends the chat."]:[];return{id:t.id,title:t.title,summary:[t.title,`Deleted by ${by2}`].join(" \xB7 "),removes,stays,warnings,remote:!1,local:{by:by2},base:base2,command:`squad delete ${t.id}`}}var sleep8=ms2=>new Promise(r=>setTimeout(r,ms2));function runsHere(bp,id2){return(agentRuns().find(r=>r.root===bp.root)?.keys??[]).filter(k=>k===id2||k.endsWith(`:${id2}`))}async function waitForStop(bp,id2,mark,ms2){let end=Date.now()+ms2;for(;;){let t=readTicket(bp,id2);if(!t)return;t.deleting||updateTicket(bp,id2,x=>x.deleting={...mark});let here=runsHere(bp,id2),there=squadRunsElsewhere(bp)&&!t.deleting?.stopped;if(!here.length&&!there)return;if(Date.now()>end)throw new Error("The squad did not stop in time");await sleep8(200)}}async function removeRemote(bp,t){let target=repoTarget(bp,t);if(!target)return[];let branch=branchOf2(t),host3=repoHostFor(bp,target.url),notes=[];if(t.pr&&host3)try{await host3.close(t.pr.id),notes.push(`Closed pull request ${t.pr.id}`)}catch(err2){throw new Error(`Could not close pull request ${t.pr.id}: ${plainWords(err2,`Closing pull request ${t.pr.id}`,"something technical stopped it. Try again.")}`)}try{await netGitLater(target.dir,["push",target.remote,"--delete",branch],{env:host3?.gitEnv()??{}}),notes.push(`Deleted ${branch} on ${target.remote}`)}catch(err2){if(!/remote ref does not exist/i.test(err2.message))throw new Error(`Could not delete its work on ${target.remote}: ${plainWords(err2,`Deleting ${branch} on ${target.remote}`,"something technical stopped it. Try again.")}`);git(target.dir,["update-ref","-d",`refs/remotes/${target.remote}/${branch}`],{allowFail:!0})}return notes}async function removeCopy(bp,meta,t,opts={}){let projectCopies2=worktreeRoot(bp.root),one2=async(dir2,base2,copy2)=>{if(!isRepo(dir2))return[];let root=worktreeRoot(dir2),keep=opts.safe?localCopy(dir2,t,base2,copy2):null;(!keep||keep.copy)&&(await removeWorktree(dir2,copy2),(inside4(root,copy2)||inside4(projectCopies2,copy2))&&await fs163.promises.rm(copy2,{recursive:!0,force:!0}));let before=path155.join(root,`${t.id.toLowerCase()}-before`);return fs163.existsSync(before)&&await dropSnapshot(dir2,before),await gitLater(dir2,["worktree","prune"],{allowFail:!0}),(!keep||keep.branch)&&hasBranch(dir2,branchOf2(t))&&await gitLater(dir2,["branch","-D",branchOf2(t)]),keep?.kept??[]},dir=repoDir2(bp,meta,t),kept2=await one2(dir,repoFor(meta,t).base,copyOf(dir,t));for(let c of t.copies??[])kept2.push(...await one2(path155.join(bp.root,c.repo),reposOf(meta).find(r=>r.path===c.repo)?.base??repoFor(meta,t).base,c.dir));return t.storyDir&&inside4(projectCopies2,t.storyDir)&&fs163.existsSync(t.storyDir)&&!copyDirsIn(t.storyDir).length&&await fs163.promises.rm(t.storyDir,{recursive:!0,force:!0}),kept2}async function leaveNote(bp,t,by2,at3){let target=repoTarget(bp,t),repo=docsRepo(bp);if(!target||!repo||repo!==target.dir)return[];let branch=branchOf2(t),env2=repoHostFor(bp,target.url)?.gitEnv()??{},ref2=`refs/remotes/${target.remote}/${branch}`;try{await netGitLater(target.dir,["fetch","--quiet",target.remote,`+refs/heads/${branch}:${ref2}`],{env:env2});let tip=git(target.dir,["rev-parse","--verify","-q",ref2],{allowFail:!0});if(!tip)return[];let note2=deleteNoteCommit(target.dir,tip,squadInRepo(bp,repo),t,by2,at3);return note2?(await netGitLater(target.dir,["push",target.remote,`${note2}:refs/heads/${branch}`],{env:env2}),git(target.dir,["update-ref",ref2,note2],{allowFail:!0}),[`Left a note on ${branch} on ${target.remote}, so teammates' boards show ${t.id} as deleted`]):[]}catch(err2){return[`Teammates still see ${t.id}: squad-kit could not leave a note on ${branch} on ${target.remote} (${err2.message})`]}}function unlinkDecisions(bp,t){let{going}=decisionsOf(bp,t),ids=new Set(going.map(d=>d.id));for(let d of listDecisions(bp)){if(ids.has(d.id))continue;let tickets=d.tickets.filter(x=>x!==t.id),buildsOn=d.buildsOn?.filter(x=>!ids.has(x)),back=!!(d.replacedBy&&ids.has(d.replacedBy));if(tickets.length===d.tickets.length&&buildsOn?.length===d.buildsOn?.length&&!back)continue;let{replacedBy,buildsOn:_was,...rest}=d;writeDecision(bp,{...rest,tickets,...buildsOn?.length?{buildsOn}:{},...replacedBy&&!back?{replacedBy}:{}})}if(ids.size){for(let x of listTickets(bp).filter(y=>y.id!==t.id&&y.outdated?.some(o=>ids.has(o.adr))))updateTicket(bp,x.id,y=>{y.outdated=y.outdated?.filter(o=>!ids.has(o.adr)),y.outdated?.length||delete y.outdated});let meta=readMeta(bp);meta.impactChecks?.some(c=>ids.has(c.adr))&&writeMeta(bp,{...meta,impactChecks:meta.impactChecks.filter(c=>!ids.has(c.adr))})}for(let r of listReleases(bp).filter(x=>x.bugs?.includes(t.id)))writeRelease(bp,{...r,bugs:r.bugs.filter(x=>x!==t.id)});for(let d of going)fs163.rmSync(path155.join(bp.decisionsDir,`${d.id}.md`),{force:!0})}function removeChatsAndShots(bp,t){let sessions=[...Object.values(t.sessions??{}),t.chat?.side].filter(s=>typeof s=="string"&&/^[\w-]{8,64}$/.test(s));for(let s of sessions)fs163.rmSync(path155.join(bp.chatsDir,`${s}.jsonl`),{force:!0});if(!fs163.existsSync(bp.proofDir))return;let shown2=new Set(listTickets(bp).filter(x=>x.id!==t.id).flatMap(x=>(x.proof??[]).flatMap(p=>[p.before,p.after,...(p.more??[]).map(m=>m.after)]))),own2=new RegExp(`^${t.id.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}-(before|after)-`),named=(t.proof??[]).flatMap(p=>[p.before,p.after,...(p.more??[]).map(m=>m.after)]).filter(Boolean).map(f=>path155.basename(f));for(let f of new Set([...named,...fs163.readdirSync(bp.proofDir).filter(f2=>own2.test(f2))]))shown2.has(f)||fs163.rmSync(path155.join(bp.proofDir,f),{force:!0})}function keepRecord(bp,t,mark,fromBoard=!1){let meta=readMeta(bp),rest=(meta.deleted??[]).filter(d=>!(d.id===t.id&&d.at===mark.at));writeMeta(bp,{...meta,deleted:[...rest,{id:t.id,title:t.title,at:mark.at,by:mark.by,...t.usage?.length?{usage:t.usage}:{},...fromBoard?{fromBoard}:{}}]})}function deleteTicket(bp,id2,opts={}){let key=`${bp.squadDir}\0${id2.toUpperCase()}`,now3=deletesNow.get(key);if(now3)return now3;let run7=deleteOnce(bp,id2,opts).finally(()=>deletesNow.delete(key));return deletesNow.set(key,run7),run7}var deletesNow=new Map;async function deleteOnce(bp,id2,opts){refuseWhileSaving(bp,id2);let first2=readTicket(bp,id2);if(!first2){if(fs163.existsSync(ticketStateFile(bp,id2)))return removeTicket(bp,id2),{id:id2.toUpperCase(),title:"",notes:[]};throw new BoardActionError(`No ticket ${id2.toUpperCase()} on this board.`)}let meta=readMeta(bp),no=refusal(bp,meta,first2);if(no)throw new BoardActionError(no.why);let local=!isMine(first2,meta),pushed=local?void 0:pushedTo(bp,repoDir2(bp,meta,first2),first2),remote=!!(opts.remote??first2.deleting?.remote)&&!!pushed,mark={at:first2.deleting?.at??now(),by:meta.user.name,...remote?{remote}:{},...first2.deleting?.remoteDone?{remoteDone:!0}:{}};updateTicket(bp,first2.id,x=>{x.deleting||addActivity(x,"you",local?"Pressed Remove from my board":"Pressed Delete"),x.deleting={...mark},x.status=local?"Removing\u2026":"Deleting\u2026",x.waiting=null,x.live=!1,x.squad=null});try{await waitForStop(bp,first2.id,mark,opts.waitMs??6e4);let t=requireTicket(bp,first2.id),notes=[];return remote&&!t.deleting?.remoteDone?(notes.push(...await removeRemote(bp,t)),updateTicket(bp,t.id,x=>x.deleting={...mark,...x.deleting,remoteDone:!0})):pushed&&!remote&&!t.deleting?.remoteDone&&(notes.push(...await leaveNote(bp,t,mark.by,mark.at)),updateTicket(bp,t.id,x=>x.deleting={...mark,...x.deleting,remoteDone:!0})),await stopTry(bp,t.id),notes.push(...(await removeCopy(bp,meta,t,{safe:local})).map(k=>`Kept ${k.charAt(0).toLowerCase()}${k.slice(1)}`)),await dropStoryDb(bp,t.id).catch(()=>!1),dropTicketCache(bp,t.id),unlinkDecisions(bp,t),removeChatsAndShots(bp,t),keepRecord(bp,t,mark,local),removeTicket(bp,t.id),{id:t.id,title:t.title,notes}}catch(err2){let why=err2.message.replace(/\.\s*$/,"");try{updateTicket(bp,first2.id,x=>{x.deleting={...mark,...x.deleting,error:why},x.status="Delete did not finish",addActivity(x,"squad",`Delete did not finish: ${why}`)})}catch{}throw new BoardActionError(`${first2.id} is not fully deleted yet: ${why}. Delete it again to finish; it goes on where it stopped.`)}}function archiveTicket(bp,id2){let meta=readMeta(bp),t=requireTicket(bp,id2);if(t.archived)return t;if(!isMerged2(t,meta))throw new BoardActionError(`${t.id} is not merged yet, so it stays on the board. Delete it instead if you no longer want it.`);return updateTicket(bp,t.id,x=>{x.archived={by:meta.user.name,at:now()},addActivity(x,"you","Archived it: off the board, its history kept")})}function unarchiveTicket(bp,id2){let t=requireTicket(bp,id2);if(!t.archived)throw new BoardActionError(`${t.id} is not archived.`);return updateTicket(bp,t.id,x=>{delete x.archived,addActivity(x,"you","Brought it back to the board")})}async function body4(c){try{return await c.req.json()}catch{return{}}}function mountDeleteApi(app,opts){let bp=boardPaths(opts.paths.squadDir);app.get("/api/v1/tickets/:id/delete",c=>act(c,()=>deletePlan(bp,c.req.param("id"),{remote:c.req.query("remote")==="1"?!0:c.req.query("remote")==="0"?!1:void 0}))),app.post("/api/v1/tickets/:id/delete",async c=>{let b=await body4(c);return act(c,()=>deleteTicket(bp,c.req.param("id"),{remote:!!b.remote}))}),app.post("/api/v1/tickets/:id/archive",async c=>{let b=await body4(c);return act(c,()=>b.undo?unarchiveTicket(bp,c.req.param("id")):archiveTicket(bp,c.req.param("id")))})}async function body5(c){try{return await c.req.json()}catch{return{}}}function how(b){return{...b.phone===!0?{phone:!0}:{},...b.web===!0?{web:!0}:{},...typeof b.language=="string"?{language:b.language}:{}}}function mountAppsApi(app,opts){let bp=boardPaths(opts.paths.squadDir),runner4=()=>appRunner(bp),name=c=>decodeURIComponent(c.req.param("name")??""),withNews=s=>({...s,...appNews(readMeta(bp),listTickets(bp),s,runner4().notNowOf(s.repo))});app.get("/api/v1/apps",async c=>{if(!boardExists(bp))return c.json({apps:[]});let meta=readMeta(bp),apps2=(await runner4().status()).map(withNews),reading=meta.scan?.status==="pending"||meta.scan?.status==="running";return c.json({apps:apps2,live:meta.engine==="live",reading,...meta.scan?.status==="failed"&&meta.scan.error?{readError:meta.scan.error}:{}})}),app.post("/api/v1/apps/:name/start",async c=>{let b=await body5(c);return act(c,async()=>withNews(await runner4().startAs(name(c),{nextPort:!!b.nextPort,stopOther:!!b.stopOther,...how(b)})))}),app.post("/api/v1/apps/:name/stop",c=>act(c,async()=>withNews(await runner4().stop(name(c))))),app.post("/api/v1/apps/:name/restart",async c=>{let b=await body5(c);return act(c,async()=>withNews(await runner4().restart(name(c),{nextPort:!!b.nextPort,stopOther:!!b.stopOther,...how(b)})))}),app.get("/api/v1/apps/:name/output",c=>act(c,async()=>{let s=await runner4().statusOf(name(c));return{name:s.name,start:s.start??null,url:s.url??null,address:s.address??null,dir:s.dir,commit:s.commit??null,lines:runner4().lines(name(c))}})),app.put("/api/v1/apps/:name/commands",async c=>{let b=await body5(c);return act(c,async()=>{let meta=readMeta(bp),a=findApp(meta,name(c));return writeMeta(bp,withAppCommands(meta,a.repo,{start:b.start,url:b.url})),withNews(await runner4().statusOf(a.name))})}),app.post("/api/v1/apps/:name/not-now",async c=>{let b=await body5(c);return act(c,()=>{if(!b.ticket)throw new Error("Say which ticket.");return runner4().notNow(name(c),b.ticket),{ok:!0}})})}async function body6(c){try{return await c.req.json()}catch{return{}}}function mountDependsApi(app,opts){let bp=boardPaths(opts.paths.squadDir);app.post("/api/v1/tickets/:id/depends",async c=>{let b=await body6(c);return act(c,()=>setDependsOn(bp,c.req.param("id"),Array.isArray(b.on)?b.on.map(String):[]))}),app.post("/api/v1/tickets/:id/build",async c=>{let b=await body6(c);return act(c,()=>chooseBuild(bp,c.req.param("id"),b.way))}),app.post("/api/v1/tickets/:id/bring-in",c=>act(c,()=>bringInLatest(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/check-merge",c=>act(c,()=>checkMerge(bp,c.req.param("id")))),app.post("/api/v1/releases/:v/take-out",async c=>{let b=await body6(c);return act(c,()=>takeOutOfRelease(bp,c.req.param("v"),b.ticket??""))}),app.post("/api/v1/releases/:v/put-back",async c=>{let b=await body6(c);return act(c,()=>putBackInRelease(bp,c.req.param("v"),b.ticket??""))}),app.get("/api/v1/releases/:v/out",c=>act(c,()=>outItems(bp,nextDraft(bp,c.req.param("v"))))),app.get("/api/v1/branches",c=>act(c,()=>readMeta(bp).engine==="live"?oldBranches(bp):{here:[],remote:[],kept:[]})),app.post("/api/v1/branches/clean",async c=>{let b=await body6(c);return act(c,()=>{if(readMeta(bp).engine!=="live")throw new Error("The sample board has no git branches to delete.");return cleanBranches(bp,{remote:!!b.remote})})})}async function body7(c){try{return await c.req.json()}catch{return{}}}function mountOverviewApi(app,opts){let bp=boardPaths(opts.paths.squadDir);app.get("/api/v1/setup",c=>c.json({setup:boardExists(bp)?setupView(bp):null})),app.post("/api/v1/setup/stop",c=>act(c,()=>stopSetup(bp))),app.post("/api/v1/setup/again",async c=>{let b=await body7(c);return act(c,()=>setupAgain(bp,b.step??"read"))}),app.post("/api/v1/setup/quality",async c=>{let b=await body7(c);return act(c,()=>({setup:setupQuality(bp,b.quality??"")}))}),app.get("/api/v1/overview",c=>boardExists(bp)?c.json(overviewView(bp)):c.json({error:"not found"},404)),app.post("/api/v1/overview/approve",c=>act(c,()=>approveOverview(bp))),app.post("/api/v1/overview/ideas/:n",c=>act(c,()=>ticketFromIdea(bp,Number(c.req.param("n"))))),app.post("/api/v1/overview/idea",async c=>{let b=await body7(c);return act(c,()=>tellIdea(bp,b.text??""))}),app.post("/api/v1/overview/read-again",c=>act(c,()=>setupAgain(bp,"read"))),app.get("/api/v1/overview/talk",c=>c.json({talk:boardExists(bp)?readTalk(bp):null})),app.post("/api/v1/overview/talk",async c=>{let b=await body7(c);return act(c,()=>({talk:readTalk(bp)?.open&&b.text?.trim()?sendTalk(bp,b.text):startTalk(bp,b.text)}))}),app.post("/api/v1/overview/talk/stop",c=>act(c,()=>({talk:stopTalk(bp)}))),app.post("/api/v1/overview/talk/close",c=>act(c,()=>({talk:closeTalk(bp)}))),app.post("/api/v1/overview/talk/read-again",c=>act(c,()=>readAgainWithTalk(bp)))}import fs164 from"fs";import path156 from"path";async function body8(c){try{return await c.req.json()}catch{return{}}}function mountBuildSendApi(app,bp){let view=id2=>viewBreakdown(bp,id2);app.get("/api/v1/requirements/waiting",c=>act(c,()=>({breakdowns:breakdownWaits(bp)}))),app.post("/api/v1/requirements/:id/build",async c=>{let b=await body8(c);return act(c,()=>{if(typeof b.on!="boolean")throw new BoardActionError('Say { "on": true } to build continuously, or { "on": false } to pause.');return view(setBuild(bp,c.req.param("id"),b.on).id)})}),app.get("/api/v1/requirements/:id/send",c=>act(c,()=>sendPreview(bp,c.req.param("id")))),app.post("/api/v1/requirements/:id/send",c=>act(c,()=>view(requestSend(bp,c.req.param("id")).id)))}async function body9(c){try{return await c.req.json()}catch{return{}}}var str3=v=>typeof v=="string"?v:void 0;function patchOf(b){return{...str3(b.title)!==void 0?{title:b.title}:{},...str3(b.goal)!==void 0?{goal:b.goal}:{},...str3(b.summary)!==void 0?{summary:b.summary}:{},...str3(b.story)!==void 0?{story:b.story}:{},...Array.isArray(b.criteria)?{criteria:b.criteria}:{},...str3(b.size)!==void 0?{size:b.size}:{},...Array.isArray(b.needs)?{needs:b.needs.map(String)}:{}}}function mountRequirementsApi(app,opts){let bp=boardPaths(opts.paths.squadDir),view=id2=>viewBreakdown(bp,id2);mountBuildSendApi(app,bp),app.get("/api/v1/requirements",c=>act(c,()=>{let broken=brokenBreakdowns(bp);return{breakdowns:breakdownRows(bp),...broken.length?{broken}:{}}})),app.post("/api/v1/requirements",async c=>{let b=await body9(c),files=Array.isArray(b.files)?b.files:[],file3=b.file??(str3(b.text)?void 0:files[0]),extra=[...Array.isArray(b.with)?b.with:[],...b.file||str3(b.text)?files:files.slice(1)];return act(c,async()=>view(addBreakdown(bp,{...file3?{file:file3}:{},...str3(b.text)?{text:b.text}:{},...str3(b.name)?{name:b.name}:{},with:extra}).id))}),app.get("/api/v1/requirements/:id",async c=>{let problem2=breakdownProblem(bp,c.req.param("id"));return problem2?c.json({error:problem2},409):readBreakdown(bp,c.req.param("id"))?act(c,()=>view(c.req.param("id"))):c.json({error:"not found"},404)}),app.get("/api/v1/requirements/:id/file",c=>{let b=readBreakdown(bp,c.req.param("id")),asked=c.req.query("path")??"";if(!b)return c.json({error:"not found"},404);let dir=breakdownDir(bp,b.id),file3=path156.resolve(dir,asked),rel=path156.relative(dir,file3);if(!asked||rel.startsWith("..")||path156.isAbsolute(rel)||!/^(source|text|media)[\\/]/.test(rel)||!fs164.existsSync(file3)||!fs164.statSync(file3).isFile())return c.json({error:"not found"},404);let type=mimeOf(file3),scripts=type==="image/svg+xml"||type==="text/html";return new Response(fs164.readFileSync(file3),{headers:{"content-type":type,"cache-control":"no-store","x-content-type-options":"nosniff",...scripts?{"content-disposition":dispositionOf("attachment",file3)}:{}}})}),app.patch("/api/v1/requirements/:id",async c=>{let b=await body9(c);return act(c,()=>view(renameBreakdown(bp,c.req.param("id"),b.title??"").id))}),app.delete("/api/v1/requirements/:id",c=>act(c,()=>deleteBreakdown(bp,c.req.param("id")))),app.post("/api/v1/requirements/:id/stop",c=>act(c,()=>view(stopBreakdown(bp,c.req.param("id")).id))),app.post("/api/v1/requirements/:id/retry",c=>act(c,()=>view(retryBreakdown(bp,c.req.param("id")).id))),app.post("/api/v1/requirements/:id/more",c=>act(c,()=>view(moreBreakdown(bp,c.req.param("id")).id))),app.post("/api/v1/requirements/:id/answers",async c=>{let b=await body9(c);return act(c,()=>view(answerBreakdown(bp,c.req.param("id"),b.answers??{}).id))}),app.post("/api/v1/requirements/:id/skip",c=>act(c,()=>view(skipBreakdownQuestions(bp,c.req.param("id")).id))),app.post("/api/v1/requirements/:id/questions/:q/ask",async c=>{let b=await body9(c);return act(c,()=>view(askAboutQuestion(bp,c.req.param("id"),c.req.param("q"),b.text??"").id))}),app.post("/api/v1/requirements/:id/items",async c=>{let b=await body9(c);return act(c,async()=>{let r=addItem(bp,c.req.param("id"),{...patchOf(b),title:b.title??"",...str3(b.under)?{under:b.under}:{},...b.level==="epic"||b.level==="feature"||b.level==="story"?{level:b.level}:{},...str3(b.before)?{before:b.before}:{}});return{key:r.key,breakdown:await view(r.breakdown.id)}})}),app.patch("/api/v1/requirements/:id/items/:key",async c=>{let b=await body9(c);return act(c,()=>view(editItem(bp,c.req.param("id"),c.req.param("key"),patchOf(b)).id))}),app.post("/api/v1/requirements/:id/items/:key/move",async c=>{let b=await body9(c);return act(c,()=>view(moveItem(bp,c.req.param("id"),c.req.param("key"),{...str3(b.under)?{under:b.under}:{},...str3(b.before)?{before:b.before}:{}}).id))}),app.delete("/api/v1/requirements/:id/items/:key",c=>act(c,()=>view(removeItem(bp,c.req.param("id"),c.req.param("key")).id))),app.post("/api/v1/requirements/:id/items/:key/redo",async c=>{let b=await body9(c);return act(c,()=>view(redoItem(bp,c.req.param("id"),c.req.param("key"),b.ask??"").id))}),app.post("/api/v1/requirements/:id/approve",c=>act(c,()=>view(approveBreakdown(bp,c.req.param("id")).id))),app.post("/api/v1/requirements/:id/items/:key/start",c=>act(c,async()=>({ticket:startStory(bp,c.req.param("id"),c.req.param("key")),breakdown:await view(c.req.param("id"))}))),app.post("/api/v1/requirements/:id/decided/:d",async c=>{let b=await body9(c);return act(c,()=>view(changeDecided2(bp,c.req.param("id"),c.req.param("d"),b.text??"",{whole:b.whole===!0}).id))})}function mountTryApi(app,opts){let bp=boardPaths(opts.paths.squadDir),runner4=()=>tryRunner(bp);app.get("/api/v1/tickets/:id/try",c=>act(c,()=>runner4().status(c.req.param("id")))),app.post("/api/v1/tickets/:id/try",async c=>{let b=await c.req.json().catch(()=>({}))??{},how2={...b.phone===!0?{phone:!0}:{},...b.web===!0?{web:!0}:{},...typeof b.language=="string"?{language:b.language}:{}};return act(c,()=>runner4().startAs(c.req.param("id"),how2))}),app.post("/api/v1/tickets/:id/try/stop",c=>act(c,()=>runner4().stop(c.req.param("id"))))}function mountToolsApi(app,opts){let bp=boardPaths(opts.paths.squadDir);app.get("/api/v1/tools",c=>act(c,()=>projectTools(bp))),app.post("/api/v1/tools/check",c=>act(c,async()=>(await checkTools(),projectTools(bp)))),app.post("/api/v1/tools/:tool/get",c=>act(c,()=>(getToolNow(c.req.param("tool"),opts.source?{source:opts.source}:{}),projectTools(bp)))),app.post("/api/v1/tools/:tool/update",c=>act(c,async()=>{let b=await c.req.json().catch(()=>({}));return await updateToolNow(bp,c.req.param("tool"),{ok:b?.ok===!0}),projectTools(bp)}))}async function phoneView(){let phone=shotPhone(),home2=squadHome(),base2={times:stepTimes(home2),idleMinutes:Math.round(IDLE_MS2/6e4)},notHere=phone.notHere();if(notHere)return{notHere,devices:[],...base2};let saved=savedDevices(home2),states=saved.length?new Map((await phone.devices().catch(()=>[])).map(d=>[d.id,d.state])):new Map,used=lastUsed(home2),busy=phoneInUse();return{devices:saved.map(d=>{let state=states.get(d.id)??"gone",last=used[d.id],off=state==="booted"&&last&&!busy?new Date(Date.parse(last)+IDLE_MS2).toISOString():void 0;return{size:d.size,name:d.name,model:d.model,runtime:d.runtime,id:d.id,state,inUse:busy&&state==="booted",keptUp:keptUp(d.id),...last?{lastUsed:last}:{},...off?{offAt:off}:{}}}),...base2}}async function stopPhoneNow(){let phone=shotPhone(),word2=phone.words.phone;if(phone.notHere())return{...await phoneView(),message:`${phone.notHere()} There is nothing to stop here.`};let r=await stopPhones(phone);r.busy||closeDrivers();let message=r.busy?`squad-kit's ${word2} is taking pictures for a story now. It shuts down by itself when it has been unused for ${Math.round(IDLE_MS2/6e4)} minutes.`:r.left.length?`Something else keeps squad-kit's ${word2} open, like a Simulator window, so squad-kit left it. Close that window to shut it down.`:r.stopped.length?`squad-kit shut down its ${[...new Set(r.stopped.map(id2=>phone.words[savedDevices().find(d=>d.id===id2)?.size??"phone"]))].join(" and ")}.`:"squad-kit's phones are not running.";return{...await phoneView(),message}}function mountPhoneApi(app){app.get("/api/v1/phone",c=>act(c,()=>phoneView())),app.post("/api/v1/phone/stop",c=>act(c,()=>stopPhoneNow()))}function mountApi(app,opts){mountMetaApi(app,opts),mountDashboardApi(app,opts),mountProjectsApi(app,opts),mountStoriesApi(app,opts),mountPlansApi(app,opts),mountConfigApi(app,opts),mountRunsApi(app,opts),mountSecretsApi(app,opts),mountPlannerAuthApi(app,opts),mountTrackerApi(app,opts),mountDoctorApi(app,opts),mountCopyPlanPromptApi(app,opts),mountBoardApi(app,opts),mountDeleteApi(app,opts),mountAppsApi(app,opts),mountDependsApi(app,opts),mountOverviewApi(app,opts),mountRequirementsApi(app,opts),mountTryApi(app,opts),mountToolsApi(app,opts),mountPhoneApi(app),app.onError(onUnexpected)}function setupSummary(bp){let v=setupView(bp);return v?{line:setupLine(v),ready:v.ready,approved:v.approved,...v.brd?{brd:v.brd}:{}}:{}}var OpenProjects=class{constructor(opts){this.opts=opts}opts;open=new Map;closed=new Map;first=null;get(id2){return this.open.get(id2)}all(){return[...this.open.values()]}openSaved(){for(let entry of readProjects())if(!(this.open.has(entry.id)||[...this.open.values()].some(p=>p.root===entry.root))){if(!fs165.existsSync(path157.join(entry.root,SQUAD_DIR,"config.yaml"))){this.closed.set(entry.id,{...entry,why:fs165.existsSync(entry.root)?"Its squad-kit files are gone from the folder.":"The folder is not there any more."});continue}try{this.prepareAndOpen(entry.root)}catch(err2){this.closed.set(entry.id,{...entry,why:err2.message})}}}add(folder,choice){let asked=path157.resolve(folder);if(!fs165.existsSync(asked)||!fs165.statSync(asked).isDirectory())throw new Error(`There is no folder at ${asked}.`);let ask2=addAsk(asked);if(ask2&&(!choice||choice==="here"&&ask2.kind==="inside-repo"))throw new Error(ask2.words);let root=ask2?choice==="above"?ask2.root:asked:findSquadRoot(asked)??asked,known=this.all().find(p=>p.root===root);return known?{project:known,did:[],added:!1}:{...this.prepareAndOpen(root),added:!0}}openSample(){let root=demoFolder(),paths=buildPaths(root);fs165.existsSync(paths.configFile)||(fs165.mkdirSync(paths.squadDir,{recursive:!0}),saveConfig(paths.configFile,{...DEFAULT_CONFIG,project:{...DEFAULT_CONFIG.project,name:"Case portal"}}));let bp=boardPaths(paths.squadDir);return boardExists(bp)||seedDemoBoard(bp),this.add(root).project}create(input7){let made=makeNewProject(input7);try{return{project:this.openReady(made.paths),did:made.did,opens:made.opens}}catch(err2){throw this.open.has(made.bp.projectId)&&this.remove(made.bp.projectId),dropNewProject(made.bp.root,made.bp),err2}}prepareAndOpen(root){if(runningConsole(boardPaths(path157.join(root,SQUAD_DIR))))throw new Error("Its board is open in another squad-kit. Close that one first, then add it here.");let ready=prepareProject(root);return{project:this.openReady(ready.paths),did:ready.did}}openReady(paths){let bp=boardPaths(paths.squadDir),id2=bp.projectId,same5=this.open.get(id2);if(same5&&same5.root!==bp.root){if(fs165.existsSync(path157.join(same5.root,SQUAD_DIR)))throw new Error(`The same project is open already from ${same5.root}. squad-kit can run one copy of a project at a time.`);same5.close(),this.open.delete(id2)}if(this.open.get(id2))return this.open.get(id2);let api=new Hono;api.use("/api/*",authMiddleware(this.opts.token)),mountApi(api,{paths,control:this.opts.control});let stopTimer=startDemoSquad(bp),stopLive=new LiveSquad(bp).start(),releaseLock=claimSquad(bp);writeConsoleInfo(bp,this.opts.info());let project={id:id2,root:bp.root,paths,bp,api,close:()=>{stopTimer(),stopLive(),stopApps(bp),stopTries(bp),releaseLock(),writeConsoleInfo(bp,{...this.opts.info(),pid:null})}};this.open.set(id2,project);for(let c of[...this.closed.values()])(c.id===id2||c.root===bp.root)&&this.closed.delete(c.id);return this.opts.remember&&rememberProject(id2,bp.root),project}remove(id2){let p=this.open.get(id2);p?.close(),this.open.delete(id2);let was=this.closed.delete(id2)||!!p;return this.first===id2&&(this.first=null),this.opts.remember&&forgetProject(id2),was}closeAll(){for(let p of this.open.values())p.close();closeDrivers(),stopPhones(shotPhone()).catch(()=>{})}summaries(){let rows2=[];for(let p of this.open.values()){let base2={id:p.id,name:projectName(p.root),root:p.root,url:`/p/${encodeURIComponent(p.id)}/board`,tickets:0,waiting:0,working:0};try{if(!boardExists(p.bp)){let why=settingsProblem(p.bp);rows2.push({...base2,open:!0,...why?{why}:{}});continue}let meta=readMeta(p.bp),open3=listTickets(p.bp).filter(t=>t.stage!=="done");rows2.push({...base2,name:meta.project||base2.name,open:!0,engine:meta.engine,...meta.scan&&(meta.engine==="live"||meta.setup)?{setup:{status:meta.scan.status,now:meta.scan.now,error:meta.scan.error,...setupSummary(p.bp)}}:{},tickets:open3.length,waiting:inbox(p.bp).length,working:open3.filter(t=>t.live&&t.squad&&!t.waiting).length})}catch(err2){rows2.push({...base2,open:!0,why:err2.message})}}for(let c of this.closed.values())rows2.push({id:c.id,name:projectName(c.root),root:c.root,url:`/p/${encodeURIComponent(c.id)}/board`,open:!1,why:c.why,tickets:0,waiting:0,working:0});return rows2}waiting(){let items3=[];for(let p of this.open.values())try{if(!boardExists(p.bp))continue;let project={id:p.id,name:readMeta(p.bp).project||projectName(p.root)};for(let t of inbox(p.bp))items3.push({project,ticket:t,at:t.activity[0]?.at??t.createdAt})}catch{}return items3.sort((a,b)=>b.at.localeCompare(a.at)).map(({project,ticket})=>({project,ticket}))}};async function startConsoleServer(opts){let bindPort=opts.requestedPort===0?await reserveEphemeralPort():await findFreePortFromBase(opts.requestedPort),app=new Hono2,startedAt=new Date().toISOString(),restart=!1,stopForRestart=()=>{};app.use("*",ownAddressOnly(bindPort)),app.get("/healthz",c=>c.json({ok:!0,version:readInstalledPackage().version,port:bindPort,startedAt},200,{"cache-control":"no-store"}));let MOCKUP_PATH=/^(\/p\/[^/]+)?\/mockup\/[^/]+$/,MOCKUP_CSP="sandbox allow-scripts; default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data:; font-src data:; frame-ancestors 'self'",strict=secureHeaders({contentSecurityPolicy:{defaultSrc:["'self'"],styleSrc:["'self'","'unsafe-inline'"],imgSrc:["'self'","data:"],connectSrc:["'self'"],scriptSrc:["'self'"],frameAncestors:["'none'"]},referrerPolicy:"no-referrer"}),FILE_PATH=/\/tickets\/[^/]+\/file$/;app.use("*",async(c,next)=>{if(!MOCKUP_PATH.test(c.req.path)){await strict(c,next);let csp=c.res.headers.get("content-security-policy");FILE_PATH.test(c.req.path)&&c.res.headers.get("content-type")==="application/pdf"&&csp&&c.res.headers.set("content-security-policy",csp.replace("frame-ancestors 'none'","frame-ancestors 'self'"));return}await next(),c.res.headers.set("content-security-policy",MOCKUP_CSP),c.res.headers.set("x-content-type-options","nosniff"),c.res.headers.set("referrer-policy","no-referrer")}),app.use("*",cors({origin:"http://127.0.0.1:"+bindPort}));let control={startedAt,build:opts.build??0,restart:()=>{restart=!0,setTimeout(()=>stopForRestart(),200)}},actualPort=bindPort,info2=()=>({pid:process.pid,port:actualPort,token:opts.token,startedAt,version:readInstalledPackage().version,build:opts.build??0}),projects=new OpenProjects({token:opts.token,control,info:info2,remember:!!opts.app});app.use("/api/*",authMiddleware(opts.token)),mountAppApi(app,{projects});let updates=new UpdateService({busy:()=>runsNow(projects),carrying:()=>ticketsInProgress(projects),restart:control.restart,...opts.update});mountUpdateApi(app,updates),mountSupportApi(app,{projects}),app.onError(onUnexpected),app.all("/p/:project/*",async(c,next)=>{let url=new URL(c.req.url),prefix=url.pathname.split("/").slice(0,3).join("/");if(url.pathname=url.pathname.slice(prefix.length),!url.pathname.startsWith("/api/")&&!url.pathname.startsWith("/mockup/"))return next();let p=projects.get(c.req.param("project"));return p?p.api.fetch(new Request(url,c.req.raw),c.env):c.json({error:"This project is not open in the app. Add it from Home."},404)});let first2=()=>projects.first?projects.get(projects.first):void 0;app.all("/api/*",c=>{let p=first2();return p?p.api.fetch(c.req.raw,c.env):c.json({error:"Name the project: /p/<project id>/api/v1/\u2026"},404)}),app.get("/mockup/:id",async c=>{for(let p of[first2(),...projects.all()]){if(!p)continue;let res=await p.api.fetch(c.req.raw,c.env);if(res.status!==404)return res}return c.text("not found",404)}),app.all("/mcp",c=>answerWindow(c.req.raw)),mountStaticAssets(app,{paths:opts.paths});let nodeServer=serve({fetch:app.fetch,hostname:"127.0.0.1",port:bindPort});nodeServer.keepAliveTimeout=KEEP_IDLE_MS,actualPort=(()=>{let addr=nodeServer.address();return addr&&typeof addr=="object"?addr.port:bindPort})(),windowAt(`http://127.0.0.1:${actualPort}`),opts.paths&&(projects.first=projects.openReady(opts.paths).id),opts.app&&projects.openSaved(),log.info(`squad-kit ${readInstalledPackage().version} started on port ${actualPort} with ${projects.all().length} project${projects.all().length===1?"":"s"} open (Node ${process.version})`);let stopSquad=()=>projects.closeAll(),resolveDone=()=>{},done2=new Promise(resolve2=>{resolveDone=resolve2}),shutdown=onClose=>{log.info("squad-kit stopped"),process.off("SIGINT",stop),process.off("SIGTERM",stop),process.off("SIGHUP",onHangup),stopSquad(),windowAt(null),closeServer(nodeServer,onClose)},stop=()=>{shutdown(err2=>{err2&&warning(`server close: ${err2.message}`),resolveDone()})};stopForRestart=stop,process.once("SIGINT",stop),process.once("SIGTERM",stop);let onHangup=opts.keepOnHangup?()=>log.info("The terminal closed; squad-kit keeps running, as it was started to outlive it"):stop;return opts.keepOnHangup?process.on("SIGHUP",onHangup):process.once("SIGHUP",onHangup),{port:actualPort,done:done2,restartRequested:()=>restart,projects,updates,close:()=>new Promise((resolve2,reject)=>{shutdown(err2=>{if(err2){reject(err2);return}resolve2(),resolveDone()})})}}function startedToOutlive(fds=[1,2],info2={isatty,fstat:fs166.fstatSync}){return fds.every(fd=>{try{if(info2.isatty(fd))return!1;let st=info2.fstat(fd);return!st.isFIFO()&&!st.isSocket()}catch{return!1}})}function reserveEphemeralPort(){return new Promise((resolve2,reject)=>{let s=createServer();s.once("error",reject),s.listen(0,"127.0.0.1",()=>{let addr=s.address();s.close(closeErr=>{if(closeErr){reject(closeErr);return}addr&&typeof addr=="object"?resolve2(addr.port):reject(new Error("Could not determine ephemeral port"))})})})}async function findFreePortFromBase(start,attempts=20){for(let i=0;i<attempts;i+=1){let candidate=start+i;if(await isPortFree(candidate))return candidate}throw new Error(`Could not find a free port in [${start}, ${start+attempts-1}]. Pass \`--port <n>\` to pick another base.`)}function isPortFree(port){return new Promise(resolve2=>{let probe=createServer();probe.once("error",()=>resolve2(!1)),probe.once("listening",()=>probe.close(()=>resolve2(!0))),probe.listen(port,"127.0.0.1")})}function outputGone(err2){let e=err2;return e?.code==="EPIPE"&&e.syscall==="write"||e?.code==="ERR_STREAM_DESTROYED"||e?.code==="ERR_STREAM_WRITE_AFTER_END"}function fatal(err2){let e=err2;return e?.syscall==="listen"&&(e.code==="EADDRINUSE"||e.code==="EACCES")}function installCrashNet(proc=process,say=text3=>log.error(text3)){let saying=!1,onError=(err2,kind)=>{if(!outputGone(err2)){if(!saying){saying=!0;try{say(`squad-kit kept running after ${kind}: ${err2 instanceof Error?err2.stack??err2.message:String(err2)}`)}catch{}finally{saying=!1}}fatal(err2)&&proc.exit(1)}},quiet=()=>{};proc.stdout?.on("error",quiet),proc.stderr?.on("error",quiet);let uncaught=err2=>onError(err2,"an error nobody caught"),rejected=err2=>onError(err2,"a promise that failed with nobody waiting");return proc.on("uncaughtException",uncaught),proc.on("unhandledRejection",rejected),()=>{proc.off("uncaughtException",uncaught),proc.off("unhandledRejection",rejected),proc.stdout?.off("error",quiet),proc.stderr?.off("error",quiet)}}import kleur9 from"kleur";import fs167 from"fs";import path158 from"path";import kleur8 from"kleur";import{confirm as confirm10,input as input5,select as select8,Separator}from"@inquirer/prompts";import kleur6 from"kleur";var out=(s="")=>process.stdout.write(`${s}
2023
- `),cmd=s=>kleur6.green(s),amber=s=>kleur6.yellow(s);function board2(){let{bp}=openBoard(buildPaths(requireSquadRoot()).squadDir);if(!boardExists(bp))throw new Error("This project has no board yet. Run `squad demo` to try squad-kit v1 with a sample board.");return bp}function barText(now3){return(now3.bar??[]).map(b=>b==="done"?kleur6.green("\u25B0"):b==="now"?kleur6.green("\u25B0"):kleur6.gray("\u25B1")).join("")}function nowLines(t,now3){let lines3=[],pad2=" ";now3.state==="working"?(lines3.push(` ${kleur6.bold("Now")} ${now3.line}`),lines3.push(`${pad2}${kleur6.gray(now3.time??"")}${now3.bar?.length?` ${barText(now3)}`:""}`),lines3.push(`${pad2}${now3.sentence}`),now3.next&&lines3.push(`${pad2}${kleur6.gray("Next:")} ${now3.next}`)):(lines3.push(` ${kleur6.bold("Now")} ${now3.state==="you"?amber(now3.sentence):now3.sentence}`),now3.next&&now3.state!=="you"&&lines3.push(`${pad2}${now3.next}`));let s=now3.slow;if(s){lines3.push(""),lines3.push(` ${amber(s.title)} ${kleur6.gray(s.where)}`),lines3.push(`${pad2}${s.text}`);for(let f of s.facts)lines3.push(`${pad2}${kleur6.gray(`${f.label}:`)} ${f.value}`);s.limit&&lines3.push(`${pad2}${kleur6.gray(s.limit)}`);let ways=s.kind==="stuck"&&s.canEnd?[`squad unstick ${t.id}`]:[`squad wait ${t.id} --more 10m`];lines3.push(`${pad2}${[...ways,`squad stop ${t.id} --again`,`squad stop ${t.id}`].map(cmd).join(kleur6.gray(" or "))}`)}return lines3}async function runTicketStatus(ticketId){let bp=board2(),t=requireTicket(bp,ticketId);out(),out(` ${kleur6.bold(t.id)} ${kleur6.bold(t.title)}`);for(let l of nowLines(t,liveNow(t,readUsual(bp))))out(l);out()}function offerCommand(t,o,question){let q5=shellQuote(question);switch(o.do){case"propose":return`squad propose ${t.id} ${q5}`;case"note":return o.to?`squad message ${t.id} --to ${o.to} ${q5}`:`squad note ${t.id} ${q5}`;case"chat":return`squad chat ${t.id} --with ${o.to}`;case"stop":return`squad stop ${t.id}`}}async function runAsk(ticketId,words2){let question=words2.join(" ").trim();if(!question)throw new Error('Ask a question, for example: squad ask SK-12 "is there a problem?"');let bp=board2(),t=requireTicket(bp,ticketId),usual=readUsual(bp),a=answerAsk(t,liveNow(t,usual),usual,question);out(),out(` ${kleur6.bold("squad-kit")}${a.instant?kleur6.gray(a.lang==="ar"?" \u0625\u062C\u0627\u0628\u0629 \u0641\u0648\u0631\u064A\u0629":" answered at once"):""}`);for(let l of a.lines)out(` ${l}`);if(a.offers.length){out();for(let o of a.offers)out(` ${o.label}${"note"in o?kleur6.gray(` ${o.note}`):""}
2022
+ `).map(b=>b.replace(`${remote}/`,"")).filter(b=>b&&b!=="HEAD"&&b!==remote);if(branches.length&&!branches.includes(base2))throw new Error(`There is no branch called "${base2}" here. It has: ${[...new Set(branches)].slice(0,8).join(", ")}. Nothing was saved.`)}async function body3(c){try{return await c.req.json()}catch{return{}}}function mountBoardApi(app,opts){let bp=boardPaths(opts.paths.squadDir),tried=t=>(lookAgain(bp),t),nowOf=(t,usual)=>liveNow(t,usual,Date.now(),isSaving(bp,t.id)),connection=null;async function trackerStatus(){let tracker2=trackerFor(bp);if(!tracker2)return{tracker:null};let key=`${tracker2.name}/${tracker2.identity}`;if(!connection||connection.key!==key||Date.now()-connection.at>6e4)try{connection={at:Date.now(),key,user:await tracker2.whoAmI()}}catch(err2){connection={at:Date.now(),key,error:plainWords(err2,"Checking the tracker sign-in")}}return{tracker:connection.error?null:tracker2,user:connection.user,error:connection.error}}app.get("/api/v1/tracker",async c=>{let config2=fs162.existsSync(opts.paths.configFile)?loadConfig(opts.paths.configFile):null,root2=path154.dirname(opts.paths.squadDir),type=config2?.tracker.type??"none",mod=trackerModule(type),status=mod?await trackerStatus():{user:void 0,error:void 0},dirs=[root2,...findNestedRepos(root2).map(r=>path154.join(root2,r.path))],guessed=guessTracker(dirs.map(d=>git(d,["config","--get","remote.origin.url"],{allowFail:!0}))),saved=mod?mod.read(root2):{values:{},hasSecret:!1},fieldsOf=mod??guessed?.module??TRACKERS[0],values=Object.fromEntries(fieldsOf.fields.filter(f=>!f.secret).map(f=>[f.key,saved.values[f.key]||guessed?.values[f.key]||""]));return c.json({type,module:fieldsOf.name,label:fieldsOf.label,fields:fieldsOf.fields,...values,hasToken:saved.hasSecret,user:status.user,error:status.error})}),app.put("/api/v1/tracker",async c=>{let b=await body3(c);return act(c,async()=>{let config2=loadConfig(opts.paths.configFile);(trackerModule(b.module??config2.tracker.type)??TRACKERS[0]).save(path154.dirname(opts.paths.squadDir),b),connection=null;let status=await trackerStatus();if(status.error)throw new Error(status.error);return{ok:!0,user:status.user}})});let root=path154.dirname(opts.paths.squadDir);app.get("/api/v1/speech",c=>c.json({hasKey:speechModule().hasKey(root),label:speechModule().label,video:videoNotesWork()})),app.put("/api/v1/speech",async c=>{let b=await body3(c);return act(c,async()=>{let speech=speechModule();if(b.remove)return speech.removeKey(root),{ok:!0,hasKey:speech.hasKey(root)};let key=(b.key??b.munsit)?.trim();if(!key)throw new Error(`Paste your ${speech.label} key first.`);let said=await speech.checkKey(key);if(said==="wrong")throw new BoardActionError(`${speech.label} did not accept this key, so it was not saved. Copy it again from your ${speech.label} account.`);return speech.saveKey(root,key),{ok:!0,hasKey:!0,...said==="no-answer"?{unchecked:!0}:{}}})}),app.get("/api/v1/backlog",async c=>{let status=await trackerStatus(),tracker2=status.tracker;if(!tracker2)return c.json({connected:!1,error:status.error??null,sprints:[],items:[]});try{let sprints=tracker2.abilities.sprints?await tracker2.sprints():[],asked=c.req.query("sprint"),sprint=asked==="all"?null:sprints.find(s=>s.path===asked)??sprints.find(s=>s.current)??sprints[0]??null,items3=await tracker2.list(sprint?{sprint:sprint.path}:{}),onBoard=new Map(listTickets(bp).filter(t=>t.source.type===tracker2.name).map(t=>[t.source.id??tracker2.parseRef(t.source.ref??""),t]));return c.json({connected:!0,user:status.user,sprints,sprint:sprint?.path??"all",items:items3.map(w=>{let t=onBoard.get(w.id);return{...w,ticket:t?{id:t.id,stage:t.stage,status:t.status}:null}})})}catch(err2){return c.json({connected:!0,error:plainWords(err2,"Reading the backlog"),sprints:[],items:[]})}}),app.post("/api/v1/backlog/:id/start",async c=>act(c,async()=>{let status=await trackerStatus();if(!status.tracker)throw new Error(status.error??"No tracker is connected. Add it in Settings.");return startFromTracker(bp,status.tracker,c.req.param("id"))})),app.get("/api/v1/board",c=>{if(!boardExists(bp)){let settings=settingsProblem(bp);return c.json({ready:!1,...settings?{settings}:{}})}let meta=readMeta(bp),all=listTickets(bp),broken=brokenTickets(bp,all),records=[...brokenDecisions(bp),...brokenReleases(bp),...brokenBreakdowns(bp)],recent=listReleases(bp).filter(r=>r.status==="released"&&r.date&&Date.now()-Date.parse(r.date)<30*DAY3),ctl=opts.control,usual=readUsual(bp);return c.json({ready:!0,meta,console:ctl?{startedAt:ctl.startedAt,outdated:ctl.build>0&&buildStamp()>ctl.build}:null,tickets:all.filter(t=>t.stage!=="done"&&!t.archived).map(t=>({...t,now:nowOf(t,usual),...toolWaitOf(bp,t)})),archived:all.filter(t=>t.stage!=="done"&&t.archived),...broken.length?{broken}:{},...records.length?{brokenRecords:records}:{},stats:{released30:recent.reduce((n,r)=>n+r.tickets.length,0),bugs30:recent.reduce((n,r)=>n+(r.bugs?.length??0),0),open:all.filter(t=>t.stage!=="done"&&!t.mergedAt&&!t.archived).length}})}),app.get("/api/v1/tickets/:id",c=>{let problem2=ticketProblem(bp,c.req.param("id"));if(problem2)return c.json({error:problem2},409);let t=readTicket(bp,c.req.param("id"));return t?c.json({...withProblem(t),now:nowOf(t,readUsual(bp)),...toolWaitOf(bp,t,!0)}):c.json({error:"not found"},404)}),app.get("/api/v1/tickets/:id/status",c=>{let t=readTicket(bp,c.req.param("id"));return t?c.json(nowOf(t,readUsual(bp))):c.json({error:"not found"},404)}),app.post("/api/v1/tickets/:id/status/ask",async c=>{let b=await body3(c);return act(c,()=>{let t=readTicket(bp,c.req.param("id"));if(!t)throw new Error(`No ticket ${c.req.param("id").toUpperCase()} on this board.`);let text3=(b.text??"").trim();if(!text3)throw new Error("Write your question first.");let usual=readUsual(bp);return answerAsk(t,nowOf(t,usual),usual,text3)})}),app.get("/api/v1/tickets/:id/conversations",c=>{let t=readTicket(bp,c.req.param("id"));return t?c.json(ticketConversations(t,void 0,bp.chatsDir)):c.json({error:"not found"},404)}),app.get("/api/v1/tickets/:id/conversations/:key",c=>{let t=readTicket(bp,c.req.param("id")),key=c.req.param("key"),id2=key==="chat"?t?.chat?.side:t?.sessions?.[key];if(!id2)return c.json({error:"not found"},404);let conv=readConversation(id2,Math.min(1e3,Number(c.req.query("limit"))||400),void 0,bp.chatsDir),role=key==="chat"?t?.chat?.with??"analyst":SESSION_ROLE[key]??"builder",tool5=aiTool(readAgent(bp,role).tool),open3=tool5?.resumeCommand?{command:tool5.resumeCommand(id2,conv?.cwd),tool:tool5.label}:{};return conv?c.json({...conv,protocol:"acp/v1",...open3}):c.json({sessionId:id2,updates:[],total:0,...open3})}),app.get("/api/v1/tickets/:id/file",c=>{let t=readTicket(bp,c.req.param("id")),asked=c.req.query("path")??"",file3=t&&asked?allowedFile(asked.replace(/^file:\/\//,""),ticketFolders(t,bp)):null;if(!file3)return c.text("not found",404);let size2=fs162.statSync(file3).size;if(size2>100*1024*1024)return c.text("too large",413);let type=mimeOf(file3),inline2=/^(image|video|audio)\/|^application\/pdf$|^text\//.test(type)&&type!=="image/svg+xml"&&type!=="text/html";return new Response(fs162.readFileSync(file3),{headers:{"content-type":type,"content-length":String(size2),"content-disposition":dispositionOf(inline2?"inline":"attachment",file3),"x-content-type-options":"nosniff","cache-control":"private, max-age=300"}})}),app.get("/api/v1/tickets/:id/mockup-link",c=>{let id2=c.req.param("id"),file3=mockupFile(bp,id2,c.req.query("path")??"");return file3?c.json({url:`/mockup/${encodeURIComponent(id2)}?path=${encodeURIComponent(file3)}&k=${mockupKey(id2,file3)}`}):c.json({error:"not found"},404)}),app.get("/mockup/:id",c=>{let id2=c.req.param("id"),file3=mockupFile(bp,id2,c.req.query("path")??""),k=c.req.query("k")??"";return!file3||!timingSafeEqualHex(k,mockupKey(id2,file3))?c.text("not found",404):new Response(fs162.readFileSync(file3),{headers:{"content-type":"text/html; charset=utf-8","cache-control":"private, no-store"}})}),app.post("/api/v1/tickets",async c=>{let b=await body3(c);return act(c,()=>createTicket(bp,{title:b.title??"",ask:b.ask,priority:b.priority,kind:b.kind==="bug"?"bug":"feature",severity:b.severity==="stops-work"||b.severity==="annoying"||b.severity==="small"?b.severity:void 0,track:b.track==="express"?"express":"full",criteria:b.criteria,rules:b.rules,edgeCases:b.edgeCases,files:b.files,talkFirst:!!b.talkFirst,...b.noNewTests===!0?{noNewTests:!0}:{},...typeof b.key=="string"&&b.key?{key:b.key.slice(0,80)}:{}}))}),app.post("/api/v1/tickets/draft",async c=>{let b=await body3(c);return act(c,()=>startDraft(bp,{text:b.text??"",files:b.files}))}),app.get("/api/v1/tickets/draft/current",c=>c.json({draft:currentDraft(bp)})),app.get("/api/v1/tickets/draft/:id",c=>{try{return c.json(draftJob(bp,c.req.param("id")))}catch(err2){return c.json({error:plainWords(err2,"Reading a draft")},404)}}),app.get("/api/v1/tickets/draft/:id/files",c=>act(c,()=>draftFiles(bp,c.req.param("id")))),app.post("/api/v1/tickets/draft/:id/cancel",c=>act(c,()=>cancelDraft(bp,c.req.param("id")))),app.post("/api/v1/tickets/draft/:id/dismiss",c=>act(c,()=>dismissDraft(bp,c.req.param("id")))),app.post("/api/v1/pull",async c=>{let b=await body3(c);return act(c,()=>pullTicket(bp,b.ref??""))}),app.post("/api/v1/tickets/:id/answers",async c=>{let b=await body3(c);return act(c,()=>answerQuestions(bp,c.req.param("id"),b.answers??{}))}),app.post("/api/v1/tickets/:id/decision-update",async c=>{let b=await body3(c),id2=c.req.param("id");return act(c,()=>b.do==="keep"?keepPlan(bp,id2,b.adr??"",b.reason??""):updateForDecision(bp,id2,b.adr??""))}),app.post("/api/v1/tickets/:id/local-setup",c=>act(c,()=>setUpLocalCopy(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/skip",c=>act(c,()=>skipQuestions(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/ask",async c=>{let b=await body3(c);return act(c,()=>askAbout(bp,c.req.param("id"),b.questionId??"",b.text??""))}),app.post("/api/v1/tickets/:id/approve",c=>act(c,async()=>tried(await approveBeside(bp,c.req.param("id"))))),app.post("/api/v1/tickets/:id/stop",c=>act(c,()=>stopTicket(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/start",c=>act(c,()=>startTicket(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/unstick",c=>act(c,()=>endStuckCommand(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/free-port",c=>act(c,()=>freePortAndRetry(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/keep-waiting",async c=>{let b=await body3(c);return act(c,()=>keepWaiting(bp,c.req.param("id"),b.minutes??10))}),app.post("/api/v1/tickets/:id/restart",c=>act(c,()=>restartStep(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/change",async c=>{let b=await body3(c);return act(c,()=>tried(requestChange(bp,c.req.param("id"),b.text??"")))}),app.get("/api/v1/tickets/:id/changes",c=>act(c,()=>ticketChanges(bp,c.req.param("id")))),app.get("/api/v1/editors",c=>c.json({editors:installedEditors()})),app.post("/api/v1/tickets/:id/open-editor",async c=>{let b=await body3(c);return act(c,()=>openInEditor(bp,c.req.param("id"),b.editor))}),app.post("/api/v1/tickets/:id/reply",async c=>{let b=await body3(c);return act(c,()=>answerAgent(bp,c.req.param("id"),b.text??""))}),app.post("/api/v1/tickets/:id/notes",async c=>{let b=await body3(c);return act(c,()=>addNote(bp,c.req.param("id"),{text:b.text??"",kind:b.kind,to:b.to}))}),app.post("/api/v1/tickets/:id/plan/edit",async c=>{let b=await body3(c);return act(c,()=>editPlan(bp,c.req.param("id"),b))}),app.post("/api/v1/tickets/:id/plan/send-back",async c=>{let b=await body3(c);return act(c,()=>sendBack(bp,c.req.param("id"),b.comments??[]))}),app.post("/api/v1/tickets/:id/plan/start-over",async c=>{let b=await body3(c);return act(c,()=>startOver(bp,c.req.param("id"),b.direction??""))}),app.post("/api/v1/tickets/:id/chat/start",async c=>{let b=await body3(c);return act(c,()=>b.ask?askQuestion(bp,c.req.param("id"),b.with):startChat(bp,c.req.param("id"),b.with??"analyst"))}),app.post("/api/v1/tickets/:id/chat/send",async c=>{let b=await body3(c);return act(c,()=>sendChat(bp,c.req.param("id"),b.text??"",b.files??[]))}),app.post("/api/v1/tickets/:id/chat/write",c=>act(c,()=>chatWrite(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/chat/done",c=>act(c,()=>chatDone(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/spec-change",async c=>{let b=await body3(c);try{return c.json(proposeChange(bp,c.req.param("id"),b.text??"",b.from))}catch(err2){return err2 instanceof ReleasedError?c.json({error:err2.message,released:!0,choices:["ticket","bug"]},409):act(c,()=>{throw err2})}}),app.post("/api/v1/tickets/:id/spec-change/approve",c=>act(c,()=>approveChange(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/spec-change/decline",async c=>{let b=await body3(c);return act(c,()=>declineChange(bp,c.req.param("id"),b.reason))}),app.post("/api/v1/tickets/:id/spec-change/retry",c=>act(c,()=>retryChange(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/spec-change/withdraw",async c=>{let b=await body3(c);return act(c,()=>withdrawChange(bp,c.req.param("id"),b.reason))}),app.post("/api/v1/tickets/:id/spec-change/new-ticket",async c=>{let b=await body3(c);return act(c,()=>changeAfterRelease(bp,c.req.param("id"),b.text??"",b.as==="bug"?"bug":"ticket"))}),app.post("/api/v1/tickets/:id/decided/:did",async c=>{let b=await body3(c);return act(c,()=>changeDecided(bp,c.req.param("id"),c.req.param("did"),b.text??""))}),app.post("/api/v1/tickets/:id/chat/stop",c=>act(c,()=>chatStop(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/chat/retry",c=>act(c,()=>chatRetry(bp,c.req.param("id")))),app.get("/api/v1/conversations",c=>c.json(boardExists(bp)?chatSummaries(listTickets(bp),c.req.query("q")??""):[])),app.post("/api/v1/tickets/:id/take-over",c=>act(c,()=>takeOver(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/give-back",async c=>{let b=await body3(c);return act(c,()=>giveBack(bp,c.req.param("id"),b.note))}),app.get("/api/v1/outside-prompt",c=>act(c,()=>({prompt:outsidePrompt(bp)}))),app.get("/api/v1/tickets/:id/outside-prompt",c=>act(c,()=>({prompt:outsidePrompt(bp,requireTicket(bp,c.req.param("id")))}))),app.post("/api/v1/tickets/:id/outside",c=>act(c,async()=>{let t=await workOutside(bp,c.req.param("id"));return{...t,prompt:outsidePrompt(bp,t)}})),app.post("/api/v1/tickets/:id/bring-back",c=>act(c,()=>bringBack2(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/check-outside",c=>act(c,()=>checkOutside(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/hand-over",async c=>{let b=await body3(c);return act(c,()=>handOver(bp,c.req.param("id"),{to:b.to??"",note:b.note}))}),app.post("/api/v1/tickets/:id/take-back",async c=>{let b=await body3(c);return act(c,()=>takeBack(bp,c.req.param("id"),b.reason??""))}),app.post("/api/v1/tickets/:id/decide",async c=>{let b=await body3(c);return act(c,()=>decideStuckBeside(bp,c.req.param("id"),Number(b.choice),{note:b.note,reason:b.reason}))}),app.post("/api/v1/tickets/:id/test-command",async c=>{let b=await body3(c);return act(c,()=>{if(typeof b.use!="boolean")throw new BoardActionError('Say { "use": true } to use the new test command, or { "use": false } to keep the current one.');return decideTestCommand(bp,c.req.param("id"),b.use)})}),app.get("/api/v1/releases",c=>{let tickets=listTickets(bp),meta=readMeta(bp),titleOf=(id2,r)=>tickets.find(t=>t.id===id2)?.title??r.titles?.[id2]??id2,targets=r=>{if(meta.engine!=="live")return[];let seen=new Map;for(let t of tickets.filter(x=>r.tickets.includes(x.id)))for(let view of copyViews(meta,t)){let repo=repoFor(meta,view);seen.set(repo.path,{repo:repo.path,branch:repo.base,remote:repo.remote})}return[...seen.values()]};return c.json(listReleases(bp).map(r=>({...withNotes(bp,r),targets:targets(r),items:r.tickets.map(id2=>{let t=tickets.find(x=>x.id===id2);return{id:id2,title:titleOf(id2,r),gates:t?.gates??null,kind:t?.kind??"feature",...t?.outside?{outside:t.outside}:{},...t?.noNewTests?{noNewTests:!0}:{}}})})))}),app.put("/api/v1/releases/:v/notes",async c=>{let b=await body3(c);return act(c,()=>saveReleaseNotes(bp,c.req.param("v"),b.notes??""))}),app.post("/api/v1/releases/:v/release",async c=>{let{goOn:goOn3}=await body3(c);return act(c,()=>releaseVersion(bp,c.req.param("v"),goOn3))}),app.post("/api/v1/releases/:v/push",c=>act(c,()=>pushReleaseAgain(bp,c.req.param("v")))),app.post("/api/v1/releases/:v/mark-sent",async c=>{let b=await c.req.json().catch(()=>({}));try{return c.json(await markReleaseSent(bp,c.req.param("v"),{...b.reason?{reason:b.reason}:{},...b.anyway===!0?{anyway:!0}:{}}))}catch(err2){return err2 instanceof SentNotSure?c.json({error:err2.message},409):act(c,()=>{throw err2})}}),app.get("/api/v1/releases/:v",c=>{let r=readRelease(bp,c.req.param("v"));return r?c.json(withNotes(bp,r)):c.json({error:"not found"},404)}),app.post("/api/v1/bugs",async c=>{let b=await body3(c);return act(c,()=>reportBug(bp,{what:b.what??"",how:b.how,expected:b.expected,severity:b.severity,foundIn:b.foundIn}))}),app.get("/api/v1/decisions",c=>{if(!boardExists(bp))return c.json({decisions:[],map:[],lessons:[]});let meta=readMeta(bp),broken=brokenDecisions(bp);return c.json({decisions:listDecisions(bp),map:meta.map,lessons:meta.lessons,...broken.length?{broken}:{}})}),app.post("/api/v1/architecture/read",c=>act(c,()=>readCodeAgain(bp))),app.post("/api/v1/architecture/impact",async c=>{let b=await body3(c);return act(c,()=>checkTicketsAgainst(bp,b.adr??""))}),app.post("/api/v1/architecture/decide",async c=>{let b=await body3(c),tech=b.tech??"";return b.do==="approve"?act(c,()=>approveTechDecision(bp,tech)):b.do==="discard"?act(c,()=>(discardTechDecision(bp,tech),{ok:!0})):act(c,()=>draftTechDecision(bp,tech))}),app.get("/api/v1/architecture",c=>boardExists(bp)?c.json(architecture(bp)):c.json({decisions:[],tickets:[],map:[],lessons:[],stack:[],patterns:[],impact:[]})),app.get("/api/v1/proof/:file",c=>{let name=path154.basename(c.req.param("file")),abs=path154.join(bp.proofDir,name);return fs162.existsSync(abs)?isApiProof(name)?new Response(fs162.readFileSync(abs),{headers:{"content-type":"application/json"}}):/-api-\d+-answer\.[a-z]+$/.test(name)?new Response(fs162.readFileSync(abs),{headers:{"content-type":"application/octet-stream","content-disposition":`attachment; filename="${name}"`}}):name.endsWith(".png")?new Response(fs162.readFileSync(abs),{headers:{"content-type":"image/png","cache-control":"private, max-age=3600"}}):c.text("not found",404):c.text("not found",404)}),app.get("/api/v1/settings/start",c=>{let repo=c.req.query("repo")??".",folder=boardExists(bp)&&readMeta(bp).repos?.some(r=>r.path===repo)?path154.join(root,repo):root;return c.json({note:windowStartNote(folder,c.req.query("command"))})}),app.put("/api/v1/settings",async c=>{let b=await body3(c);return act(c,()=>{let meta=readMeta(bp);if(b.allowedHosts!==void 0){let parsed=parseHosts(Array.isArray(b.allowedHosts)?b.allowedHosts:[]),problem2=hostsProblem(parsed);if(problem2)throw new Error(problem2);meta.allowedHosts=parsed.hosts}if(b.network!==void 0){let level=parseNetwork(b.network);if(!level)throw new Error(`"${b.network}" is not a network level. Use standard, strict or off.`);meta.network=level}if(b.guard!==void 0){let clean4=list6=>[...new Set((Array.isArray(list6)?list6:[]).map(p=>String(p).trim()).filter(Boolean).map(p=>sharedPath(p)))],read7=clean4(b.guard.allowRead),write2=clean4(b.guard.allowWrite);meta.guard=read7.length||write2.length?{...read7.length?{allowRead:read7}:{},...write2.length?{allowWrite:write2}:{}}:void 0}if(b.quality!==void 0){let q5=parseQuality(b.quality);if(!q5)throw new Error(`"${b.quality}" is not a Quality setting. Use best, balanced or thrifty.`);meta.quality=q5}if(b.commands&&(meta.commands=Object.fromEntries(Object.entries(b.commands).map(([k,v])=>[k,String(v??"").trim()]).filter(([,v])=>v))),b.git){let next={base:b.git.base?.trim()||meta.git?.base||"main",remote:b.git.remote?.trim()??meta.git?.remote??""};checkWhereCodeGoes(root,next.base,next.remote),meta.git=next}return typeof b.parallel=="number"&&b.parallel>=1&&b.parallel<=5&&(meta.parallel=Math.round(b.parallel)),b.delivery===null?delete meta.delivery:(b.delivery==="pr"||b.delivery==="push")&&(meta.delivery=b.delivery),b.atOnce===null?delete meta.atOnce:typeof b.atOnce=="number"&&b.atOnce>=1&&b.atOnce<=5&&(meta.atOnce=Math.round(b.atOnce)),(b.runner==="acp"||b.runner==="direct")&&(meta.runner=b.runner),typeof b.documentsInGit=="boolean"&&(meta.documentsInGit=b.documentsInGit),b.alwaysNewTests===!0?meta.alwaysNewTests=!0:b.alwaysNewTests===!1&&delete meta.alwaysNewTests,b.repos&&meta.repos&&(meta.repos=meta.repos.map(r=>{let u=b.repos.find(x=>x.path===r.path);if(!u)return r;let commands=Object.fromEntries(Object.entries(u.commands??r.commands).map(([k,v])=>[k,String(v??"").trim()]).filter(([,v])=>v)),next={base:u.base?.trim()||r.base,remote:u.remote?.trim()??r.remote};return checkWhereCodeGoes(path154.join(root,r.path),next.base,next.remote),{...r,...next,commands}})),writeMeta(bp,meta),meta})});let partChange=b=>({...typeof b.name=="string"?{name:b.name}:{},...typeof b.shared=="boolean"?{shared:b.shared}:{},...typeof b.repo=="string"?{repo:b.repo}:{},...typeof b.folder=="string"?{folder:b.folder}:{},...Array.isArray(b.needs)?{needs:b.needs.map(String)}:{},...typeof b.url=="string"?{url:b.url}:{},...typeof b.off=="boolean"?{off:b.off}:{}});app.get("/api/v1/parts",c=>{if(!boardExists(bp))return c.json({parts:[],repos:[],several:!1});let meta=readMeta(bp);return c.json({parts:partsOf2(meta),repos:repoPaths(meta),several:severalParts(meta)})}),app.post("/api/v1/parts",async c=>{let b=await body3(c);return act(c,()=>addPart(bp,partChange(b)))}),app.put("/api/v1/parts/:id",async c=>{let b=await body3(c);return act(c,()=>changePart(bp,c.req.param("id"),partChange(b)))}),app.post("/api/v1/parts/:id/remove",c=>act(c,()=>(removePart(bp,c.req.param("id")),{ok:!0}))),app.get("/api/v1/project-files",c=>boardExists(bp)?c.json(projectFilesView(bp)):c.json({local:[],locked:[],repos:[],docker:[]}));let fileRoute=(route,run7)=>app.post(`/api/v1/project-files/${route}`,async c=>{let b=await body3(c);return act(c,()=>run7(typeof b.repo=="string"?b.repo:void 0,typeof b.path=="string"?b.path:void 0))});fileRoute("local",(repo,file3)=>addLocalFile(bp,repo,file3)),fileRoute("locked",(repo,file3)=>addLockedFile(bp,repo,file3)),fileRoute("use-mine",(repo,file3)=>useMyFile(bp,repo,file3)),fileRoute("remove",(repo,file3)=>removeProjectFile(bp,repo,file3)),app.post("/api/v1/project-files/lock-docker",c=>act(c,()=>({locked:lockDockerFiles(bp)})));let noDatabase={database:null,template:null,stories:[],notes:[],missing:[],words:"squad-kit has not found a database in this project."};app.get("/api/v1/database",c=>boardExists(bp)?c.json(databaseView(bp)):c.json(noDatabase)),app.post("/api/v1/database/find",c=>act(c,()=>(learnDatabase(bp,{again:!0}),databaseView(bp)))),app.post("/api/v1/database/confirm",async c=>{let b=await body3(c),change=Object.fromEntries(["kind","name","repo","env","file","key","address"].filter(k=>typeof b[k]=="string").map(k=>[k,b[k]]));return act(c,()=>(confirmDatabase(bp,readMeta(bp).user.name,change),databaseView(bp)))}),app.post("/api/v1/database/template",c=>act(c,()=>(makeTemplateBeside(bp,readMeta(bp).user.name),databaseView(bp)))),app.put("/api/v1/database",async c=>{let b=await body3(c);return act(c,()=>(setDatabaseOptions(bp,{...typeof b.hidePersonal=="boolean"?{hidePersonal:b.hidePersonal}:{},...typeof b.sampleRows=="number"||b.sampleRows===null?{sampleRows:b.sampleRows}:{}}),databaseView(bp)))}),app.get("/api/v1/sandbox",c=>{let tools4=sandboxStatus(),note2=guardNote(process.platform,tools4),g=guardSupport(),allow=boardExists(bp)?readMeta(bp).guard:void 0,guard={on:g.on,why:g.why,...g.fix?{fix:g.fix}:{},allowRead:allow?.allowRead??[],allowWrite:allow?.allowWrite??[]},level=(boardExists(bp)?parseNetwork(readMeta(bp).network):null)??DEFAULT_NETWORK,network={level,commands:networkPolicy(level).commands,blocked:boardExists(bp)?blockedSites(bp).slice(0,20):[]};return c.json({tools:tools4,registries:PACKAGE_HOSTS,allowedHosts:boardExists(bp)?readMeta(bp).allowedHosts??[]:[],guard,network,...note2?{note:note2}:{}})}),app.get("/api/v1/agents",c=>{if(!boardExists(bp))return c.json([]);readMeta(bp).engine==="live"&&ensureAgents(bp);let tickets=listTickets(bp).filter(t=>t.live&&t.squad),mine2=personLevels(),quality=readMeta(bp).quality,STEP_NAME2={chat:"Talking it through",draft:"Filling in a new ticket"};return c.json(AGENTS.map(a=>{let agent=readAgent(bp,a.role),tool5=aiTool(agent.tool),floor=a.role==="reviewer"?{builder:builderLevel({quality},readAgent(bp,"builder").levels,mine2)}:{},levels=levelSteps(a.role).map(step2=>{let now3=resolveLevel({role:a.role,step:step2,quality,...floor},agent.levels,mine2),effort=tool5?effortOn(tool5,now3.effort):now3.effort;return{step:step2,name:STEPS[a.role].find(x=>x.id===step2)?.name??STEP_NAME2[step2]??step2,standard:standardLevel({role:a.role,step:step2,quality}),team:agent.levels[`${a.role}.${step2}`]??null,you:mine2[`${a.role}.${step2}`]??null,now:{...now3,effort,modelName:now3.model||tool5?.modelFor(now3.level)||""}}});return{...a,...agent,levels,teamRole:agent.levels[a.role]??null,youRole:mine2[a.role]??null,modelOf:Object.fromEntries(MODEL_LEVELS.map(l=>[l,tool5?.modelFor(l)??""])),supportsEffort:tool5?.supportsEffort??!1,efforts:tool5?toolEfforts(tool5):[],file:`.squad/board/agents/${a.role}.md`,stepList:STEPS[a.role],working:tickets.filter(t=>roleForPhase(t.squad?.phase)===a.role).map(t=>({id:t.id,title:t.title,status:t.status}))}}))});let addOne=(input7,from)=>{let meta=readMeta(bp),by2=meta.user.name;if(input7.kind!=="gate"){let added3=addAddition(bp,by2,input7,from);return{added:added3,...added3.kind==="rule"&&ruleNote(added3.text)?{note:ruleNote(added3.text)}:{}}}if(severalParts(meta)&&!input7.part)throw new Error("Pick which part it runs in.");let{added:added2,failsToday}=addTriedGate(bp,by2,input7,from);return{added:added2,...failsToday?{note:"It fails on today's code, so it stays on Try first until that is fixed."}:{}}};app.post("/api/v1/harness/additions",async c=>{let input7=await body3(c);return act(c,()=>addOne(input7))}),app.post("/api/v1/harness/additions/:id/state",async c=>{let{state}=await body3(c);return act(c,()=>setAdditionState(bp,readMeta(bp).user.name,c.req.param("id"),state))}),app.post("/api/v1/harness/additions/:id/remove",c=>act(c,()=>removeAddition(bp,readMeta(bp).user.name,c.req.param("id")))),app.post("/api/v1/harness/try",async c=>{let input7=await body3(c);return act(c,()=>tryGate(bp,readMeta(bp).user.name,input7))});let lookIn=folder=>{if(!folder?.trim())throw new Error("Choose a folder first.");return lookInFolder(folder,readHarness(bp).additions,reposOf(readMeta(bp))[0]?.commands??{})};app.post("/api/v1/harness/folder",async c=>{let{path:folder}=await body3(c);return act(c,()=>lookIn(folder))}),app.post("/api/v1/harness/folder/add",async c=>{let{path:folder,picks}=await body3(c);return act(c,()=>{let look=lookIn(folder),added2=[],left2=[];for(let pick2 of picks??[]){let f=look.finds.find(x=>x.key===pick2.key);if(!(!f||f.out||f.already))try{added2.push(addOne(additionOf(f,pick2),look.name).added.name)}catch(err2){left2.push({name:f.name,why:plainWords(err2,"Adding from a folder")})}}return{added:added2,left:left2}})}),app.get("/api/v1/harness",c=>c.json(boardExists(bp)?harnessView(readMeta(bp),bp):harnessView())),app.get("/api/v1/usage",c=>boardExists(bp)?c.json(boardUsage(bp)):c.json({total:{runs:0},project:{runs:0},roles:[],tickets:0})),app.put("/api/v1/agents/:role",async c=>{let b=await body3(c);return act(c,()=>writeAgent(bp,c.req.param("role"),{tool:b.tool,model:b.model?.trim(),instructions:b.instructions,steps:b.steps,...b.levels?{levels:b.levels}:{}}))}),app.put("/api/v1/me/levels",async c=>{let b=await body3(c);return act(c,()=>{let key=b.key??"",[role,step2]=key.split(".");if(!AGENTS.some(a=>a.role===role)||step2&&!levelSteps(role).includes(step2))throw new Error(`"${key}" is not a role or one of its steps.`);if(b.level&&!parseLevel(b.level))throw new Error(`"${b.level}" is not a level. Use strongest, balanced or fast.`);if(b.effort&&!parseEffort(b.effort))throw new Error(`"${b.effort}" is not an effort. Use xhigh (extra high), high, medium or low.`);return setPersonLevel(key,{level:parseLevel(b.level),effort:parseEffort(b.effort),model:b.model})})}),app.get("/api/v1/agents/:role/prompts",async c=>{let role=c.req.param("role");return AGENTS.some(a=>a.role===role)?c.json(await corePrompts(bp,role,c.req.query("ticket")||void 0)):c.json({error:"not found"},404)}),app.post("/api/v1/restart",c=>opts.control?(opts.control.restart(),c.json({ok:!0})):c.json({error:"This board cannot restart itself. Press Ctrl-C and run: squad console"},400)),app.post("/api/v1/scan",c=>act(c,()=>{let meta=readMeta(bp);if(meta.engine!=="live")throw new Error("Only a live board reads the project.");return meta.scan={status:"pending"},writeMeta(bp,meta),meta})),mountCostApi(app,bp),app.post("/api/v1/demo/reset",c=>{if(boardExists(bp)&&readMeta(bp).engine!=="demo")return c.json({error:"Only a demo board can be reset."},400);let speed=boardExists(bp)?readMeta(bp).speed:1,project=boardExists(bp)?readMeta(bp).project:void 0;return seedDemoBoard(bp,{speed,project}),c.json({ok:!0})})}import fs163 from"fs";import path155 from"path";function isMerged2(t,meta){return!!t.mergedAt||t.stage==="done"||meta.engine==="demo"&&t.stage==="release"}function branchOf2(t){return t.branch??`squad/${t.id.toLowerCase()}`}function repoDir2(bp,meta,t){return path155.join(bp.root,repoFor(meta,t).path)}function copyOf(dir,t){return t.worktree??path155.join(worktreeRoot(dir),t.id.toLowerCase())}function inside4(dir,file3){let rel=path155.relative(dir,file3);return!!rel&&!rel.startsWith("..")&&!path155.isAbsolute(rel)}function hasBranch(dir,branch){return!!git(dir,["rev-parse","--verify","-q",`refs/heads/${branch}`],{allowFail:!0})}function and4(items3){return items3.length<2?items3[0]??"":`${items3.slice(0,-1).join(", ")} and ${items3[items3.length-1]}`}function decisionsOf(bp,t){let others=listTickets(bp).filter(x=>x.id!==t.id),going=[],kept2=[];for(let d of listDecisions(bp).filter(x=>x.from===t.id)){let by2=others.filter(x=>x.follows?.includes(d.id)||x.replanFor===d.id).map(x=>x.id);by2.length?kept2.push({d,by:by2}):going.push(d)}return{going,kept:kept2}}function pushedTo(bp,dir,t){if(!isRepo(dir))return;let target=repoTarget(bp,t);if(!target)return;let branch=branchOf2(t);if(!git(dir,["rev-parse","--verify","-q",`refs/remotes/${target.remote}/${branch}`],{allowFail:!0})&&!t.handedOver&&!t.pr)return;let why=t.handedOver?"squad-kit pushed it when the ticket was handed over.":t.pr?`squad-kit pushed it for pull request ${t.pr.id}.`:`${target.remote} has it, pushed from this computer or a teammate's.`;return{remote:target.remote,branch,why}}function refusal(bp,meta,t){let base2=repoFor(meta,t).base;if(!isMine(t,meta)&&!t.ownerDeleted)return{why:`${t.id} is ${ownerOf(t)}'s. Only ${ownerOf(t)} can delete it.`};if(isMerged2(t,meta))return{why:`${t.id} is merged already: its code is in ${base2}, so it cannot be deleted. Archive it to hide it from the board. Its history stays.`,archive:!0};let going=releaseGoingOut(bp),draft=listReleases(bp).find(r=>r.status==="draft"&&r.tickets.includes(t.id));if(draft&&going===draft.version)return{why:`${draft.version} is going out right now with ${t.id}. Wait for it to finish.`};if(draft)return{why:`${t.id} is in ${draft.version}, the release being made, so it cannot be deleted. It goes out with that release; after that you can archive it.`};let dir=repoDir2(bp,meta,t);if(isRepo(dir)&&currentBranch(dir)===branchOf2(t))return{why:`Your project folder is on ${branchOf2(t)}. Switch it back to ${base2} first (git switch ${base2}), then delete ${t.id}.`}}function deletePlan(bp,id2,opts={}){let meta=readMeta(bp),t=requireTicket(bp,id2),dir=repoDir2(bp,meta,t),base2=repoFor(meta,t).base,no=refusal(bp,meta,t);if(!no&&!isMine(t,meta)&&t.ownerDeleted)return removePlan(bp,meta,t,dir,base2);let pushed=no?void 0:pushedTo(bp,dir,t),remote=!!(pushed&&(opts.remote??t.deleting?.remote)),removes=["The ticket, its spec, notes and questions"],stays=[`Your ${base2} code and every other ticket`],warnings=[],inGit=isRepo(dir),copy2=inGit&&fs163.existsSync(copyOf(dir,t)),branch=inGit&&hasBranch(dir,branchOf2(t));(copy2||branch)&&removes.push(t.copies?.length?"Its copies of the code":copy2?"Its copy of the code":"Its saved work on this computer"),fs163.existsSync(ticketCacheDir(bp,t.id))&&removes.push("The packages its Builder and Tester downloaded for it");let chats=!!(Object.values(t.sessions??{}).some(Boolean)||t.chat),shots=!!t.proof?.length;(chats||shots)&&removes.push(chats&&shots?"Its agent conversations and screenshots":chats?"Its agent conversations":"Its screenshots"),t.trackerQueue?.length&&removes.push(`${t.trackerQueue.length===1?"An update":`${t.trackerQueue.length} updates`} for the tracker that ${t.trackerQueue.length===1?"was":"were"} not sent yet`);let{going,kept:kept2}=decisionsOf(bp,t),all=listDecisions(bp);for(let d of going){removes.push(`Its decision ${d.id}, "${d.title}". It never reached ${base2}.`);let old=all.find(x=>x.replacedBy===d.id);old&&stays.push(`Decision ${old.id}, "${old.title}". It is a rule again, as it was before ${d.id} replaced it.`)}for(let r of listReleases(bp).filter(x=>x.bugs?.includes(t.id)))removes.push(`Its place among the bugs found in ${r.version}`);pushed&&remote&&removes.push(`Its saved work in your team's shared copy of the code${t.pr?`, and pull request ${t.pr.id} is closed`:""}`);let tracker2=trackerModule(t.source.type);tracker2&&stays.push(`The ${t.kind==="bug"?"bug":"story"}${t.source.ref?` ${t.source.ref}`:""} in ${tracker2.label}. Only the link to it is removed.`),stays.push("The cost it used, still counted in Usage");for(let k of kept2)stays.push(`Its decision ${k.d.id}, "${k.d.title}", because ${and4(k.by)} ${k.by.length===1?"follows":"follow"} it`);pushed&&!remote&&(stays.push(`Its saved work in your team's shared copy of the code, with a note that you deleted ${t.id}, so teammates' boards show it as deleted.`),t.pr&&stays.push(`Pull request ${t.pr.id}, still open. Close it on the code host if you no longer need it.`)),(t.live||t.squad||t.questions?.some(q5=>q5.thinking)||t.chat?.thinking)&&!t.deleting&&warnings.push("The squad is working on it. Delete stops it first and waits for it."),t.chat?.open&&warnings.push("A chat is open on it. Delete ends the chat."),t.takenOver&&warnings.push("You took it over. Close the session you opened in its copy first.");for(let k of kept2)warnings.push(`${and4(k.by)} ${k.by.length===1?"builds":"build"} on its decision ${k.d.id}.`);return{id:t.id,title:t.title,summary:[t.title,t.status,isMerged2(t,meta)?t.stage==="done"?`released${t.release?` in ${t.release}`:""}`:`merged into ${base2}`:"never merged"].filter(Boolean).join(" \xB7 "),...no?{refused:no.why,...no.archive?{archive:!0}:{}}:{},removes,stays,warnings,...pushed?{pushed}:{},...t.deleting?.error?{unfinished:t.deleting.error}:{},remote,base:base2,command:`squad delete ${t.id}${remote?" --remote":""}`}}function removePlan(bp,meta,t,dir,base2){let by2=t.ownerDeleted.by,local=localCopy(dir,t,base2,copyOf(dir,t)),removes=[`${t.id} from your board: its files, notes and questions on this computer`];(local.copy||local.branch)&&removes.push(local.copy?"Its copy of the code on this computer":"Its saved work on this computer");let chats=!!(Object.values(t.sessions??{}).some(Boolean)||t.chat),shots=!!t.proof?.length;(chats||shots)&&removes.push(chats&&shots?"Its agent conversations and screenshots on this computer":chats?"Its agent conversations on this computer":"Its screenshots on this computer");let{going}=decisionsOf(bp,t);for(let d of going)removes.push(`Its decision ${d.id}, "${d.title}", on this computer`);let stays=[`Everything on the remote: squad-kit pushes and deletes nothing there. ${by2}'s delete is theirs.`,`Your ${base2} code and every other ticket`,...local.kept,...t.usage?.length?["The cost it used on this computer, still counted in Usage"]:[]],warnings=t.chat?.open?["A chat is open on it. Removing it ends the chat."]:[];return{id:t.id,title:t.title,summary:[t.title,`Deleted by ${by2}`].join(" \xB7 "),removes,stays,warnings,remote:!1,local:{by:by2},base:base2,command:`squad delete ${t.id}`}}var sleep8=ms2=>new Promise(r=>setTimeout(r,ms2));function runsHere(bp,id2){return(agentRuns().find(r=>r.root===bp.root)?.keys??[]).filter(k=>k===id2||k.endsWith(`:${id2}`))}async function waitForStop(bp,id2,mark,ms2){let end=Date.now()+ms2;for(;;){let t=readTicket(bp,id2);if(!t)return;t.deleting||updateTicket(bp,id2,x=>x.deleting={...mark});let here=runsHere(bp,id2),there=squadRunsElsewhere(bp)&&!t.deleting?.stopped;if(!here.length&&!there)return;if(Date.now()>end)throw new Error("The squad did not stop in time");await sleep8(200)}}async function removeRemote(bp,t){let target=repoTarget(bp,t);if(!target)return[];let branch=branchOf2(t),host3=repoHostFor(bp,target.url),notes=[];if(t.pr&&host3)try{await host3.close(t.pr.id),notes.push(`Closed pull request ${t.pr.id}`)}catch(err2){throw new Error(`Could not close pull request ${t.pr.id}: ${plainWords(err2,`Closing pull request ${t.pr.id}`,"something technical stopped it. Try again.")}`)}try{await netGitLater(target.dir,["push",target.remote,"--delete",branch],{env:host3?.gitEnv()??{}}),notes.push(`Deleted ${branch} on ${target.remote}`)}catch(err2){if(!/remote ref does not exist/i.test(err2.message))throw new Error(`Could not delete its work on ${target.remote}: ${plainWords(err2,`Deleting ${branch} on ${target.remote}`,"something technical stopped it. Try again.")}`);git(target.dir,["update-ref","-d",`refs/remotes/${target.remote}/${branch}`],{allowFail:!0})}return notes}async function removeCopy(bp,meta,t,opts={}){let projectCopies2=worktreeRoot(bp.root),one2=async(dir2,base2,copy2)=>{if(!isRepo(dir2))return[];let root=worktreeRoot(dir2),keep=opts.safe?localCopy(dir2,t,base2,copy2):null;(!keep||keep.copy)&&(await removeWorktree(dir2,copy2),(inside4(root,copy2)||inside4(projectCopies2,copy2))&&await fs163.promises.rm(copy2,{recursive:!0,force:!0}));let before=path155.join(root,`${t.id.toLowerCase()}-before`);return fs163.existsSync(before)&&await dropSnapshot(dir2,before),await gitLater(dir2,["worktree","prune"],{allowFail:!0}),(!keep||keep.branch)&&hasBranch(dir2,branchOf2(t))&&await gitLater(dir2,["branch","-D",branchOf2(t)]),keep?.kept??[]},dir=repoDir2(bp,meta,t),kept2=await one2(dir,repoFor(meta,t).base,copyOf(dir,t));for(let c of t.copies??[])kept2.push(...await one2(path155.join(bp.root,c.repo),reposOf(meta).find(r=>r.path===c.repo)?.base??repoFor(meta,t).base,c.dir));return t.storyDir&&inside4(projectCopies2,t.storyDir)&&fs163.existsSync(t.storyDir)&&!copyDirsIn(t.storyDir).length&&await fs163.promises.rm(t.storyDir,{recursive:!0,force:!0}),kept2}async function leaveNote(bp,t,by2,at3){let target=repoTarget(bp,t),repo=docsRepo(bp);if(!target||!repo||repo!==target.dir)return[];let branch=branchOf2(t),env2=repoHostFor(bp,target.url)?.gitEnv()??{},ref2=`refs/remotes/${target.remote}/${branch}`;try{await netGitLater(target.dir,["fetch","--quiet",target.remote,`+refs/heads/${branch}:${ref2}`],{env:env2});let tip=git(target.dir,["rev-parse","--verify","-q",ref2],{allowFail:!0});if(!tip)return[];let note2=deleteNoteCommit(target.dir,tip,squadInRepo(bp,repo),t,by2,at3);return note2?(await netGitLater(target.dir,["push",target.remote,`${note2}:refs/heads/${branch}`],{env:env2}),git(target.dir,["update-ref",ref2,note2],{allowFail:!0}),[`Left a note on ${branch} on ${target.remote}, so teammates' boards show ${t.id} as deleted`]):[]}catch(err2){return[`Teammates still see ${t.id}: squad-kit could not leave a note on ${branch} on ${target.remote} (${err2.message})`]}}function unlinkDecisions(bp,t){let{going}=decisionsOf(bp,t),ids=new Set(going.map(d=>d.id));for(let d of listDecisions(bp)){if(ids.has(d.id))continue;let tickets=d.tickets.filter(x=>x!==t.id),buildsOn=d.buildsOn?.filter(x=>!ids.has(x)),back=!!(d.replacedBy&&ids.has(d.replacedBy));if(tickets.length===d.tickets.length&&buildsOn?.length===d.buildsOn?.length&&!back)continue;let{replacedBy,buildsOn:_was,...rest}=d;writeDecision(bp,{...rest,tickets,...buildsOn?.length?{buildsOn}:{},...replacedBy&&!back?{replacedBy}:{}})}if(ids.size){for(let x of listTickets(bp).filter(y=>y.id!==t.id&&y.outdated?.some(o=>ids.has(o.adr))))updateTicket(bp,x.id,y=>{y.outdated=y.outdated?.filter(o=>!ids.has(o.adr)),y.outdated?.length||delete y.outdated});let meta=readMeta(bp);meta.impactChecks?.some(c=>ids.has(c.adr))&&writeMeta(bp,{...meta,impactChecks:meta.impactChecks.filter(c=>!ids.has(c.adr))})}for(let r of listReleases(bp).filter(x=>x.bugs?.includes(t.id)))writeRelease(bp,{...r,bugs:r.bugs.filter(x=>x!==t.id)});for(let d of going)fs163.rmSync(path155.join(bp.decisionsDir,`${d.id}.md`),{force:!0})}function removeChatsAndShots(bp,t){let sessions=[...Object.values(t.sessions??{}),t.chat?.side].filter(s=>typeof s=="string"&&/^[\w-]{8,64}$/.test(s));for(let s of sessions)fs163.rmSync(path155.join(bp.chatsDir,`${s}.jsonl`),{force:!0});if(!fs163.existsSync(bp.proofDir))return;let shown2=new Set(listTickets(bp).filter(x=>x.id!==t.id).flatMap(x=>(x.proof??[]).flatMap(p=>[p.before,p.after,...(p.more??[]).map(m=>m.after)]))),own2=new RegExp(`^${t.id.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}-(before|after)-`),named=(t.proof??[]).flatMap(p=>[p.before,p.after,...(p.more??[]).map(m=>m.after)]).filter(Boolean).map(f=>path155.basename(f));for(let f of new Set([...named,...fs163.readdirSync(bp.proofDir).filter(f2=>own2.test(f2))]))shown2.has(f)||fs163.rmSync(path155.join(bp.proofDir,f),{force:!0})}function keepRecord(bp,t,mark,fromBoard=!1){let meta=readMeta(bp),rest=(meta.deleted??[]).filter(d=>!(d.id===t.id&&d.at===mark.at));writeMeta(bp,{...meta,deleted:[...rest,{id:t.id,title:t.title,at:mark.at,by:mark.by,...t.usage?.length?{usage:t.usage}:{},...fromBoard?{fromBoard}:{}}]})}function deleteTicket(bp,id2,opts={}){let key=`${bp.squadDir}\0${id2.toUpperCase()}`,now3=deletesNow.get(key);if(now3)return now3;let run7=deleteOnce(bp,id2,opts).finally(()=>deletesNow.delete(key));return deletesNow.set(key,run7),run7}var deletesNow=new Map;async function deleteOnce(bp,id2,opts){refuseWhileSaving(bp,id2);let first2=readTicket(bp,id2);if(!first2){if(fs163.existsSync(ticketStateFile(bp,id2)))return removeTicket(bp,id2),{id:id2.toUpperCase(),title:"",notes:[]};throw new BoardActionError(`No ticket ${id2.toUpperCase()} on this board.`)}let meta=readMeta(bp),no=refusal(bp,meta,first2);if(no)throw new BoardActionError(no.why);let local=!isMine(first2,meta),pushed=local?void 0:pushedTo(bp,repoDir2(bp,meta,first2),first2),remote=!!(opts.remote??first2.deleting?.remote)&&!!pushed,mark={at:first2.deleting?.at??now(),by:meta.user.name,...remote?{remote}:{},...first2.deleting?.remoteDone?{remoteDone:!0}:{}};updateTicket(bp,first2.id,x=>{x.deleting||addActivity(x,"you",local?"Pressed Remove from my board":"Pressed Delete"),x.deleting={...mark},x.status=local?"Removing\u2026":"Deleting\u2026",x.waiting=null,x.live=!1,x.squad=null});try{await waitForStop(bp,first2.id,mark,opts.waitMs??6e4);let t=requireTicket(bp,first2.id),notes=[];return remote&&!t.deleting?.remoteDone?(notes.push(...await removeRemote(bp,t)),updateTicket(bp,t.id,x=>x.deleting={...mark,...x.deleting,remoteDone:!0})):pushed&&!remote&&!t.deleting?.remoteDone&&(notes.push(...await leaveNote(bp,t,mark.by,mark.at)),updateTicket(bp,t.id,x=>x.deleting={...mark,...x.deleting,remoteDone:!0})),await stopTry(bp,t.id),notes.push(...(await removeCopy(bp,meta,t,{safe:local})).map(k=>`Kept ${k.charAt(0).toLowerCase()}${k.slice(1)}`)),await dropStoryDb(bp,t.id).catch(()=>!1),dropTicketCache(bp,t.id),unlinkDecisions(bp,t),removeChatsAndShots(bp,t),keepRecord(bp,t,mark,local),removeTicket(bp,t.id),{id:t.id,title:t.title,notes}}catch(err2){let why=err2.message.replace(/\.\s*$/,"");try{updateTicket(bp,first2.id,x=>{x.deleting={...mark,...x.deleting,error:why},x.status="Delete did not finish",addActivity(x,"squad",`Delete did not finish: ${why}`)})}catch{}throw new BoardActionError(`${first2.id} is not fully deleted yet: ${why}. Delete it again to finish; it goes on where it stopped.`)}}function archiveTicket(bp,id2){let meta=readMeta(bp),t=requireTicket(bp,id2);if(t.archived)return t;if(!isMerged2(t,meta))throw new BoardActionError(`${t.id} is not merged yet, so it stays on the board. Delete it instead if you no longer want it.`);return updateTicket(bp,t.id,x=>{x.archived={by:meta.user.name,at:now()},addActivity(x,"you","Archived it: off the board, its history kept")})}function unarchiveTicket(bp,id2){let t=requireTicket(bp,id2);if(!t.archived)throw new BoardActionError(`${t.id} is not archived.`);return updateTicket(bp,t.id,x=>{delete x.archived,addActivity(x,"you","Brought it back to the board")})}async function body4(c){try{return await c.req.json()}catch{return{}}}function mountDeleteApi(app,opts){let bp=boardPaths(opts.paths.squadDir);app.get("/api/v1/tickets/:id/delete",c=>act(c,()=>deletePlan(bp,c.req.param("id"),{remote:c.req.query("remote")==="1"?!0:c.req.query("remote")==="0"?!1:void 0}))),app.post("/api/v1/tickets/:id/delete",async c=>{let b=await body4(c);return act(c,()=>deleteTicket(bp,c.req.param("id"),{remote:!!b.remote}))}),app.post("/api/v1/tickets/:id/archive",async c=>{let b=await body4(c);return act(c,()=>b.undo?unarchiveTicket(bp,c.req.param("id")):archiveTicket(bp,c.req.param("id")))})}async function body5(c){try{return await c.req.json()}catch{return{}}}function how(b){return{...b.phone===!0?{phone:!0}:{},...b.web===!0?{web:!0}:{},...typeof b.language=="string"?{language:b.language}:{}}}function mountAppsApi(app,opts){let bp=boardPaths(opts.paths.squadDir),runner4=()=>appRunner(bp),name=c=>decodeURIComponent(c.req.param("name")??""),withNews=s=>({...s,...appNews(readMeta(bp),listTickets(bp),s,runner4().notNowOf(s.repo))});app.get("/api/v1/apps",async c=>{if(!boardExists(bp))return c.json({apps:[]});let meta=readMeta(bp),apps2=(await runner4().status()).map(withNews),reading=meta.scan?.status==="pending"||meta.scan?.status==="running";return c.json({apps:apps2,live:meta.engine==="live",reading,...meta.scan?.status==="failed"&&meta.scan.error?{readError:meta.scan.error}:{}})}),app.post("/api/v1/apps/:name/start",async c=>{let b=await body5(c);return act(c,async()=>withNews(await runner4().startAs(name(c),{nextPort:!!b.nextPort,stopOther:!!b.stopOther,...how(b)})))}),app.post("/api/v1/apps/:name/stop",c=>act(c,async()=>withNews(await runner4().stop(name(c))))),app.post("/api/v1/apps/:name/restart",async c=>{let b=await body5(c);return act(c,async()=>withNews(await runner4().restart(name(c),{nextPort:!!b.nextPort,stopOther:!!b.stopOther,...how(b)})))}),app.get("/api/v1/apps/:name/output",c=>act(c,async()=>{let s=await runner4().statusOf(name(c));return{name:s.name,start:s.start??null,url:s.url??null,address:s.address??null,dir:s.dir,commit:s.commit??null,lines:runner4().lines(name(c))}})),app.put("/api/v1/apps/:name/commands",async c=>{let b=await body5(c);return act(c,async()=>{let meta=readMeta(bp),a=findApp(meta,name(c));return writeMeta(bp,withAppCommands(meta,a.repo,{start:b.start,url:b.url})),withNews(await runner4().statusOf(a.name))})}),app.post("/api/v1/apps/:name/not-now",async c=>{let b=await body5(c);return act(c,()=>{if(!b.ticket)throw new Error("Say which ticket.");return runner4().notNow(name(c),b.ticket),{ok:!0}})})}async function body6(c){try{return await c.req.json()}catch{return{}}}function mountDependsApi(app,opts){let bp=boardPaths(opts.paths.squadDir);app.post("/api/v1/tickets/:id/depends",async c=>{let b=await body6(c);return act(c,()=>setDependsOn(bp,c.req.param("id"),Array.isArray(b.on)?b.on.map(String):[]))}),app.post("/api/v1/tickets/:id/build",async c=>{let b=await body6(c);return act(c,()=>chooseBuild(bp,c.req.param("id"),b.way))}),app.post("/api/v1/tickets/:id/bring-in",c=>act(c,()=>bringInLatest(bp,c.req.param("id")))),app.post("/api/v1/tickets/:id/check-merge",c=>act(c,()=>checkMerge(bp,c.req.param("id")))),app.post("/api/v1/releases/:v/take-out",async c=>{let b=await body6(c);return act(c,()=>takeOutOfRelease(bp,c.req.param("v"),b.ticket??""))}),app.post("/api/v1/releases/:v/put-back",async c=>{let b=await body6(c);return act(c,()=>putBackInRelease(bp,c.req.param("v"),b.ticket??""))}),app.get("/api/v1/releases/:v/out",c=>act(c,()=>outItems(bp,nextDraft(bp,c.req.param("v"))))),app.get("/api/v1/branches",c=>act(c,()=>readMeta(bp).engine==="live"?oldBranches(bp):{here:[],remote:[],kept:[]})),app.post("/api/v1/branches/clean",async c=>{let b=await body6(c);return act(c,()=>{if(readMeta(bp).engine!=="live")throw new Error("The sample board has no git branches to delete.");return cleanBranches(bp,{remote:!!b.remote})})})}async function body7(c){try{return await c.req.json()}catch{return{}}}function mountOverviewApi(app,opts){let bp=boardPaths(opts.paths.squadDir);app.get("/api/v1/setup",c=>c.json({setup:boardExists(bp)?setupView(bp):null})),app.post("/api/v1/setup/stop",c=>act(c,()=>stopSetup(bp))),app.post("/api/v1/setup/again",async c=>{let b=await body7(c);return act(c,()=>setupAgain(bp,b.step??"read"))}),app.post("/api/v1/setup/quality",async c=>{let b=await body7(c);return act(c,()=>({setup:setupQuality(bp,b.quality??"")}))}),app.get("/api/v1/overview",c=>boardExists(bp)?c.json(overviewView(bp)):c.json({error:"not found"},404)),app.post("/api/v1/overview/approve",c=>act(c,()=>approveOverview(bp))),app.post("/api/v1/overview/ideas/:n",c=>act(c,()=>ticketFromIdea(bp,Number(c.req.param("n"))))),app.post("/api/v1/overview/idea",async c=>{let b=await body7(c);return act(c,()=>tellIdea(bp,b.text??""))}),app.post("/api/v1/overview/read-again",c=>act(c,()=>setupAgain(bp,"read"))),app.get("/api/v1/overview/talk",c=>c.json({talk:boardExists(bp)?readTalk(bp):null})),app.post("/api/v1/overview/talk",async c=>{let b=await body7(c);return act(c,()=>({talk:readTalk(bp)?.open&&b.text?.trim()?sendTalk(bp,b.text):startTalk(bp,b.text)}))}),app.post("/api/v1/overview/talk/stop",c=>act(c,()=>({talk:stopTalk(bp)}))),app.post("/api/v1/overview/talk/close",c=>act(c,()=>({talk:closeTalk(bp)}))),app.post("/api/v1/overview/talk/read-again",c=>act(c,()=>readAgainWithTalk(bp)))}import fs164 from"fs";import path156 from"path";async function body8(c){try{return await c.req.json()}catch{return{}}}function mountBuildSendApi(app,bp){let view=id2=>viewBreakdown(bp,id2);app.get("/api/v1/requirements/waiting",c=>act(c,()=>({breakdowns:breakdownWaits(bp)}))),app.post("/api/v1/requirements/:id/build",async c=>{let b=await body8(c);return act(c,()=>{if(typeof b.on!="boolean")throw new BoardActionError('Say { "on": true } to build continuously, or { "on": false } to pause.');return view(setBuild(bp,c.req.param("id"),b.on).id)})}),app.get("/api/v1/requirements/:id/send",c=>act(c,()=>sendPreview(bp,c.req.param("id")))),app.post("/api/v1/requirements/:id/send",c=>act(c,()=>view(requestSend(bp,c.req.param("id")).id)))}async function body9(c){try{return await c.req.json()}catch{return{}}}var str3=v=>typeof v=="string"?v:void 0;function patchOf(b){return{...str3(b.title)!==void 0?{title:b.title}:{},...str3(b.goal)!==void 0?{goal:b.goal}:{},...str3(b.summary)!==void 0?{summary:b.summary}:{},...str3(b.story)!==void 0?{story:b.story}:{},...Array.isArray(b.criteria)?{criteria:b.criteria}:{},...str3(b.size)!==void 0?{size:b.size}:{},...Array.isArray(b.needs)?{needs:b.needs.map(String)}:{}}}function mountRequirementsApi(app,opts){let bp=boardPaths(opts.paths.squadDir),view=id2=>viewBreakdown(bp,id2);mountBuildSendApi(app,bp),app.get("/api/v1/requirements",c=>act(c,()=>{let broken=brokenBreakdowns(bp);return{breakdowns:breakdownRows(bp),...broken.length?{broken}:{}}})),app.post("/api/v1/requirements",async c=>{let b=await body9(c),files=Array.isArray(b.files)?b.files:[],file3=b.file??(str3(b.text)?void 0:files[0]),extra=[...Array.isArray(b.with)?b.with:[],...b.file||str3(b.text)?files:files.slice(1)];return act(c,async()=>view(addBreakdown(bp,{...file3?{file:file3}:{},...str3(b.text)?{text:b.text}:{},...str3(b.name)?{name:b.name}:{},with:extra}).id))}),app.get("/api/v1/requirements/:id",async c=>{let problem2=breakdownProblem(bp,c.req.param("id"));return problem2?c.json({error:problem2},409):readBreakdown(bp,c.req.param("id"))?act(c,()=>view(c.req.param("id"))):c.json({error:"not found"},404)}),app.get("/api/v1/requirements/:id/file",c=>{let b=readBreakdown(bp,c.req.param("id")),asked=c.req.query("path")??"";if(!b)return c.json({error:"not found"},404);let dir=breakdownDir(bp,b.id),file3=path156.resolve(dir,asked),rel=path156.relative(dir,file3);if(!asked||rel.startsWith("..")||path156.isAbsolute(rel)||!/^(source|text|media)[\\/]/.test(rel)||!fs164.existsSync(file3)||!fs164.statSync(file3).isFile())return c.json({error:"not found"},404);let type=mimeOf(file3),scripts=type==="image/svg+xml"||type==="text/html";return new Response(fs164.readFileSync(file3),{headers:{"content-type":type,"cache-control":"no-store","x-content-type-options":"nosniff",...scripts?{"content-disposition":dispositionOf("attachment",file3)}:{}}})}),app.patch("/api/v1/requirements/:id",async c=>{let b=await body9(c);return act(c,()=>view(renameBreakdown(bp,c.req.param("id"),b.title??"").id))}),app.delete("/api/v1/requirements/:id",c=>act(c,()=>deleteBreakdown(bp,c.req.param("id")))),app.post("/api/v1/requirements/:id/stop",c=>act(c,()=>view(stopBreakdown(bp,c.req.param("id")).id))),app.post("/api/v1/requirements/:id/retry",c=>act(c,()=>view(retryBreakdown(bp,c.req.param("id")).id))),app.post("/api/v1/requirements/:id/more",c=>act(c,()=>view(moreBreakdown(bp,c.req.param("id")).id))),app.post("/api/v1/requirements/:id/answers",async c=>{let b=await body9(c);return act(c,()=>view(answerBreakdown(bp,c.req.param("id"),b.answers??{}).id))}),app.post("/api/v1/requirements/:id/skip",c=>act(c,()=>view(skipBreakdownQuestions(bp,c.req.param("id")).id))),app.post("/api/v1/requirements/:id/questions/:q/ask",async c=>{let b=await body9(c);return act(c,()=>view(askAboutQuestion(bp,c.req.param("id"),c.req.param("q"),b.text??"").id))}),app.post("/api/v1/requirements/:id/items",async c=>{let b=await body9(c);return act(c,async()=>{let r=addItem(bp,c.req.param("id"),{...patchOf(b),title:b.title??"",...str3(b.under)?{under:b.under}:{},...b.level==="epic"||b.level==="feature"||b.level==="story"?{level:b.level}:{},...str3(b.before)?{before:b.before}:{}});return{key:r.key,breakdown:await view(r.breakdown.id)}})}),app.patch("/api/v1/requirements/:id/items/:key",async c=>{let b=await body9(c);return act(c,()=>view(editItem(bp,c.req.param("id"),c.req.param("key"),patchOf(b)).id))}),app.post("/api/v1/requirements/:id/items/:key/move",async c=>{let b=await body9(c);return act(c,()=>view(moveItem(bp,c.req.param("id"),c.req.param("key"),{...str3(b.under)?{under:b.under}:{},...str3(b.before)?{before:b.before}:{}}).id))}),app.delete("/api/v1/requirements/:id/items/:key",c=>act(c,()=>view(removeItem(bp,c.req.param("id"),c.req.param("key")).id))),app.post("/api/v1/requirements/:id/items/:key/redo",async c=>{let b=await body9(c);return act(c,()=>view(redoItem(bp,c.req.param("id"),c.req.param("key"),b.ask??"").id))}),app.post("/api/v1/requirements/:id/approve",c=>act(c,()=>view(approveBreakdown(bp,c.req.param("id")).id))),app.post("/api/v1/requirements/:id/items/:key/start",c=>act(c,async()=>({ticket:startStory(bp,c.req.param("id"),c.req.param("key")),breakdown:await view(c.req.param("id"))}))),app.post("/api/v1/requirements/:id/decided/:d",async c=>{let b=await body9(c);return act(c,()=>view(changeDecided2(bp,c.req.param("id"),c.req.param("d"),b.text??"",{whole:b.whole===!0}).id))})}function mountTryApi(app,opts){let bp=boardPaths(opts.paths.squadDir),runner4=()=>tryRunner(bp);app.get("/api/v1/tickets/:id/try",c=>act(c,()=>runner4().status(c.req.param("id")))),app.post("/api/v1/tickets/:id/try",async c=>{let b=await c.req.json().catch(()=>({}))??{},how2={...b.phone===!0?{phone:!0}:{},...b.web===!0?{web:!0}:{},...typeof b.language=="string"?{language:b.language}:{}};return act(c,()=>runner4().startAs(c.req.param("id"),how2))}),app.post("/api/v1/tickets/:id/try/stop",c=>act(c,()=>runner4().stop(c.req.param("id"))))}function mountToolsApi(app,opts){let bp=boardPaths(opts.paths.squadDir);app.get("/api/v1/tools",c=>act(c,()=>projectTools(bp))),app.post("/api/v1/tools/check",c=>act(c,async()=>(await checkTools(),projectTools(bp)))),app.post("/api/v1/tools/:tool/get",c=>act(c,()=>(getToolNow(c.req.param("tool"),opts.source?{source:opts.source}:{}),projectTools(bp)))),app.post("/api/v1/tools/:tool/update",c=>act(c,async()=>{let b=await c.req.json().catch(()=>({}));return await updateToolNow(bp,c.req.param("tool"),{ok:b?.ok===!0}),projectTools(bp)}))}async function phoneView(){let phone=shotPhone(),home2=squadHome(),base2={times:stepTimes(home2),idleMinutes:Math.round(IDLE_MS2/6e4)},notHere=phone.notHere();if(notHere)return{notHere,devices:[],...base2};let saved=savedDevices(home2),states=saved.length?new Map((await phone.devices().catch(()=>[])).map(d=>[d.id,d.state])):new Map,used=lastUsed(home2),busy=phoneInUse();return{devices:saved.map(d=>{let state=states.get(d.id)??"gone",last=used[d.id],off=state==="booted"&&last&&!busy?new Date(Date.parse(last)+IDLE_MS2).toISOString():void 0;return{size:d.size,name:d.name,model:d.model,runtime:d.runtime,id:d.id,state,inUse:busy&&state==="booted",keptUp:keptUp(d.id),...last?{lastUsed:last}:{},...off?{offAt:off}:{}}}),...base2}}async function stopPhoneNow(){let phone=shotPhone(),word2=phone.words.phone;if(phone.notHere())return{...await phoneView(),message:`${phone.notHere()} There is nothing to stop here.`};let r=await stopPhones(phone);r.busy||closeDrivers();let message=r.busy?`squad-kit's ${word2} is taking pictures for a story now. It shuts down by itself when it has been unused for ${Math.round(IDLE_MS2/6e4)} minutes.`:r.left.length?`Something else keeps squad-kit's ${word2} open, like a Simulator window, so squad-kit left it. Close that window to shut it down.`:r.stopped.length?`squad-kit shut down its ${[...new Set(r.stopped.map(id2=>phone.words[savedDevices().find(d=>d.id===id2)?.size??"phone"]))].join(" and ")}.`:"squad-kit's phones are not running.";return{...await phoneView(),message}}function mountPhoneApi(app){app.get("/api/v1/phone",c=>act(c,()=>phoneView())),app.post("/api/v1/phone/stop",c=>act(c,()=>stopPhoneNow()))}function mountApi(app,opts){mountMetaApi(app,opts),mountDashboardApi(app,opts),mountProjectsApi(app,opts),mountStoriesApi(app,opts),mountPlansApi(app,opts),mountConfigApi(app,opts),mountRunsApi(app,opts),mountSecretsApi(app,opts),mountPlannerAuthApi(app,opts),mountTrackerApi(app,opts),mountDoctorApi(app,opts),mountCopyPlanPromptApi(app,opts),mountBoardApi(app,opts),mountDeleteApi(app,opts),mountAppsApi(app,opts),mountDependsApi(app,opts),mountOverviewApi(app,opts),mountRequirementsApi(app,opts),mountTryApi(app,opts),mountToolsApi(app,opts),mountPhoneApi(app),app.onError(onUnexpected)}function setupSummary(bp){let v=setupView(bp);return v?{line:setupLine(v),ready:v.ready,approved:v.approved,...v.brd?{brd:v.brd}:{}}:{}}var OpenProjects=class{constructor(opts){this.opts=opts}opts;open=new Map;closed=new Map;first=null;get(id2){return this.open.get(id2)}all(){return[...this.open.values()]}openSaved(){for(let entry of readProjects())if(!(this.open.has(entry.id)||[...this.open.values()].some(p=>p.root===entry.root))){if(!fs165.existsSync(path157.join(entry.root,SQUAD_DIR,"config.yaml"))){this.closed.set(entry.id,{...entry,why:fs165.existsSync(entry.root)?"Its squad-kit files are gone from the folder.":"The folder is not there any more."});continue}try{this.prepareAndOpen(entry.root)}catch(err2){this.closed.set(entry.id,{...entry,why:err2.message})}}}add(folder,choice){let asked=path157.resolve(folder);if(!fs165.existsSync(asked)||!fs165.statSync(asked).isDirectory())throw new Error(`There is no folder at ${asked}.`);let ask2=addAsk(asked);if(ask2&&(!choice||choice==="here"&&ask2.kind==="inside-repo"))throw new Error(ask2.words);let root=ask2?choice==="above"?ask2.root:asked:findSquadRoot(asked)??asked,known=this.all().find(p=>p.root===root);return known?{project:known,did:[],added:!1}:{...this.prepareAndOpen(root),added:!0}}openSample(){let root=demoFolder(),paths=buildPaths(root);fs165.existsSync(paths.configFile)||(fs165.mkdirSync(paths.squadDir,{recursive:!0}),saveConfig(paths.configFile,{...DEFAULT_CONFIG,project:{...DEFAULT_CONFIG.project,name:"Case portal"}}));let bp=boardPaths(paths.squadDir);return boardExists(bp)||seedDemoBoard(bp),this.add(root).project}create(input7){let made=makeNewProject(input7);try{return{project:this.openReady(made.paths),did:made.did,opens:made.opens}}catch(err2){throw this.open.has(made.bp.projectId)&&this.remove(made.bp.projectId),dropNewProject(made.bp.root,made.bp),err2}}prepareAndOpen(root){if(runningConsole(boardPaths(path157.join(root,SQUAD_DIR))))throw new Error("Its board is open in another squad-kit. Close that one first, then add it here.");let ready=prepareProject(root);return{project:this.openReady(ready.paths),did:ready.did}}openReady(paths){let bp=boardPaths(paths.squadDir),id2=bp.projectId,same5=this.open.get(id2);if(same5&&same5.root!==bp.root){if(fs165.existsSync(path157.join(same5.root,SQUAD_DIR)))throw new Error(`The same project is open already from ${same5.root}. squad-kit can run one copy of a project at a time.`);same5.close(),this.open.delete(id2)}if(this.open.get(id2))return this.open.get(id2);let api=new Hono;api.use("/api/*",authMiddleware(this.opts.token)),mountApi(api,{paths,control:this.opts.control});let stopTimer=startDemoSquad(bp),stopLive=new LiveSquad(bp).start(),releaseLock=claimSquad(bp);writeConsoleInfo(bp,this.opts.info());let project={id:id2,root:bp.root,paths,bp,api,close:()=>{stopTimer(),stopLive(),stopApps(bp),stopTries(bp),releaseLock(),writeConsoleInfo(bp,{...this.opts.info(),pid:null})}};this.open.set(id2,project);for(let c of[...this.closed.values()])(c.id===id2||c.root===bp.root)&&this.closed.delete(c.id);return this.opts.remember&&rememberProject(id2,bp.root),project}remove(id2){let p=this.open.get(id2);p?.close(),this.open.delete(id2);let was=this.closed.delete(id2)||!!p;return this.first===id2&&(this.first=null),this.opts.remember&&forgetProject(id2),was}closeAll(){for(let p of this.open.values())p.close();closeDrivers(),stopPhones(shotPhone()).catch(()=>{})}summaries(){let rows2=[];for(let p of this.open.values()){let base2={id:p.id,name:projectName(p.root),root:p.root,url:`/p/${encodeURIComponent(p.id)}/board`,tickets:0,waiting:0,working:0};try{if(!boardExists(p.bp)){let why=settingsProblem(p.bp);rows2.push({...base2,open:!0,...why?{why}:{}});continue}let meta=readMeta(p.bp),open3=listTickets(p.bp).filter(t=>t.stage!=="done");rows2.push({...base2,name:meta.project||base2.name,open:!0,engine:meta.engine,...meta.scan&&(meta.engine==="live"||meta.setup)?{setup:{status:meta.scan.status,now:meta.scan.now,error:meta.scan.error,...setupSummary(p.bp)}}:{},tickets:open3.length,waiting:inbox(p.bp).length,working:open3.filter(t=>t.live&&t.squad&&!t.waiting).length})}catch(err2){rows2.push({...base2,open:!0,why:err2.message})}}for(let c of this.closed.values())rows2.push({id:c.id,name:projectName(c.root),root:c.root,url:`/p/${encodeURIComponent(c.id)}/board`,open:!1,why:c.why,tickets:0,waiting:0,working:0});return rows2}waiting(){let items3=[];for(let p of this.open.values())try{if(!boardExists(p.bp))continue;let project={id:p.id,name:readMeta(p.bp).project||projectName(p.root)};for(let t of inbox(p.bp))items3.push({project,ticket:t,at:t.activity[0]?.at??t.createdAt})}catch{}return items3.sort((a,b)=>b.at.localeCompare(a.at)).map(({project,ticket})=>({project,ticket}))}};async function startConsoleServer(opts){let bindPort=opts.requestedPort===0?await reserveEphemeralPort():await findFreePortFromBase(opts.requestedPort),app=new Hono2,startedAt=new Date().toISOString(),restart=!1,stopForRestart=()=>{};app.use("*",ownAddressOnly(bindPort)),app.get("/healthz",c=>c.json({ok:!0,version:readInstalledPackage().version,port:bindPort,startedAt},200,{"cache-control":"no-store"}));let MOCKUP_PATH=/^(\/p\/[^/]+)?\/mockup\/[^/]+$/,MOCKUP_CSP="sandbox allow-scripts; default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data:; font-src data:; frame-ancestors 'self'",strict=secureHeaders({contentSecurityPolicy:{defaultSrc:["'self'"],styleSrc:["'self'","'unsafe-inline'"],imgSrc:["'self'","data:"],connectSrc:["'self'"],scriptSrc:["'self'"],frameAncestors:["'none'"]},referrerPolicy:"no-referrer"}),FILE_PATH=/\/tickets\/[^/]+\/file$/;app.use("*",async(c,next)=>{if(!MOCKUP_PATH.test(c.req.path)){await strict(c,next);let csp=c.res.headers.get("content-security-policy");FILE_PATH.test(c.req.path)&&c.res.headers.get("content-type")==="application/pdf"&&csp&&c.res.headers.set("content-security-policy",csp.replace("frame-ancestors 'none'","frame-ancestors 'self'"));return}await next(),c.res.headers.set("content-security-policy",MOCKUP_CSP),c.res.headers.set("x-content-type-options","nosniff"),c.res.headers.set("referrer-policy","no-referrer")}),app.use("*",cors({origin:"http://127.0.0.1:"+bindPort}));let control={startedAt,build:opts.build??0,restart:()=>{restart=!0,setTimeout(()=>stopForRestart(),200)}},actualPort=bindPort,info2=()=>({pid:process.pid,port:actualPort,token:opts.token,startedAt,version:readInstalledPackage().version,build:opts.build??0}),projects=new OpenProjects({token:opts.token,control,info:info2,remember:!!opts.app});app.use("/api/*",authMiddleware(opts.token)),mountAppApi(app,{projects});let updates=new UpdateService({busy:()=>runsNow(projects),carrying:()=>ticketsInProgress(projects),restart:control.restart,...opts.update});mountUpdateApi(app,updates),mountSupportApi(app,{projects}),app.onError(onUnexpected),app.all("/p/:project/*",async(c,next)=>{let url=new URL(c.req.url),prefix=url.pathname.split("/").slice(0,3).join("/");if(url.pathname=url.pathname.slice(prefix.length),!url.pathname.startsWith("/api/")&&!url.pathname.startsWith("/mockup/"))return next();let p=projects.get(c.req.param("project"));return p?p.api.fetch(new Request(url,c.req.raw),c.env):c.json({error:"This project is not open in the app. Add it from Home."},404)});let first2=()=>projects.first?projects.get(projects.first):void 0;app.all("/api/*",c=>{let p=first2();return p?p.api.fetch(c.req.raw,c.env):c.json({error:"Name the project: /p/<project id>/api/v1/\u2026"},404)}),app.get("/mockup/:id",async c=>{for(let p of[first2(),...projects.all()]){if(!p)continue;let res=await p.api.fetch(c.req.raw,c.env);if(res.status!==404)return res}return c.text("not found",404)}),app.all("/mcp",c=>answerWindow(c.req.raw)),mountStaticAssets(app,{paths:opts.paths});let nodeServer=serve({fetch:app.fetch,hostname:"127.0.0.1",port:bindPort});nodeServer.keepAliveTimeout=KEEP_IDLE_MS,actualPort=(()=>{let addr=nodeServer.address();return addr&&typeof addr=="object"?addr.port:bindPort})(),windowAt(`http://127.0.0.1:${actualPort}`),opts.paths&&(projects.first=projects.openReady(opts.paths).id),opts.app&&projects.openSaved(),log.info(`squad-kit ${readInstalledPackage().version} started on port ${actualPort} with ${projects.all().length} project${projects.all().length===1?"":"s"} open (Node ${process.version})`);let stopSquad=()=>projects.closeAll(),resolveDone=()=>{},done2=new Promise(resolve2=>{resolveDone=resolve2}),shutdown=onClose=>{log.info("squad-kit stopped"),process.off("SIGINT",stop),process.off("SIGTERM",stop),process.off("SIGHUP",onHangup),stopSquad(),windowAt(null),closeServer(nodeServer,onClose)},stop=()=>{shutdown(err2=>{err2&&warning(`server close: ${err2.message}`),resolveDone()})};stopForRestart=stop,process.once("SIGINT",stop),process.once("SIGTERM",stop);let onHangup=opts.keepOnHangup?()=>log.info("The terminal closed; squad-kit keeps running, as it was started to outlive it"):stop;return opts.keepOnHangup?process.on("SIGHUP",onHangup):process.once("SIGHUP",onHangup),{port:actualPort,done:done2,restartRequested:()=>restart,projects,updates,close:()=>new Promise((resolve2,reject)=>{shutdown(err2=>{if(err2){reject(err2);return}resolve2(),resolveDone()})})}}function startedToOutlive(fds=[1,2],info2={isatty,fstat:fs166.fstatSync}){return fds.every(fd=>{try{if(info2.isatty(fd))return!1;let st=info2.fstat(fd);return!st.isFIFO()&&!st.isSocket()}catch{return!1}})}function reserveEphemeralPort(){return new Promise((resolve2,reject)=>{let s=createServer();s.once("error",reject),s.listen(0,"127.0.0.1",()=>{let addr=s.address();s.close(closeErr=>{if(closeErr){reject(closeErr);return}addr&&typeof addr=="object"?resolve2(addr.port):reject(new Error("Could not determine ephemeral port"))})})})}async function findFreePortFromBase(start,attempts=20){for(let i=0;i<attempts;i+=1){let candidate=start+i;if(await isPortFree(candidate))return candidate}throw new Error(`Could not find a free port in [${start}, ${start+attempts-1}]. Pass \`--port <n>\` to pick another base.`)}function isPortFree(port){return new Promise(resolve2=>{let probe=createServer();probe.once("error",()=>resolve2(!1)),probe.once("listening",()=>probe.close(()=>resolve2(!0))),probe.listen(port,"127.0.0.1")})}function outputGone(err2){let e=err2;return e?.code==="EPIPE"&&e.syscall==="write"||e?.code==="ERR_STREAM_DESTROYED"||e?.code==="ERR_STREAM_WRITE_AFTER_END"}function fatal(err2){let e=err2;return e?.syscall==="listen"&&(e.code==="EADDRINUSE"||e.code==="EACCES")}function installCrashNet(proc=process,say=text3=>log.error(text3)){let saying=!1,onError=(err2,kind)=>{if(!outputGone(err2)){if(!saying){saying=!0;try{say(`squad-kit kept running after ${kind}: ${err2 instanceof Error?err2.stack??err2.message:String(err2)}`)}catch{}finally{saying=!1}}fatal(err2)&&proc.exit(1)}},quiet=()=>{};proc.stdout?.on("error",quiet),proc.stderr?.on("error",quiet);let uncaught=err2=>onError(err2,"an error nobody caught"),rejected=err2=>onError(err2,"a promise that failed with nobody waiting");return proc.on("uncaughtException",uncaught),proc.on("unhandledRejection",rejected),()=>{proc.off("uncaughtException",uncaught),proc.off("unhandledRejection",rejected),proc.stdout?.off("error",quiet),proc.stderr?.off("error",quiet)}}import kleur9 from"kleur";import fs167 from"fs";import path158 from"path";import kleur8 from"kleur";import{confirm as confirm10,input as input5,select as select8,Separator}from"@inquirer/prompts";import kleur6 from"kleur";var out=(s="")=>process.stdout.write(`${s}
2023
+ `),cmd=s=>kleur6.green(s),amber=s=>kleur6.yellow(s);function board2(){let{bp}=openBoard(buildPaths(requireSquadRoot()).squadDir);if(!boardExists(bp))throw new Error("This project has no board yet. Run `squad demo` to try squad-kit v1 with a sample board.");return bp}function barText(now3){return(now3.bar??[]).map(b=>b==="done"?kleur6.green("\u25B0"):b==="now"?kleur6.green("\u25B0"):kleur6.gray("\u25B1")).join("")}function nowLines(t,now3){let lines3=[],pad2=" ";now3.state==="working"?(lines3.push(` ${kleur6.bold("Now")} ${now3.line}`),lines3.push(`${pad2}${kleur6.gray(now3.time??"")}${now3.bar?.length?` ${barText(now3)}`:""}`),lines3.push(`${pad2}${now3.sentence}`),now3.next&&lines3.push(`${pad2}${kleur6.gray("Next:")} ${now3.next}`)):(lines3.push(` ${kleur6.bold("Now")} ${now3.state==="you"?amber(now3.sentence):now3.sentence}`),now3.next&&now3.state!=="you"&&lines3.push(`${pad2}${now3.next}`));let s=now3.slow;if(s){lines3.push(""),lines3.push(` ${s.calm?kleur6.bold(s.title):amber(s.title)} ${kleur6.gray(s.where)}`),lines3.push(`${pad2}${s.text}`);for(let f of s.facts)lines3.push(`${pad2}${kleur6.gray(`${f.label}:`)} ${f.value}`);s.limit&&s.kind==="stuck"&&lines3.push(`${pad2}${kleur6.gray(s.limit)}`);let ways=s.kind==="stuck"&&s.canEnd?[`squad unstick ${t.id}`]:[`squad wait ${t.id} --more 10m`];lines3.push(`${pad2}${[...ways,`squad stop ${t.id} --again`,`squad stop ${t.id}`].map(cmd).join(kleur6.gray(" or "))}`)}return lines3}async function runTicketStatus(ticketId){let bp=board2(),t=requireTicket(bp,ticketId);out(),out(` ${kleur6.bold(t.id)} ${kleur6.bold(t.title)}`);for(let l of nowLines(t,liveNow(t,readUsual(bp))))out(l);out()}function offerCommand(t,o,question){let q5=shellQuote(question);switch(o.do){case"propose":return`squad propose ${t.id} ${q5}`;case"note":return o.to?`squad message ${t.id} --to ${o.to} ${q5}`:`squad note ${t.id} ${q5}`;case"chat":return`squad chat ${t.id} --with ${o.to}`;case"stop":return`squad stop ${t.id}`}}async function runAsk(ticketId,words2){let question=words2.join(" ").trim();if(!question)throw new Error('Ask a question, for example: squad ask SK-12 "is there a problem?"');let bp=board2(),t=requireTicket(bp,ticketId),usual=readUsual(bp),a=answerAsk(t,liveNow(t,usual),usual,question);out(),out(` ${kleur6.bold("squad-kit")}${a.instant?kleur6.gray(a.lang==="ar"?" \u0625\u062C\u0627\u0628\u0629 \u0641\u0648\u0631\u064A\u0629":" answered at once"):""}`);for(let l of a.lines)out(` ${l}`);if(a.offers.length){out();for(let o of a.offers)out(` ${o.label}${"note"in o?kleur6.gray(` ${o.note}`):""}
2024
2024
  ${cmd(offerCommand(t,o,question))}`)}out()}function parseMinutes(text3){let m=/^\s*(\d+(?:\.\d+)?)\s*(h|hours?|m|min|mins|minutes?|s|sec|seconds?)?\s*$/i.exec(text3??"10");if(!m)throw new Error(`"${text3}" is not a time. Use minutes, like --more 10m.`);let n=Number(m[1]),unit=(m[2]??"m").toLowerCase();return unit.startsWith("h")?n*60:unit.startsWith("s")?n/60:n}async function runWait(ticketId,opts){let bp=board2(),minutes4=Math.max(1,Math.round(parseMinutes(opts.more))),t=keepWaiting(bp,ticketId,minutes4),limit=t.clock?.limit;success(`OK, waiting. squad-kit stays quiet about ${t.id} for ${minutes4} minute${minutes4===1?"":"s"}.${limit?` The step still stops at ${limit} minutes.`:""}`)}import kleur7 from"kleur";var out2=(s="")=>process.stdout.write(`${s}
2025
2025
  `),cmd2=s=>kleur7.green(s),amber2=s=>kleur7.yellow(s),usd3=n=>n<.005?"under $0.01":`$${n.toFixed(2)}`,pct=share2=>`${Math.round(share2*100)}%`;function clock2(iso2){return new Date(iso2).toTimeString().slice(0,5)}function paidWords(p){let how2=p.payment==="subscription"?`your ${p.name} plan`:"an API key, paid per use",why=p.from==="you"?"you set it":p.from==="found"?`${p.tool} says ${p.found?.label??"so"}`:"squad-kit could not tell, so it assumes a plan";return`${how2} (${why})`}function costLines(c){let lines3=[],value=c.total.costUsd;if(c.payment.payment==="subscription")lines3.push(` ${kleur7.bold("Cost")} ${kleur7.green(`Included in your ${c.payment.name} plan. No extra charge.`)}`),value!==void 0&&lines3.push(kleur7.gray(` Value of the work at API prices: ${usd3(value)}. A value, not a bill. This month: ${usd3(c.month.total.costUsd??0)} of AI work for ${c.project}.`));else{let b=c.budget,spent=value!==void 0?usd3(value):"not reported",of=b?.ticketUsd?`, ${pct(b.ticketShare??0)} of its ${usd3(b.ticketUsd)} budget`:"";lines3.push(` ${kleur7.bold("Cost")} ${spent} on your API key${of}`);let month=b?.monthUsd?`${usd3(b.monthSpent)} of your ${usd3(b.monthUsd)} budget (${pct(b.monthShare??0)})`:usd3(b?.monthSpent??c.month.total.costUsd??0);lines3.push(kleur7.gray(` This month for ${c.project}: ${month}`));for(let k of b?.over??[])lines3.push(` ${amber2(k==="ticket"?"Over its budget: the ticket waits for you before its next step.":"Over this month's budget: tickets wait for you before their next step.")}`);for(let k of b?.warn??[])lines3.push(` ${amber2(k==="ticket"?"At 80% of its budget.":"At 80% of this month's budget.")}`)}return c.size&&lines3.push(` ${kleur7.bold("Size")} ${c.size.size[0].toUpperCase()}${c.size.size.slice(1)}: ${c.size.words.charAt(0).toLowerCase()}${c.size.words.slice(1)}${c.why?` ${c.why}`:""}`),c.where.parts.length&&lines3.push(` ${kleur7.bold("Where the work went")} ${c.where.parts.map(p=>`${p.name} ${p.share<.01?"under 1%":pct(p.share)}`).join(" \xB7 ")}`),c.tip&&lines3.push(kleur7.gray(` Tip: ${c.tip}`)),c.details.models&&lines3.push(kleur7.gray(` For engineers: ${c.details.models}${c.details.tokens?`. ${c.details.tokens}`:""}`)),lines3}async function happenedLines(bp,t){let files=await changedFiles3(bp,t.id).catch(()=>null),h=whatHappened(bp,t,{files:()=>files});if(!h.steps.length)return[];let lines3=[` ${kleur7.bold("What happened")}${h.seconds?kleur7.gray(` ${h.steps.length} steps \xB7 ${durationWords(h.seconds)} in all`):""}`];for(let s of h.steps){let mark=s.running?kleur7.green("\u25CF"):kleur7.green("\u2713"),when2=s.running?"now ":s.to?clock2(s.to):" ",notes=[s.seconds!==void 0?durationWords(s.seconds):"",...s.notes].filter(Boolean).join(" \xB7 ");lines3.push(` ${mark} ${kleur7.gray(when2)} ${s.name}${notes?kleur7.gray(` ${notes}`):""}`),lines3.push(` ${s.summary}`)}return lines3}async function runCost(ticketId){let bp=board3(),pay=await howPaid(bp);if(ticketId){let t=readTicket(bp,ticketId);if(!t)throw new Error(`No ticket ${ticketId.toUpperCase()} on this board.`);out2();for(let l of costLines(ticketCost(bp,t,pay)))out2(l);out2();return}let o=costOverview(bp,pay);out2(),out2(` ${kleur7.bold("You pay with")} ${paidWords(pay)}`);let month=o.month.total.costUsd??0;if(out2(pay.payment==="subscription"?` ${kleur7.bold("This month")} ${usd3(month)} of AI work for ${o.project} at API prices, covered by your plan. A value, not a bill.`:` ${kleur7.bold("This month")} ${usd3(month)}${o.budget.monthUsd?` of your ${usd3(o.budget.monthUsd)} budget`:""} for ${o.project}`),pay.payment==="api-key"){out2(` ${kleur7.bold("Budgets")} ${o.budget.ticketUsd?`${usd3(o.budget.ticketUsd)} per ticket`:"none per ticket"} \xB7 ${o.budget.monthUsd?`${usd3(o.budget.monthUsd)} per month`:"none per month"}`);for(let w of o.warnings){let who4=w.kind==="ticket"?`${w.id} ${w.title}`:"This month";out2(` ${amber2(w.over?"\u25CF":"\u25CB")} ${who4}: ${usd3(w.spent)} of ${usd3(w.limit)} (${pct(w.share)})${w.over?amber2(". Waits for you before its next step"):""}`)}}out2(),out2(` ${kleur7.bold("Per Quality setting")} ${kleur7.gray(`average per accepted ticket on ${o.project}, once a setting has ${o.qualityAfter}`)}`);for(let q5 of o.quality){let avg=q5.tickets>=o.qualityAfter?[q5.costUsd!==void 0?`${usd3(q5.costUsd)} ${pay.payment==="subscription"?"value":"each"}`:"",q5.seconds!==void 0?`${durationWords(q5.seconds)} of squad time`:""].filter(Boolean).join(", "):`${q5.tickets} of ${o.qualityAfter} tickets so far`;out2(` ${q5.quality.padEnd(9)} ${avg} ${kleur7.gray(QUALITY_NOTE[q5.quality])}`)}out2(),info(`Change how you pay: ${cmd2("squad config set payment subscription")} (or api-key, auto). Budgets: ${cmd2("squad config set budget --ticket 5 --month 50")}`)}async function runConfigPayment(mode){let bp=board3();if(mode===void 0){let pay2=await howPaid(bp);out2(),out2(` You pay with ${paidWords(pay2)}.`),out2(),info(`Change it: ${cmd2("squad config set payment subscription")} (or api-key, or auto to let squad-kit find out)`);return}let m=mode.trim().toLowerCase().replace(/^api$|^key$|^apikey$/,"api-key").replace(/^plan$/,"subscription");if(m!=="subscription"&&m!=="api-key"&&m!=="auto")throw new Error(`"${mode}" is not a way to pay. Use subscription, api-key or auto.`);setPaymentChoice(m);let pay=await howPaid(bp);success(m==="auto"?`squad-kit finds out how you pay: ${paidWords(pay)}.`:m==="subscription"?"Subscription: the Cost tab shows the value of the work, with no extra charge.":`API key: the Cost tab shows real dollars. Set budgets: ${cmd2("squad config set budget --ticket 5 --month 50")}`)}async function runConfigBudget(opts){let bp=board3();if(opts.ticket===void 0&&opts.month===void 0){let b2=readMeta(bp).budget??{};out2(),out2(` Per ticket: ${b2.ticketUsd?usd3(b2.ticketUsd):"none"} Per month: ${b2.monthUsd?usd3(b2.monthUsd):"none"}`),out2(kleur7.gray(" Budgets count when you pay with an API key. At 80% the ticket and the board say so; at 100% the ticket waits for you between two steps.")),out2(),info(`Change them: ${cmd2("squad config set budget --ticket 5 --month 50")} (off takes one away)`);return}let change={};opts.ticket!==void 0&&(change.ticketUsd=parseUsd(opts.ticket)),opts.month!==void 0&&(change.monthUsd=parseUsd(opts.month));let b=setBudget(bp,change),pay=await howPaid(bp);success(`Budgets: ${b.ticketUsd?`${usd3(b.ticketUsd)} per ticket`:"none per ticket"}, ${b.monthUsd?`${usd3(b.monthUsd)} per month`:"none per month"}.`),pay.payment!=="api-key"&&info(`They count when you pay with an API key. You pay with ${paidWords(pay)}.`)}var out3=(s="")=>process.stdout.write(`${s}
2026
2026
  `),cmd3=s=>kleur8.green(s),id=s=>kleur8.bold(s),amber3=s=>kleur8.yellow(s),sleep9=ms2=>new Promise(r=>setTimeout(r,ms2)),STAGE_NAME={tickets:"Tickets",specs:"Specs",implement:"Implement",test:"Test",review:"Review",release:"Release",bugs:"Bugs",done:"Released"},TRACK2=["tickets","specs","implement","test","review","release"];function board3(){let{bp,moved}=openBoard(buildPaths(requireSquadRoot()).squadDir);if(moved&&success(`Moved this board to the new layout first (${moved.tickets} tickets, all checked). A copy of the old files: ${moved.backup}`),!boardExists(bp))throw new Error(settingsProblem(bp)??"This project has no board yet. Run `squad demo` to try squad-kit v1 with a sample board.");return bp}function stepSquad(bp){squadRunsElsewhere(bp)||readMeta(bp).engine==="demo"&&tick(bp)}function warnIfNoSquad(bp){readMeta(bp).engine==="live"&&!squadRunsElsewhere(bp)&&warning("The squad works while the board is open. Run `squad console` in another tab.")}function nextCommand(t){if(!t.waiting&&t.quiet)return`${t.quiet.pid?`squad unstick ${t.id} or `:""}squad wait ${t.id} --more 10m or squad stop ${t.id} --again`;if(!t.waiting&&changeNeedsYou(t))return`squad show ${t.id} then squad propose ${t.id} --approve or --decline`;if(!t.waiting&&t.testChange?.ask)return`squad test-command ${t.id} --use or --keep`;switch(t.waiting?.kind){case"depends":return`squad build ${t.id} --on-branch or --anyway or --wait`;case"change":return`squad show ${t.id} then squad approve ${t.id} or squad propose ${t.id} --decline`;case"questions":return`squad answer ${t.id}`;case"spec":case"plan":return`squad show ${t.id} then squad approve ${t.id}`;case"proof":return`squad show ${t.id} then squad approve ${t.id}`;case"stuck":return`squad decide ${t.id}`;case"yours":return t.takenOver?.free?`squad outside back ${t.id}`:`cd ${shellQuote(t.takenOver?.cwd??"")} && claude --resume ${t.takenOver?.session} then squad give-back ${t.id}`;default:return`squad watch ${t.id}`}}function trackLine(t){if(t.stage==="bugs")return amber3("\u25CF Bugs")+kleur8.gray(" then the same 6 steps as any ticket");let at3=TRACK2.indexOf(t.stage);return TRACK2.map((s,i)=>i<at3||t.stage==="done"?kleur8.green(`\u2713 ${STAGE_NAME[s]}`):i===at3?(t.waiting?amber3:kleur8.green)(`\u25CF ${STAGE_NAME[s]}`)+(t.waiting?amber3(" your turn"):kleur8.gray(" working")):kleur8.gray(`\u25CB ${STAGE_NAME[s]}`)).join(kleur8.gray(" \u2500 "))}function brokenLines(bp){for(let b of[...brokenTickets(bp),...brokenDecisions(bp),...brokenReleases(bp),...brokenBreakdowns(bp)])out3(),out3(` ${amber3(b.problem)}`),out3(kleur8.gray(` For an engineer: ${b.file}: ${b.detail}`))}async function runInbox(){if(!findSquadRoot())return inboxEverywhere();let bp=board3(),items3=inbox(bp),brds=breakdownWaits(bp);if(brokenLines(bp),out3(),out3(` ${kleur8.bold("Waiting for you")} ${amber3(`(${items3.length+brds.length})`)}`),out3(),items3.length+brds.length===0){out3(kleur8.gray(" Nothing needs you right now. The squad is working.")),out3();return}let w=Math.max(...items3.map(t=>t.title.length),...brds.map(b=>b.title.length));for(let b of brds)out3(` ${id(b.id)} ${b.title.padEnd(w)} ${amber3(`The Analyst has ${b.questions} ${b.questions===1?"question":"questions"}`)}`),out3(` ${" ".repeat(b.id.length)} ${cmd3(`squad requirements answer ${b.id}`)}`);for(let t of items3)out3(` ${id(t.id)} ${t.title.padEnd(w)} ${amber3(inboxLine(t))}`),out3(` ${" ".repeat(t.id.length)} ${cmd3(nextCommand(t))}`);out3()}function inboxLine(t){if(t.waiting?.summary)return t.waiting.summary;let change=openChange(t);if(changeNeedsYou(t)&&change)return`Change ${change.id} to the spec: ${change.summary??change.ask}`;if(t.testChange?.ask)return testChangeLine(t.testChange);let o=t.outdated??[];return o.length?o.length===1?`${o[0].adr} came after this plan and changes it`:`${o.map(x=>x.adr).join(" and ")} came after this plan and change it`:`Looks stuck: ${t.quiet?.detail??"no progress"}`}function inboxEverywhere(){let groups=readProjects().map(p=>({root:p.root,bp:boardPaths(path158.join(p.root,".squad"))})).filter(p=>fs167.existsSync(p.bp.configFile)&&boardExists(p.bp)).map(p=>({name:readMeta(p.bp).project||projectName(p.root),items:inbox(p.bp)}));if(!groups.length)throw new Error("No projects in squad-kit yet. Open a terminal in a project folder (or a new, empty one) and run: squad console");let total=groups.reduce((n,g)=>n+g.items.length,0);out3(),out3(` ${kleur8.bold("Waiting for you")} ${amber3(`(${total})`)} ${kleur8.gray(`in ${groups.length} ${groups.length===1?"project":"projects"}`)}`),total===0&&(out3(),out3(kleur8.gray(" Nothing needs you right now. The squads are working.")));for(let g of groups.filter(x=>x.items.length)){let ref2=shellArg(g.name),w=Math.max(...g.items.map(t=>t.title.length));out3(),out3(` ${kleur8.bold(g.name)}`);for(let t of g.items)out3(` ${id(t.id)} ${t.title.padEnd(w)} ${amber3(inboxLine(t))}`),out3(` ${" ".repeat(t.id.length)} ${cmd3(nextCommand(t).replace(/\bsquad ([a-z-]+) (\S+)/g,(_m,verb,arg)=>`squad ${verb} ${arg} --project ${ref2}`))}`)}out3()}async function runBoard(opts={}){let bp=board3(),meta=readMeta(bp),open3=listTickets(bp).filter(t=>t.stage!=="done"),tickets=open3.filter(t=>opts.archived||!t.archived);out3();let used=boardUsage(bp).total;out3(` ${kleur8.bold(meta.project)} ${kleur8.gray(`${tickets.length} open tickets`)}${used.runs?kleur8.gray(` \xB7 used ${costWords(used)??"an unknown amount"} at API prices`):""}`);for(let stage of["tickets","specs","implement","test","review","release","bugs"]){let here=tickets.filter(t=>t.stage===stage);out3(),out3(` ${kleur8.bold(STAGE_NAME[stage])} ${kleur8.gray(String(here.length))}`);for(let t of here){let mark=t.waiting?amber3("\u25CF you"):t.live?kleur8.green("\u25CF ")+kleur8.gray(" "):kleur8.gray("\u25CB ");out3(` ${mark} ${id(t.id)} ${t.title} ${kleur8.gray(t.status)}${isMine(t,meta)?"":kleur8.gray(` (${ownerOf(t)})`)}${t.archived?kleur8.gray(" (archived)"):""}`)}}let hidden=open3.length-tickets.length;hidden&&(out3(),out3(kleur8.gray(` ${hidden} archived ${hidden===1?"ticket is":"tickets are"} not shown. Show them: squad board --archived`))),brokenLines(bp),out3()}async function runShow(ticketId){let bp=board3(),t=requireTicket(bp,ticketId);out3(),out3(` ${id(t.id)} ${kleur8.bold(t.title)}${t.priority==="high"?kleur8.red(" High priority"):""}${t.noNewTests?kleur8.gray(` ${NO_NEW_TESTS_LABEL}`):""}`),out3(` ${trackLine(t)}`);let usual=readUsual(bp);for(let l of nowLines(t,liveNow(t,usual)))out3(l);out3(` ${kleur8.gray("Owner:")} ${ownerOf(t)}${isMine(t,readMeta(bp))?kleur8.gray(" (you)"):kleur8.gray(". Their squad works on it; you see their work when they push")}`),t.waiting&&(out3(),out3(` ${amber3("Waiting for you:")} ${t.waiting.summary}`)),t.dependsOn?.length&&out3(` ${kleur8.gray("Depends on:")} ${t.dependsOn.join(", ")}${t.dependency?kleur8.gray(` (${t.dependency.way==="branch"?"built on its branch":t.dependency.way==="anyway"?"built without it":"waits for it"})`):""} ${cmd3(`squad depends ${t.id}`)}`),t.deleting&&out3(` ${amber3(t.deleting.error?`Delete did not finish: ${t.deleting.error}.`:"Being deleted.")} ${cmd3(`squad delete ${t.id}`)}`),t.archived&&out3(` ${kleur8.gray(`Archived by ${t.archived.by}. Bring it back:`)} ${cmd3(`squad archive ${t.id} --undo`)}`);let change=openChange(t);if(change){out3(),out3(` ${amber3(`Change ${change.id}${change.size?` (${change.size})`:""}:`)} ${change.summary??change.ask} ${kleur8.gray(change.status)}`);for(let p of change.parts??[]){let name=p.part==="story"?"Story":p.index===void 0?"New done when":`Done when ${p.index+1}`;p.before&&out3(` ${kleur8.gray(`${name}, was:`)} ${p.before}`),out3(` ${kleur8.gray(`${name}, ${p.before?"now":"new"}:`)} ${p.after||kleur8.gray("(goes)")}`)}for(let[i,s]of(change.design?.steps??[]).entries())out3(` ${kleur8.gray(`Step ${i+1}, ${s.change}:`)} ${s.title}`)}if(t.story&&(out3(),out3(` ${kleur8.bold("The story")}`),out3(` ${t.story}`)),t.criteria?.length&&(out3(),out3(` ${kleur8.bold("Done when")}`),t.criteria.forEach((c,i)=>out3(` ${i+1}. ${kleur8.green("Given")} ${c.given}, ${kleur8.green("when")} ${c.when}, ${kleur8.green("then")} ${c.then}.`))),t.decision){out3(),out3(` ${kleur8.bold("The Architect's decision")}${t.decision.adr?kleur8.gray(` saved as ${t.decision.adr}`):""}`),out3(` ${t.decision.title}`);for(let o of t.decision.options)out3(` ${o.chosen?kleur8.green("\u2713"):kleur8.gray("\xB7")} ${o.name} ${kleur8.gray(o.note)}`);out3(` ${kleur8.gray("Why:")} ${t.decision.why}`),t.decision.clientCheck&&out3(` ${kleur8.gray("Client's choice:")} ${t.decision.clientCheck}`)}if(t.steps?.length&&t.waiting?.kind!=="plan"){out3(),out3(` ${kleur8.bold("Steps")}`);for(let s of t.steps){let mark=s.status==="done"?kleur8.green("\u2713"):s.status==="doing"?kleur8.green("\u25CF"):kleur8.gray("\u25CB");out3(` ${mark} ${s.status==="todo"?kleur8.gray(s.title):s.title}${s.live?kleur8.gray(` ${s.live}`):""}`)}}else t.work&&t.waiting?.kind==="plan"&&(out3(),out3(` ${kleur8.bold("The work")} ${t.work.steps} small steps, ${t.work.tests} new tests, ${t.work.screens} before and after screens`));if(t.gates){let g=(ok2,label3)=>ok2==="pass"?kleur8.green(`\u2713 ${label3}`):ok2==="unchecked"?kleur8.yellow(`! Not checked: ${label3}`):kleur8.gray(`\u25CB ${label3}`);out3(),t.outside&&!t.outside.checked&&[t.gates.checks,t.gates.review,t.gates.proof].every(s=>s==="pending")?out3(` ${kleur8.bold("Gates")} ${kleur8.gray("\u25CB Not checked by squad-kit")}`):out3(` ${kleur8.bold("Gates")} ${g(t.gates.checks,t.gates.checksText??"All checks pass")} ${g(t.gates.review,"Review approved")} ${g(t.gates.proof,"Proof made")}`)}if((t.waiting?.kind==="proof"||t.stuck?.gate==="proof")&&t.proof?.length){out3(),out3(` ${kleur8.bold("Proof")} ${kleur8.gray("before and after screens, open them on the board or here:")}`);for(let p of t.proof){let open3=p.shows==="broken"||p.shows==="not-shown"||p.shows==="off-screen",paint=p.shows==="broken"?kleur8.red:p.shows==="off-screen"?kleur8.gray:kleur8.yellow;out3(` ${p.shows==="broken"?kleur8.red("\u2717"):p.shows==="not-shown"?kleur8.yellow("?"):p.shows==="off-screen"?kleur8.gray("-"):kleur8.green("\u2713")} ${p.criterion}${p.shows?` ${open3?paint(p.shows==="broken"?"Not working yet":p.shows==="off-screen"?"Not on a screen":"Not shown yet"):kleur8.green("Met")}`:""}`),open3&&p.why&&out3(` ${p.why}`),out3(kleur8.gray(` ${path158.join(bp.proofDir,p.before)} \u2192 ${path158.join(bp.proofDir,p.after)}`));for(let m of p.more??[])out3(kleur8.gray(` ${path158.join(bp.proofDir,m.after)}${m.note?` ${m.note}`:""}`))}for(let o of t.offScreen??[])out3(kleur8.gray(` - ${o.criterion}: not on a screen. ${o.why}`))}if(t.alsoChanged?.length&&(out3(),out3(` ${amber3(alsoChangedLine(t.alsoChanged))}`)),t.testChange&&(out3(),out3(` ${amber3(`${testChangeLine(t.testChange)}.`)} ${kleur8.gray(t.testChange.ask?`squad-kit cannot tell (${t.testChange.ran??"it tried both"}), so the checks keep the current one until you choose.`:"squad-kit tries it at the next checks and keeps what works. You can choose now.")}`),out3(` ${cmd3(`squad test-command ${t.id} --use`)} or ${cmd3(`squad test-command ${t.id} --keep`)}`)),t.stuck&&t.waiting?.kind==="stuck"){let problem2=problemOf(t);out3(),out3(` ${kleur8.bold(problem2.title)}`),out3(` ${problem2.what}`);for(let line3 of t.stuck.points??[])out3(` - ${line3}`);for(let line3 of problem2.safe)out3(` ${kleur8.green("\u2713")} ${line3}`);t.stuck.options.forEach((o,i)=>out3(` ${i+1}. ${o.name} ${kleur8.gray(o.note)}`)),problem2.detail&&out3(kleur8.gray(` The exact error: ${problem2.detail.split(`
2027
- `)[0].slice(0,300)}`)),out3(kleur8.gray(` Need help? ${cmd3(`squad report ${t.id}`)} saves one file to send.`))}let happened=await happenedLines(bp,t);if(happened.length){out3();for(let l of happened)out3(l)}if(t.usage?.length){out3();for(let l of costLines(ticketCost(bp,t,await howPaid(bp))))out3(l);out3(kleur8.gray(` Each role: ${usedWords(totalOf(t.usage))}`));for(let row3 of usageRows(t.usage))out3(row3)}out3(),out3(` ${kleur8.bold("Latest")}`);for(let a of t.activity.slice(0,4))out3(` ${kleur8.gray(time(a.at))} ${a.text}`);out3(),out3(` ${kleur8.gray("Next:")} ${cmd3(nextCommand(t))}`),out3()}function time(iso2){return new Date(iso2).toTimeString().slice(0,5)}function usedWords(t){let cost=costWords(t);return`${cost?`${cost} at API prices`:"cost not reported"}${kleur8.gray(`, ${t.runs} ${t.runs===1?"run":"runs"}`)}`}function usageRows(lines3){return byRole(lines3).flatMap(r=>r.levels.map((l,i)=>{let name=i?"":AGENTS.find(a=>a.role===r.role)?.name??r.role,model=l.model?`${l.model} (${l.level})`:`${l.level} level`,what=[l.steps.map(s=>stepWords(r.role,s)).join(", "),`${l.total.runs} ${l.total.runs===1?"run":"runs"}`,tokenWords(l.total)].filter(Boolean).join(" \xB7 ");return` ${name.padEnd(10)} ${model.padEnd(20)} ${(costWords(l.total)??"not reported").padEnd(12)} ${kleur8.gray(what)}`}))}async function runAnswer(ticketId,opts){let bp=board3();if(opts.skip){let t2=skipQuestions(bp,ticketId);success(`The squad made its own guesses for ${t2.id}. You'll see them in the plan.`);return}let t=requireTicket(bp,ticketId);if(t.waiting?.kind!=="questions"||!t.questions)throw new Error(`${t.id} has no open questions.`);out3(),out3(` ${id(t.id)} ${kleur8.bold(t.title)} ${amber3(`${t.questions.length} questions`)}`);let answers2={};for(let q5 of t.questions)for(out3(),out3(kleur8.gray(` From the ${q5.from==="analyst"?"Analyst, what to build":"Architect, how to build"}`));;){let choice=await select8({message:q5.text,choices:[...q5.options.map(o=>({name:o===q5.inputOption?`${o} (type it)`:o,value:o})),{name:"Other (type your own)",value:"__other"},new Separator,{name:"Ask about this",value:"__ask"}]});if(choice==="__ask"){let text3=await input5({message:"Your question:"});if(!text3.trim())continue;askAbout(bp,t.id,q5.id,text3);let reply2=await waitForReply(bp,t.id,q5.id);out3(` ${kleur8.green(q5.from==="analyst"?"Analyst:":"Architect:")} ${reply2}`);continue}choice==="__other"?answers2[q5.id]={choice:"Other",value:await input5({message:"Your answer:"})}:choice===q5.inputOption?answers2[q5.id]={choice,value:await input5({message:choice.replace(/:$/,"")+", which one?"})}:answers2[q5.id]={choice};break}answerQuestions(bp,t.id,answers2),out3(),success("Answers sent. The Architect is choosing the design."),info(`Follow it live: squad watch ${t.id}`)}async function waitForReply(bp,ticketId,qid){warnIfNoSquad(bp);let spin=spinner("Thinking\u2026");for(let i=0;i<400;i+=1){stepSquad(bp);let q5=readTicket(bp,ticketId)?.questions?.find(x=>x.id===qid);if(q5&&!q5.thinking)return spin.stop(),q5.thread?.[q5.thread.length-1]?.text??"";await sleep9(400)}return spin.stop(),"The squad has not answered yet. Check the board in a moment."}async function endingSaves(bp,run7){let end=()=>endSaves(bp);process.once("SIGINT",end),process.once("SIGTERM",end);try{return await run7()}finally{process.off("SIGINT",end),process.off("SIGTERM",end)}}async function runApprove(ticketId){let bp=board3(),was=requireTicket(bp,ticketId).waiting?.kind,t=await endingSaves(bp,()=>approveBeside(bp,ticketId));if(was==="spec")success("Spec approved. The Architect is designing how to build it; you approve that next."),info(`Follow it live: squad watch ${t.id}`);else if(t.stage==="implement")success(`Plan approved${t.decision?.adr?`. Decision saved as ${t.decision.adr}`:""}. The Builder started.`),info(`Follow it live: squad watch ${t.id}`);else if(t.waiting?.kind==="stuck")warning(`${t.id} is saved, but it clashes with newer changes. Run: squad decide ${t.id}`);else{success(`Proof accepted. ${t.id} is ready for ${t.release}.`);let repo=repoFor(readMeta(bp),t),app=t.mergedAt?projectApps(readMeta(bp)).find(a=>a.repo===repo.path&&a.start):void 0;app&&info(`${t.id} is in ${repo.base} \xB7 Run the new version: squad app restart${readMeta(bp).repos?.length?` ${app.name}`:""}`),info("Release it when you are ready: squad release")}}async function runChange(ticketId,words2){let bp=board3(),was=requireTicket(bp,ticketId).waiting?.kind,text3=words2.join(" ")||await input5({message:"What should change?"}),t=requestChange(bp,ticketId,text3);success(was==="spec"?"Sent to the Analyst to update the spec.":t.stage==="implement"?"Sent back to the Builder with your note.":t.specApproved?"Sent to the Architect to update the design.":"Sent to the Spec writer to update the plan.")}async function runPropose(ticketId,words2,opts){let bp=board3();if(warnIfNoSquad(bp),opts.approve){let t=approveChange(bp,ticketId);success(t.activity[0]?.text??"Approved.");return}if(opts.decline){declineChange(bp,ticketId,words2.join(" ")),success("Declined. The spec stays as it was.");return}if(opts.withdraw){withdrawChange(bp,ticketId,words2.join(" ")),success("Withdrawn. The spec is back to how it was, and the work carries on without it.");return}if(opts.retry){retryChange(bp,ticketId),success("The Analyst writes the change again.");return}let text3=words2.join(" ")||await input5({message:"What should change?"});if(opts.as){if(opts.as!=="ticket"&&opts.as!=="bug")throw new Error("--as is one of: ticket, bug.");let made=changeAfterRelease(bp,ticketId,text3,opts.as);success(`Made ${opts.as==="bug"?"bug":"ticket"} ${made.id}, linked to ${ticketId.toUpperCase()}. Follow it: squad watch ${made.id}`);return}try{proposeChange(bp,ticketId,text3)}catch(err2){if(!(err2 instanceof ReleasedError))throw err2;warning(err2.message),info(`As a new linked ticket: squad propose ${ticketId} --as ticket ${shellQuote(text3)}`),info(`As a bug report: squad propose ${ticketId} --as bug ${shellQuote(text3)}`);return}success("Proposed. The Analyst writes it into the spec while the work goes on."),info(`Then approve it: squad propose ${ticketId} --approve, or decline it: squad propose ${ticketId} --decline`)}function parsePart(words2){let w=words2.trim().toLowerCase();if(w==="story")return{part:"story"};if(w==="decision")return{part:"decision"};let m=/^(step|done when|criterion)\s+(\d+)$/.exec(w);return m?{part:m[1]==="step"?"step":"criterion",index:Number(m[2])-1}:null}async function runSendBack(ticketId,comments){let parsed=comments.map(c=>{let i=c.indexOf(":"),part=i>0?parsePart(c.slice(0,i)):null;if(!part)throw new Error(`Start each comment with the part, like "step 2: use the cache" or "story: \u2026". Not: ${c}`);return{...part,text:c.slice(i+1).trim()}}),t=sendBack(board3(),ticketId,parsed);success(`Sent back with ${parsed.length} ${parsed.length===1?"comment":"comments"}. ${t.status}.`)}async function runEditPlan(ticketId,part,words2){let p=parsePart(part);if(!p||p.part==="decision")throw new Error('Edit "story", "step N" or "done when N".');let text3=words2.join(" ");if(p.part==="criterion"){let m=/^given (.+?),? when (.+?),? then (.+?)\.?$/i.exec(text3.trim());if(!m)throw new Error("Write it as: Given \u2026, when \u2026, then \u2026");editPlan(board3(),ticketId,{...p,criterion:{given:m[1],when:m[2],then:m[3]}})}else editPlan(board3(),ticketId,{...p,text:text3});success("Edited. Approve keeps your wording, and so does a rewrite.")}async function runStartOver(ticketId,words2){let text3=words2.join(" ")||await input5({message:"What is the new direction?"}),t=startOver(board3(),ticketId,text3);success(`Starting over. ${t.status}.`)}async function chatAnswer(bp,id2){let spin=spinner("Thinking\u2026"),started2=Date.now();for(;;){stepSquad(bp);let c=requireTicket(bp,id2).chat;if(!c?.thinking){spin.stop();let last=c?.messages.at(-1);return last&&last.who!=="you"&&(out3(),out3(` ${kleur8.bold(last.who==="squad"?"squad-kit":`${last.who[0].toUpperCase()}${last.who.slice(1)}`)}`),out3(last.text.replace(/^/gm," ")),out3()),c?.ready&&info(c.with==="analyst"?"It thinks it understands. Write the spec: /write":c.with==="architect"?"It thinks it understands. Write the design: /write":"It thinks it understands. Carry on: /done"),!0}if(Date.now()-started2>10*6e4)return spin.stop(),warning("No answer yet. Check the board in a moment."),!1;await sleep9(500)}}async function runChat(ticketId,words2,opts){let bp=board3();warnIfNoSquad(bp);let t=requireTicket(bp,ticketId);if(opts.done){chatDone(bp,t.id),success("Done talking. The squad carries on.");return}if(opts.stop){chatStop(bp,t.id),success("Stopping the answer. The talk stays open.");return}if(opts.retry){chatRetry(bp,t.id),await chatAnswer(bp,t.id);return}if(opts.write){t=chatWrite(bp,t.id),success(`${t.status}. You check it as usual: squad show ${t.id}`);return}if(!t.chat?.open){let agent=opts.with?.toLowerCase();t=opts.ask?askQuestion(bp,t.id,agent):startChat(bp,t.id,agent??"analyst"),opts.ask&&info(`Questions only: ${whereItStands(t)}. Asking changes nothing.`),await chatAnswer(bp,t.id)}let text3=words2.join(" ");if(text3){sendChat(bp,t.id,text3),await chatAnswer(bp,t.id);return}if(process.stdin.isTTY)for(;;){let line3=(await input5({message:"You (/write, /done, Enter to leave the chat open)"})).trim();if(!line3){info(`The chat stays open on the board. Continue: squad chat ${t.id}`);return}if(line3==="/done")return runChat(t.id,[],{done:!0});if(line3==="/write")return runChat(t.id,[],{write:!0});sendChat(bp,t.id,line3),await chatAnswer(bp,t.id)}}async function runAgent(role,opts){let bp=board3(),r=role.toLowerCase();if(!AGENTS.some(a2=>a2.role===r))throw new Error(`There is no ${role} agent. Agents: ${AGENTS.map(a2=>a2.role).join(", ")}.`);let steps=STEPS[r];if(opts.step&&!steps.some(s=>s.id===opts.step))throw new Error(`The ${r} has no step "${opts.step}". Its steps: ${steps.map(s=>s.id).join(", ")}.`);if(opts.set!==void 0){writeAgent(bp,r,opts.step?{steps:{[opts.step]:opts.set}}:{instructions:opts.set}),success(`Saved. The ${r} reads it ${opts.step?`in the ${opts.step} step`:"in every step"}, on top of its core prompt.`);return}if(opts.prompt){let core=await corePrompts(bp,r,opts.ticket);out3(kleur8.gray(` Core prompts, read only, filled in from ${core.ticket.id} "${core.ticket.title}"`));for(let p of core.prompts.filter(x=>!opts.step||x.step===opts.step))out3(),out3(kleur8.bold(` \u2500\u2500 ${p.name} (${p.step}) \u2500\u2500`)),out3(p.prompt),p.team&&out3(`
2027
+ `)[0].slice(0,300)}`)),out3(kleur8.gray(` Need help? ${cmd3(`squad report ${t.id}`)} saves one file to send.`))}let happened=await happenedLines(bp,t);if(happened.length){out3();for(let l of happened)out3(l)}if(t.usage?.length){out3();for(let l of costLines(ticketCost(bp,t,await howPaid(bp))))out3(l);out3(kleur8.gray(` Each role: ${usedWords(totalOf(t.usage))}`));for(let row3 of usageRows(t.usage))out3(row3)}out3(),out3(` ${kleur8.bold("Latest")}`);for(let a of t.activity.slice(0,4))out3(` ${kleur8.gray(time(a.at))} ${a.text}`);out3(),out3(` ${kleur8.gray("Next:")} ${cmd3(nextCommand(t))}`),out3()}function time(iso2){return new Date(iso2).toTimeString().slice(0,5)}function usedWords(t){let cost=costWords(t);return`${cost?`${cost} at API prices`:"cost not reported"}${kleur8.gray(`, ${t.runs} ${t.runs===1?"run":"runs"}`)}`}function usageRows(lines3){return byRole(lines3).flatMap(r=>r.levels.map((l,i)=>{let name=i?"":AGENTS.find(a=>a.role===r.role)?.name??r.role,model=l.model?`${l.model} (${l.level})`:`${l.level} level`,what=[l.steps.map(s=>stepWords2(r.role,s)).join(", "),`${l.total.runs} ${l.total.runs===1?"run":"runs"}`,tokenWords(l.total)].filter(Boolean).join(" \xB7 ");return` ${name.padEnd(10)} ${model.padEnd(20)} ${(costWords(l.total)??"not reported").padEnd(12)} ${kleur8.gray(what)}`}))}async function runAnswer(ticketId,opts){let bp=board3();if(opts.skip){let t2=skipQuestions(bp,ticketId);success(`The squad made its own guesses for ${t2.id}. You'll see them in the plan.`);return}let t=requireTicket(bp,ticketId);if(t.waiting?.kind!=="questions"||!t.questions)throw new Error(`${t.id} has no open questions.`);out3(),out3(` ${id(t.id)} ${kleur8.bold(t.title)} ${amber3(`${t.questions.length} questions`)}`);let answers2={};for(let q5 of t.questions)for(out3(),out3(kleur8.gray(` From the ${q5.from==="analyst"?"Analyst, what to build":"Architect, how to build"}`));;){let choice=await select8({message:q5.text,choices:[...q5.options.map(o=>({name:o===q5.inputOption?`${o} (type it)`:o,value:o})),{name:"Other (type your own)",value:"__other"},new Separator,{name:"Ask about this",value:"__ask"}]});if(choice==="__ask"){let text3=await input5({message:"Your question:"});if(!text3.trim())continue;askAbout(bp,t.id,q5.id,text3);let reply2=await waitForReply(bp,t.id,q5.id);out3(` ${kleur8.green(q5.from==="analyst"?"Analyst:":"Architect:")} ${reply2}`);continue}choice==="__other"?answers2[q5.id]={choice:"Other",value:await input5({message:"Your answer:"})}:choice===q5.inputOption?answers2[q5.id]={choice,value:await input5({message:choice.replace(/:$/,"")+", which one?"})}:answers2[q5.id]={choice};break}answerQuestions(bp,t.id,answers2),out3(),success("Answers sent. The Architect is choosing the design."),info(`Follow it live: squad watch ${t.id}`)}async function waitForReply(bp,ticketId,qid){warnIfNoSquad(bp);let spin=spinner("Thinking\u2026");for(let i=0;i<400;i+=1){stepSquad(bp);let q5=readTicket(bp,ticketId)?.questions?.find(x=>x.id===qid);if(q5&&!q5.thinking)return spin.stop(),q5.thread?.[q5.thread.length-1]?.text??"";await sleep9(400)}return spin.stop(),"The squad has not answered yet. Check the board in a moment."}async function endingSaves(bp,run7){let end=()=>endSaves(bp);process.once("SIGINT",end),process.once("SIGTERM",end);try{return await run7()}finally{process.off("SIGINT",end),process.off("SIGTERM",end)}}async function runApprove(ticketId){let bp=board3(),was=requireTicket(bp,ticketId).waiting?.kind,t=await endingSaves(bp,()=>approveBeside(bp,ticketId));if(was==="spec")success("Spec approved. The Architect is designing how to build it; you approve that next."),info(`Follow it live: squad watch ${t.id}`);else if(t.stage==="implement")success(`Plan approved${t.decision?.adr?`. Decision saved as ${t.decision.adr}`:""}. The Builder started.`),info(`Follow it live: squad watch ${t.id}`);else if(t.waiting?.kind==="stuck")warning(`${t.id} is saved, but it clashes with newer changes. Run: squad decide ${t.id}`);else{success(`Proof accepted. ${t.id} is ready for ${t.release}.`);let repo=repoFor(readMeta(bp),t),app=t.mergedAt?projectApps(readMeta(bp)).find(a=>a.repo===repo.path&&a.start):void 0;app&&info(`${t.id} is in ${repo.base} \xB7 Run the new version: squad app restart${readMeta(bp).repos?.length?` ${app.name}`:""}`),info("Release it when you are ready: squad release")}}async function runChange(ticketId,words2){let bp=board3(),was=requireTicket(bp,ticketId).waiting?.kind,text3=words2.join(" ")||await input5({message:"What should change?"}),t=requestChange(bp,ticketId,text3);success(was==="spec"?"Sent to the Analyst to update the spec.":t.stage==="implement"?"Sent back to the Builder with your note.":t.specApproved?"Sent to the Architect to update the design.":"Sent to the Spec writer to update the plan.")}async function runPropose(ticketId,words2,opts){let bp=board3();if(warnIfNoSquad(bp),opts.approve){let t=approveChange(bp,ticketId);success(t.activity[0]?.text??"Approved.");return}if(opts.decline){declineChange(bp,ticketId,words2.join(" ")),success("Declined. The spec stays as it was.");return}if(opts.withdraw){withdrawChange(bp,ticketId,words2.join(" ")),success("Withdrawn. The spec is back to how it was, and the work carries on without it.");return}if(opts.retry){retryChange(bp,ticketId),success("The Analyst writes the change again.");return}let text3=words2.join(" ")||await input5({message:"What should change?"});if(opts.as){if(opts.as!=="ticket"&&opts.as!=="bug")throw new Error("--as is one of: ticket, bug.");let made=changeAfterRelease(bp,ticketId,text3,opts.as);success(`Made ${opts.as==="bug"?"bug":"ticket"} ${made.id}, linked to ${ticketId.toUpperCase()}. Follow it: squad watch ${made.id}`);return}try{proposeChange(bp,ticketId,text3)}catch(err2){if(!(err2 instanceof ReleasedError))throw err2;warning(err2.message),info(`As a new linked ticket: squad propose ${ticketId} --as ticket ${shellQuote(text3)}`),info(`As a bug report: squad propose ${ticketId} --as bug ${shellQuote(text3)}`);return}success("Proposed. The Analyst writes it into the spec while the work goes on."),info(`Then approve it: squad propose ${ticketId} --approve, or decline it: squad propose ${ticketId} --decline`)}function parsePart(words2){let w=words2.trim().toLowerCase();if(w==="story")return{part:"story"};if(w==="decision")return{part:"decision"};let m=/^(step|done when|criterion)\s+(\d+)$/.exec(w);return m?{part:m[1]==="step"?"step":"criterion",index:Number(m[2])-1}:null}async function runSendBack(ticketId,comments){let parsed=comments.map(c=>{let i=c.indexOf(":"),part=i>0?parsePart(c.slice(0,i)):null;if(!part)throw new Error(`Start each comment with the part, like "step 2: use the cache" or "story: \u2026". Not: ${c}`);return{...part,text:c.slice(i+1).trim()}}),t=sendBack(board3(),ticketId,parsed);success(`Sent back with ${parsed.length} ${parsed.length===1?"comment":"comments"}. ${t.status}.`)}async function runEditPlan(ticketId,part,words2){let p=parsePart(part);if(!p||p.part==="decision")throw new Error('Edit "story", "step N" or "done when N".');let text3=words2.join(" ");if(p.part==="criterion"){let m=/^given (.+?),? when (.+?),? then (.+?)\.?$/i.exec(text3.trim());if(!m)throw new Error("Write it as: Given \u2026, when \u2026, then \u2026");editPlan(board3(),ticketId,{...p,criterion:{given:m[1],when:m[2],then:m[3]}})}else editPlan(board3(),ticketId,{...p,text:text3});success("Edited. Approve keeps your wording, and so does a rewrite.")}async function runStartOver(ticketId,words2){let text3=words2.join(" ")||await input5({message:"What is the new direction?"}),t=startOver(board3(),ticketId,text3);success(`Starting over. ${t.status}.`)}async function chatAnswer(bp,id2){let spin=spinner("Thinking\u2026"),started2=Date.now();for(;;){stepSquad(bp);let c=requireTicket(bp,id2).chat;if(!c?.thinking){spin.stop();let last=c?.messages.at(-1);return last&&last.who!=="you"&&(out3(),out3(` ${kleur8.bold(last.who==="squad"?"squad-kit":`${last.who[0].toUpperCase()}${last.who.slice(1)}`)}`),out3(last.text.replace(/^/gm," ")),out3()),c?.ready&&info(c.with==="analyst"?"It thinks it understands. Write the spec: /write":c.with==="architect"?"It thinks it understands. Write the design: /write":"It thinks it understands. Carry on: /done"),!0}if(Date.now()-started2>10*6e4)return spin.stop(),warning("No answer yet. Check the board in a moment."),!1;await sleep9(500)}}async function runChat(ticketId,words2,opts){let bp=board3();warnIfNoSquad(bp);let t=requireTicket(bp,ticketId);if(opts.done){chatDone(bp,t.id),success("Done talking. The squad carries on.");return}if(opts.stop){chatStop(bp,t.id),success("Stopping the answer. The talk stays open.");return}if(opts.retry){chatRetry(bp,t.id),await chatAnswer(bp,t.id);return}if(opts.write){t=chatWrite(bp,t.id),success(`${t.status}. You check it as usual: squad show ${t.id}`);return}if(!t.chat?.open){let agent=opts.with?.toLowerCase();t=opts.ask?askQuestion(bp,t.id,agent):startChat(bp,t.id,agent??"analyst"),opts.ask&&info(`Questions only: ${whereItStands(t)}. Asking changes nothing.`),await chatAnswer(bp,t.id)}let text3=words2.join(" ");if(text3){sendChat(bp,t.id,text3),await chatAnswer(bp,t.id);return}if(process.stdin.isTTY)for(;;){let line3=(await input5({message:"You (/write, /done, Enter to leave the chat open)"})).trim();if(!line3){info(`The chat stays open on the board. Continue: squad chat ${t.id}`);return}if(line3==="/done")return runChat(t.id,[],{done:!0});if(line3==="/write")return runChat(t.id,[],{write:!0});sendChat(bp,t.id,line3),await chatAnswer(bp,t.id)}}async function runAgent(role,opts){let bp=board3(),r=role.toLowerCase();if(!AGENTS.some(a2=>a2.role===r))throw new Error(`There is no ${role} agent. Agents: ${AGENTS.map(a2=>a2.role).join(", ")}.`);let steps=STEPS[r];if(opts.step&&!steps.some(s=>s.id===opts.step))throw new Error(`The ${r} has no step "${opts.step}". Its steps: ${steps.map(s=>s.id).join(", ")}.`);if(opts.set!==void 0){writeAgent(bp,r,opts.step?{steps:{[opts.step]:opts.set}}:{instructions:opts.set}),success(`Saved. The ${r} reads it ${opts.step?`in the ${opts.step} step`:"in every step"}, on top of its core prompt.`);return}if(opts.prompt){let core=await corePrompts(bp,r,opts.ticket);out3(kleur8.gray(` Core prompts, read only, filled in from ${core.ticket.id} "${core.ticket.title}"`));for(let p of core.prompts.filter(x=>!opts.step||x.step===opts.step))out3(),out3(kleur8.bold(` \u2500\u2500 ${p.name} (${p.step}) \u2500\u2500`)),out3(p.prompt),p.team&&out3(`
2028
2028
  ${kleur8.green(p.team)}`);return}let a=readAgent(bp,r),clean4=x=>withoutComments(x).trim();out3(),out3(` ${kleur8.bold(AGENTS.find(x=>x.role===r).name)} ${kleur8.gray(`${a.tool}${a.model?` \xB7 ${a.model}`:""}`)}`),out3(` ${kleur8.gray("Every step:")} ${clean4(a.instructions)||kleur8.gray("nothing added")}`);for(let s of steps)out3(` ${kleur8.gray(`${s.name} (${s.id}):`)} ${clean4(a.steps[s.id]??"")||kleur8.gray("nothing added")}`);out3(),info(`See a core prompt: squad agent ${r} --prompt --step ${steps[0].id}`),info(`Add to a step: squad agent ${r} --step ${steps[0].id} --set "..."`)}async function runNewTests(setting){let bp=board3(),meta=readMeta(bp);if(setting===void 0){out3(),out3(` ${kleur8.bold("New tests")} ${meta.alwaysNewTests?"always: every story writes new tests":"your choice on each new story, on by default"}`),out3(),info(`Change it: ${cmd3("squad config set new-tests always")} or ${cmd3("squad config set new-tests switch")}`);return}if(setting!=="always"&&setting!=="switch")throw new Error(`"${setting}" is not a choice here. Use always (every story writes new tests) or switch (each new story chooses).`);let{alwaysNewTests:_was,...rest}=meta;writeMeta(bp,setting==="always"?{...rest,alwaysNewTests:!0}:rest),success(setting==="always"?'Every new story writes new tests now. The "Write new tests" switch is gone from the new story form.':'Each new story can now be built without new tests: the "Write new tests" switch is on the new story form, on by default.')}async function runQuality(setting){let bp=board3(),meta=readMeta(bp);if(setting===void 0){let now3=meta.quality??DEFAULT_QUALITY;out3();for(let q6 of QUALITIES)out3(` ${q6===now3?kleur8.bold(`\u25CF ${q6}`):`\u25CB ${q6}`}${" ".repeat(10-q6.length)}${kleur8.gray(QUALITY_NOTE[q6])}`);out3(),info(`Change it: ${cmd3("squad config set quality best")} (or balanced, thrifty)`);return}let q5=parseQuality(setting);if(!q5)throw new Error(`"${setting}" is not a Quality setting. Use best, balanced or thrifty.`);writeMeta(bp,{...meta,quality:q5}),success(`Quality is ${q5} now: ${QUALITY_NOTE[q5]} The squad uses it from its next step.`)}async function runReply(ticketId,words2){let bp=board3(),t=requireTicket(bp,ticketId),q5=t.agentQuestion;if(t.waiting?.kind!=="ask"||!q5)throw new Error(`${t.id} has no question waiting for you.`);let text3=words2.join(" ");if(!text3){out3(),info(`${q5.from[0].toUpperCase()}${q5.from.slice(1)} asks: ${q5.text}`),q5.why&&info(`Why it matters: ${q5.why}`);for(let f of q5.files??[])info(`Attached: ${f}`);text3=await select8({message:"Your answer",choices:[...q5.options.map(o=>({name:o,value:o})),{name:"Let it decide",value:DECIDE_YOURSELF},{name:"Write my own",value:""}]})||await input5({message:"Your answer"})}answerAgent(bp,ticketId,text3),success("Sent. The squad carries on with your answer.")}async function runChanges(ticketId,opts){let c=await ticketChanges(board3(),ticketId);if(!c.files.length){info(c.from==="none"?"Nothing built yet, so there are no changes to show.":"No changes against main.");return}info(`${c.files.length} files changed${c.from==="branch"?` on ${c.branch}`:""}:`);for(let f of c.files)out3(` ${f.status.padEnd(8)} ${f.path}${f.binary?" (binary)":` +${f.added} -${f.removed}`}`),opts.full&&f.patch&&out3(f.patch.split(`
2029
2029
  `).map(l=>` ${l}`).join(`
2030
2030
  `));opts.full||info(`Full diff: squad changes ${ticketId} --full`)}async function runOpen(ticketId,opts){let r=openInEditor(board3(),ticketId,opts.editor);success(`Opened ${r.folder} in ${r.editor}.`)}async function runNote(ticketId,words2,opts){let bp=board3(),text3=words2.join(" ")||await input5({message:"What should the squad know?"}),kind=opts.criterion?"criterion":opts.rule?"rule":"note",t=addNote(bp,ticketId,{text:text3,kind});success(`Added to ${t.id}. ${liveNow(t,readUsual(bp)).noteWhen}`)}async function runMessage(ticketId,words2,opts){let bp=board3(),to=(opts.to??"builder").toLowerCase();if(!["analyst","architect","builder","reviewer","tester"].includes(to))throw new Error("--to is one of: analyst, architect, builder, reviewer, tester.");let text3=words2.join(" ")||await input5({message:`Your message to the ${to}`}),t=addNote(bp,ticketId,{text:text3,to});success(`Sent. ${liveNow(t,readUsual(bp)).noteWhen}`)}async function runTakeOver(ticketId){let t=takeOver(board3(),ticketId);success(`${t.id} is yours. The squad stops and waits.`),out3(),out3(" Open the Builder's session in its copy of the code:"),out3(` ${kleur8.bold(`cd ${shellQuote(t.takenOver.cwd)} && claude --resume ${t.takenOver.session}`)}`),out3(),info(`When you are done: squad give-back ${t.id} "what you changed"`)}async function runGiveBack(ticketId,words2){let t=giveBack(board3(),ticketId,words2.join(" "));success(`Given back. The checks, review and proof run again on your changes. Follow it: squad watch ${t.id}`)}async function runTakeBack(ticketId,opts){let t=takeBack(board3(),ticketId,opts.reason);success(`${t.id} is yours now. The reason stays on the ticket.`)}async function runHandOver(ticketId,words2,opts){let bp=board3(),to=words2.join(" ").trim()||await input5({message:"Hand it over to (their name, as their board shows it)"}),t=await handOver(bp,ticketId,{to,note:opts.note});success(`${t.id} is ${t.owner}'s now.${t.branch&&readMeta(bp).engine==="live"?` The work so far and the documents are pushed on ${t.branch}.`:""}`),out3(),out3(` ${t.owner}'s squad carries on from the branch: the Builder starts a fresh chat from the spec, the design and the code.`),out3(kleur8.gray(" Live progress, agent chats and screenshots stay on this computer. Your squad stops working on it.")),out3()}function testChangeLine(c){return`The Architect suggests changing the test command from "${c.from}" to "${c.to}"`}async function runTestCommand(ticketId,opts){let bp=board3(),t=requireTicket(bp,ticketId);if(opts.use&&opts.keep)throw new Error("Pick one: --use or --keep.");if(!opts.use&&!opts.keep){if(!t.testChange){info(`${t.id} has no change to the test command waiting.`);return}out3(),out3(` ${testChangeLine(t.testChange)}.`),out3(kleur8.gray(t.testChange.ask?" squad-kit cannot tell, so the checks keep using the current one until you choose.":" squad-kit tries it at the next checks and keeps what works. You can choose now.")),out3(` ${cmd3(`squad test-command ${t.id} --use`)} or ${cmd3(`squad test-command ${t.id} --keep`)}`),out3();return}let c=t.testChange;decideTestCommand(bp,t.id,!!opts.use),success(opts.use?`The test command is now "${c?.to}".`:`Kept the test command "${c?.from}".`)}async function runDecide(ticketId,choice,opts={}){let bp=board3(),t=requireTicket(bp,ticketId);if(!t.stuck||t.waiting?.kind!=="stuck")throw new Error(`${t.id} is not stuck.`);let index=choice?Number(choice)-1:NaN,ask2=Number.isNaN(index);if(ask2){out3(),out3(` ${t.stuck.question}`);for(let line3 of t.stuck.points??[])out3(` - ${line3}`);index=await select8({message:"What should happen?",choices:t.stuck.options.map((o,i)=>({name:`${o.name} ${kleur8.gray(o.note)}`,value:i}))})}let way=t.stuck.options[index]?.do,{note:note2,reason}=opts;if((way==="continue"||way==="skip-hooks")&&!reason?.trim()){if(!ask2)throw new Error('Add --reason "why". It shows on the ticket, the pull request and the release notes.');reason=await input5({message:`${way==="continue"?"Why continue anyway?":"Why save without the project's own check?"} (shows on the ticket, the pull request and the release notes)`,validate:v=>v.trim()?!0:"Write a short reason."})}way==="retry"&&ask2&&note2===void 0&&(note2=await input5({message:"A note for the squad (optional, Enter to skip)"})),way==="yourself"&&t.worktree&&out3(` The ticket's copy: ${t.worktree}`),await endingSaves(bp,()=>decideStuckBeside(bp,t.id,index,{note:note2,reason})),success(way==="yourself"?"OK. The checks, review and proof run again on your fix.":`Decided: "${t.stuck.options[index]?.name}".`)}async function runWatch(ticketId){let bp=board3();warnIfNoSquad(bp);let t=requireTicket(bp,ticketId);out3(),out3(` ${id(t.id)} ${kleur8.bold(t.title)}`),out3(kleur8.gray(" Watching live. Press Ctrl-C to stop.")),out3();let seen=new Set(t.activity.map(a=>a.at+a.text)),live="";for(let a of[...t.activity].slice(0,3).reverse())out3(` ${kleur8.gray(time(a.at))} ${a.text}`);for(;!(t.waiting||t.stage==="done");){await sleep9(400),stepSquad(bp),t=requireTicket(bp,ticketId);for(let a of[...t.activity].reverse()){let key=a.at+a.text;seen.has(key)||(seen.add(key),out3(` ${kleur8.gray(time(a.at))} ${roleLabel(a.who)}${a.text}`))}let now3=t.steps?.find(s=>s.status==="doing")?.live??"";now3&&now3!==live&&(live=now3,out3(kleur8.gray(` ${kleur8.green("\u25CF")} ${now3}`)))}out3(),t.waiting?(out3(` ${amber3("Your turn:")} ${t.waiting.summary}`),out3(` ${cmd3(nextCommand(t))}`)):out3(` ${kleur8.green("\u2713")} ${t.status}`),out3()}function roleLabel(who4){let names2={analyst:"Analyst",architect:"Architect",spec:"Spec writer",builder:"Builder",tester:"Tester",reviewer:"Reviewer",release:"Release",triage:"Triage",you:"You"};return kleur8.bold(`${names2[who4]??"Squad"} `)}function withV(version){return version&&(version.startsWith("v")?version:`v${version}`)}async function runRelease(version,opts){let bp=board3(),named=withV(version);if(opts.markSent||(opts.reason!==void 0&&warning("--reason only counts with --mark-sent, so it is left out here."),opts.anyway&&warning("--anyway only counts with --mark-sent, so it is left out here.")),opts.goOn!==void 0&&!opts.goOn.trim())throw new Error(`Say why it goes on: ${cmd3('squad release --go-on "<why>"')}`);if(opts.pushAgain)return runPushAgain(bp,named);if(opts.markSent){let target=listReleases(bp).find(r2=>r2.unpushed?.length&&(!named||r2.version===named));if(!target)throw new Error(named?`${named} has nothing left to push.`:"Every release is pushed already.");if(!opts.reason?.trim())throw new Error(`Say how it got there: ${cmd3(`squad release ${target.version} --mark-sent --reason "<how>"`)}`);try{let r2=await markReleaseSent(bp,target.version,{reason:opts.reason,...opts.anyway?{anyway:!0}:{}});success(`${r2.version} is marked as sent. Its tickets are closed.`);for(let l of(r2.log??[]).slice(-1))l.ok||warning(l.text)}catch(err2){throw err2 instanceof SentNotSure?new Error(`${err2.message} To mark it anyway, add: ${cmd3("--anyway")}`):err2}return}let draft=draftRelease(bp);if(!draft||named&&draft.version!==named)throw new Error("There is no release ready to go out.");if(opts.putBack?.length){for(let x of opts.putBack)putBackInRelease(bp,draft.version,x);success(`${opts.putBack.map(x=>x.toUpperCase()).join(", ")} ${opts.putBack.length===1?"is":"are"} back in ${draft.version}.`);return}for(let x of opts.without??[])draft=await takeOutOfRelease(bp,draft.version,x),success(`Took ${x.toUpperCase()} out of ${draft.version}. It goes out with the next release. Put it back: ${cmd3(`squad release --put-back ${x.toUpperCase()}`)}`);let tickets=listTickets(bp).filter(t=>draft.tickets.includes(t.id));out3(),out3(` ${kleur8.bold(`Release ${draft.version}`)} ${kleur8.gray(`${draft.from} \u2192 ${draft.version}`)}`),out3();for(let t of tickets)out3(` ${kleur8.green("\u2713")} ${id(t.id)} ${t.title}`);for(let x of draft.out??[])out3(kleur8.gray(` \u25CB ${x} taken out: it goes out with the next release`));out3();for(let l of(draft.notes.trim()||composeNotes(bp,draft)).split(`
@@ -2038,14 +2038,14 @@ ${d.ask}`+list6("Acceptance criteria",d.criteria)+list6("Business rules",d.rules
2038
2038
  `),!await confirm10({message:"Create this ticket?",default:!0})){info("Not created.");return}fields={title:fields.title||d.title,ask:fields.ask??d.ask,priority:fields.priority??d.priority,kind:opts.bug?"bug":d.kind,track:opts.express?"express":opts.full?"full":d.track,talkFirst:!!opts.talk,...opts.newTests===!1?{noNewTests:!0}:{},criteria:[...d.criteria,...fields.criteria??[]],rules:[...d.rules,...fields.rules??[]],edgeCases:[...d.edgeCases,...fields.edgeCases??[]]}}let title2=fields.title||await input5({message:"Ticket title:"}),t=createTicket(bp,{...fields,title:title2,files});success(t.track==="express"?`${t.id} created on the Express track. The squad goes straight to the plan.`:`${t.id} created. The Analyst is reading it.`),t.noNewTests&&info("It is built without new tests: the project's own checks, the review and the proof still run.")}async function runUnstick(ticketId,opts){let bp=board3();if(opts.freePort){let port=requireTicket(bp,ticketId).quiet?.port;if(port?.ours&&!await confirm10({message:`Stop the program on port ${port.port}, which squad-kit started, and try the step again?`,default:!0})){info("Nothing was stopped.");return}let t2=await freePortAndRetry(bp,ticketId);success(`Freed ${port?`port ${port.port}`:"the port"} on ${t2.id}. The step starts again, with the work so far.`);return}if(opts.keepWaiting){let t2=keepWaiting(bp,ticketId);success(`OK, waiting. squad-kit stays quiet about ${t2.id} for 10 minutes.`);return}let t=endStuckCommand(bp,ticketId);success(`Ended the stuck command on ${t.id}. The squad carries on from there.`)}async function runStop(ticketId,opts={}){let bp=board3();if(opts.again){let t2=restartStep(bp,ticketId);success(`Stopping the step on ${t2.id}, then it starts again. The work so far is kept.`);return}let t=stopTicket(bp,ticketId);success(`Stopping the squad on ${t.id}. The work so far is kept.`)}async function runStartTicket(ticketId){let t=startTicket(board3(),ticketId);success(`${t.id} started. The Analyst is reading it.`),info(`Follow it live: squad watch ${t.id}`)}async function runBacklog(opts){let bp=board3(),tracker2=trackerFor(bp);if(!tracker2)throw new Error("No tracker is set up for this project. Run: squad config set tracker");let sprints=tracker2.abilities.sprints?await tracker2.sprints():[],wanted=opts.sprint?.toLowerCase(),sprint=opts.all?void 0:wanted?sprints.find(s=>s.name.toLowerCase()===wanted||s.path.toLowerCase()===wanted):sprints.find(s=>s.current)??sprints[0];if(wanted&&!sprint)throw new Error(`No sprint called "${opts.sprint}". Sprints: ${sprints.map(s=>s.name).join(", ")}`);let items3=await tracker2.list(sprint?{sprint:sprint.path}:{}),tickets=listTickets(bp).filter(t=>t.source.type===tracker2.name);out3(),out3(` ${kleur8.bold("Backlog")} ${kleur8.gray(`${sprint?sprint.name:"everything open"} \xB7 ${items3.length} open`)}`),out3();let w=Math.min(60,Math.max(10,...items3.map(i=>i.title.length)));for(let i of items3){let t=tickets.find(x=>x.source.id===i.id||x.source.ref===i.ref),title2=i.title.length>w?`${i.title.slice(0,w-1)}\u2026`:i.title.padEnd(w),kind=i.isBug?kleur8.red("bug "):" ",where=t?kleur8.gray(`${t.id} ${t.stage==="done"?"released":STAGE_NAME[t.stage]}`):cmd3(`squad pull ${i.id}`);out3(` ${id(i.id.padStart(5))} ${kind}${title2} ${kleur8.gray(String(i.points??"").padStart(2))} ${kleur8.gray(i.state.padEnd(9))} ${where}`)}out3()}async function runPull(ref2){let bp=board3(),t=await pullTicket(bp,ref2);success(`${t.id} pulled from ${t.source.ref}: ${t.title}`)}async function runDemo(opts){let root=demoFolder(),paths=buildPaths(root);fs167.existsSync(paths.configFile)||(fs167.mkdirSync(paths.squadDir,{recursive:!0}),saveConfig(paths.configFile,{...DEFAULT_CONFIG,project:{...DEFAULT_CONFIG.project,name:opts.project??"Case portal"}}));let bp=boardPaths(paths.squadDir);opts.firstRun?(seedDemoBoard(bp,{speed:opts.speed,project:opts.project,firstRun:!0}),success("Sample project at its first open: follow the setup, then check the Overview.")):(!opts.keep||!boardExists(bp))&&(seedDemoBoard(bp,{speed:opts.speed,project:opts.project}),success("Sample board ready: 14 tickets, 4 releases and 5 saved decisions.")),process.chdir(root),await runConsole({port:opts.port,open:opts.open,token:opts.token,demo:!0})}var out4=(s="")=>process.stdout.write(`${s}
2039
2039
  `),cmd4=s=>kleur9.green(s),sleep10=ms2=>new Promise(r=>setTimeout(r,ms2)),MARK2={done:kleur9.green("\u2713"),now:kleur9.cyan("\u2026"),todo:kleur9.gray("\u25CB"),failed:kleur9.red("!"),skipped:kleur9.gray("\u2013"),stopped:kleur9.yellow("\u25A0")};function timeOf(st,at3=Date.now()){return st.state==="now"&&st.since?`${durationWords((at3-Date.parse(st.since))/1e3)} \xB7 ${st.usual}`:st.seconds!==void 0&&st.state!=="skipped"?durationWords(st.seconds):st.state==="todo"?st.usual:""}function stepLines(st,at3=Date.now()){let time2=timeOf(st,at3),lines3=[` ${MARK2[st.state]} ${st.state==="todo"?kleur9.gray(st.label):st.label}${time2?kleur9.gray(` ${time2}`):""}`];st.note&&st.state!=="todo"&&lines3.push(kleur9.gray(` ${st.note}`)),st.error&&lines3.push(kleur9.red(` ${st.error}`));for(let w of st.ways)w.do==="again"&&lines3.push(` ${w.label}: ${cmd4(w.command)}`);return lines3}function printSetup(v){out4(),out4(` ${kleur9.bold(`Getting to know ${v.project}`)} ${kleur9.gray(setupLine(v))}`),out4();for(let st of v.steps)for(let l of stepLines(st))out4(l);out4(),v.ready&&!v.approved?out4(v.empty?` What do you want to build? ${cmd4('squad overview --build "what you want"')}`:` Check what squad-kit understood: ${cmd4("squad overview")} then ${cmd4("squad overview --approve")}`):v.ready||out4(` Follow it: ${cmd4("squad setup --follow")} Stop: ${cmd4("squad setup --stop")}`),!v.qualityAsked&&!v.approved&&out4(kleur9.gray(` Quality is ${v.quality}. To change it: squad config set quality best|balanced|thrifty`)),out4()}function stepDemo(bp){readMeta(bp).engine==="demo"&&!squadRunsElsewhere(bp)&&tick(bp)}function noSquadNote(bp){readMeta(bp).engine==="live"&&!squadRunsElsewhere(bp)&&warning("The setup runs while squad-kit is open. Run `squad console` in another tab.")}function followSetup(bp,onDone,playDemo=!1){let said=new Map,ended=!1,look=()=>{if(ended)return;let v;try{playDemo&&stepDemo(bp),v=setupView(bp)}catch{return}if(v){for(let st of v.steps){let key=`${st.state}|${st.state==="now"?"":st.note??""}|${st.error??""}`;if(!(said.get(st.id)===key||st.state==="todo")){said.set(st.id,key);for(let l of stepLines(st))out4(l)}}(v.ready||v.approved)&&(ended=!0,out4(),success(v.approved?"Set up.":v.empty?`${v.project} is ready. One question for you on the Overview, or here: squad overview --build "what you want"`:`squad-kit knows ${v.project} now. Check what it understood on the Overview, or here: squad overview`),setTimeout(()=>onDone?.(),0))}};look();let timer=setInterval(look,1e3);return()=>{ended=!0,clearInterval(timer)}}async function runSetup(opts){let bp=board3();if(opts.stop){stopSetup(bp),success("Stopped. The setup waits until you start it again: squad setup --again");return}if(opts.again){let step2=typeof opts.again=="string"?opts.again:"read";if(!SETUP_STEPS.includes(step2))throw new Error(`"${step2}" is not a step. Use tool, read, commands, tests or app.`);setupAgain(bp,step2),success(step2==="read"||step2==="tool"?"The Architect reads the project again.":"squad-kit tries that step again."),noSquadNote(bp)}let v=setupView(bp);if(!v){info("This project is set up. See what squad-kit understood: squad overview");return}if(!opts.follow)return printSetup(v);noSquadNote(bp),out4(),out4(` ${kleur9.bold(`Getting to know ${v.project}`)} ${kleur9.gray("Ctrl-C stops watching; the setup keeps going.")}`),out4(),await new Promise(done2=>{let stop=followSetup(bp,()=>{stop(),done2()},!0)})}var list5=(head,items3)=>{if(items3.length){out4(` ${kleur9.bold(head)}`);for(let x of items3)out4(` \u2022 ${x}`);out4()}};async function runOverview(opts){let bp=board3();if(opts.approve){let v2=approveOverview(bp);success(`Approved. ${v2.rules.filter(r=>r.fromScan).length?"The rules squad-kit found are the rules every agent follows now.":"The overview is the project's home now."}`),info(`Start with your first ticket: ${v2.ideas.length?"squad overview --idea 1":'squad new "what you want"'}`);return}if(opts.idea){let t=ticketFromIdea(bp,Number(opts.idea));success(`${t.id} is on the board: ${t.title}. The Analyst reads it first.`),info(`Follow it: squad watch ${t.id}`);return}if(opts.again||opts.withTalk){opts.withTalk?readAgainWithTalk(bp):setupAgain(bp,"read"),success("The Architect reads the project again. Follow it: squad setup --follow"),noSquadNote(bp);return}if(opts.wrong)return talk(bp,Array.isArray(opts.wrong)?opts.wrong.join(" "):"");if(opts.build){tellIdea(bp,opts.build.join(" ")),success("The Architect reads what you want to build. Follow it: squad setup --follow"),noSquadNote(bp);return}let v=overviewView(bp);if(out4(),out4(` ${kleur9.bold(v.project)}${v.readAt?kleur9.gray(` read ${new Date(v.readAt).toLocaleString()}`):""}`),out4(),v.handover==="reading"&&(out4(` ${kleur9.yellow(v.setup?.line??"squad-kit is getting to know the project.")} ${cmd4("squad setup")}`),out4()),v.handover==="ready"&&v.empty&&!v.idea?(out4(` ${kleur9.yellow("What do you want to build?")} ${cmd4('squad overview --build "what you want"')}`),out4()):v.handover==="ready"&&(out4(` ${kleur9.yellow(v.empty?"The Architect wrote down what you want to build. Check it, then start.":"squad-kit has read your project. Check what it understood, then start.")}`),v.proposed&&out4(kleur9.gray(` When you approve, the ${v.proposed} ${v.proposed===1?"rule":"rules"} below become the rules every agent follows.`)),out4()),v.summary&&(out4(` ${v.summary}`),out4()),v.users&&(out4(` ${kleur9.bold("Who uses it")}`),out4(` ${v.users}`),out4()),list5("What it can do today",[...v.can,...v.since.map(x=>`${x.title} (${x.id}, new since the read)`)]),v.proof){let p=v.proof;list5("Proof squad-kit can work here",[...p.files?[readWords(p.files)]:[],...(p.tests??[]).map(t=>t.ok?`Ran your tests: ${t.passed??"they"} pass`:`Your tests did not pass here (${t.command})`),...p.app?[p.app.ok?"Opened the app":"The app did not open here"]:[],...p.branch?[`On git branch ${p.branch}${p.unsaved?`, ${p.unsaved} changes not saved in git`:", nothing unsaved"}`]:[]])}if(v.parts.length){out4(` ${kleur9.bold("How it fits together")}`);for(let part of v.parts){let to=v.links.filter(l=>l.from===part.id).map(l=>`${l.label??"\u2192"} ${v.parts.find(x=>x.id===l.to)?.name??l.to}`);out4(` ${part.name}${part.what?kleur9.gray(`: ${part.what}`):""}${part.files.length?kleur9.gray(` (${part.files.join(", ")})`):""}`),to.length&&out4(kleur9.gray(` ${to.join(" \xB7 ")}`))}v.product&&out4(` ${v.product} ${kleur9.gray("An engineer can correct this:")} ${cmd4("squad parts")}`),out4()}else v.map.length&&list5("Project map",v.map.map(m=>`${m.area}: ${m.detail}`));list5(v.proposed?"Rules the squad will follow once you approve":"Rules the squad follows",v.rules.map(r=>`${r.id} ${r.title}${r.proposed?kleur9.yellow(" (proposed)"):""}`)),list5("Worth knowing",v.worth),v.ideas.length&&(out4(` ${kleur9.bold("Good first tickets")}`),v.ideas.forEach((i,n)=>out4(` ${n+1}. ${i.title}${i.ticket?kleur9.gray(` (${i.ticket})`):""} ${cmd4(`squad overview --idea ${n+1}`)}`)),out4()),v.old&&(out4(kleur9.gray(" Read by an older squad-kit. Who uses it, what it can do and ticket ideas come with the next read: squad overview --again")),out4()),v.empty?v.handover==="ready"&&v.idea&&out4(` Looks right: ${cmd4("squad overview --approve")} Change what you said: ${cmd4('squad overview --build "what you want"')}`):v.handover==="ready"?out4(` Looks right: ${cmd4("squad overview --approve")} Something is wrong: ${cmd4('squad overview --wrong "what is wrong"')}`):out4(kleur9.gray(" Read it again: squad overview --again")),out4()}async function talk(bp,text3){let open3=readTalk(bp)?.open;if(!text3.trim()&&open3)throw new Error('Say what is wrong: squad overview --wrong "the login does check passwords"');let before=readTalk(bp)?.messages.length??0;if(open3?sendTalk(bp,text3):startTalk(bp,text3||void 0),readMeta(bp).engine==="live"&&!squadRunsElsewhere(bp)){warning("The Architect answers while squad-kit is open. Run `squad console` in another tab, then: squad overview --wrong");return}info("The Architect is answering\u2026");for(let i=0;i<600;i+=1){stepDemo(bp);let t=readTalk(bp);if(t&&!t.thinking&&t.messages.length>before){let answer2=[...t.messages].reverse().find(m=>m.who!=="you");answer2&&out4(`
2040
2040
  ${kleur9.bold("Architect")}: ${answer2.text}
2041
- `),out4(` Answer: ${cmd4('squad overview --wrong "\u2026"')} Read it again with this: ${cmd4("squad overview --again --with-talk")}`);return}await sleep10(1e3)}warning("No answer yet. It shows on the Overview when it comes.")}var DEFAULT_PORT2=4571,RESTART_EXIT=75,RESTART_APP_EXIT=76,KEEPER_ENV="SQUAD_CONSOLE_KEEPER",APP_COMMAND=["--no-open"];async function projectHere(opts){let ask2=addAsk(process.cwd());if(ask2){if(!isInteractive()||opts.yes)throw new Error(ask2.words);return info(ask2.words),await confirm11({message:`Use ${ask2.root}?`,default:!0})?ask2.root:ask2.kind==="inside-repo"?null:process.cwd()}let found2=findSquadRoot();if(found2)return found2;let here=process.cwd();if(!isOwnFolder(here))throw new Error("squad-kit works inside a project folder. Open the terminal in your project folder (or a new, empty one) and run: squad console. To open squad-kit with all your projects, run: squad");return isInteractive()&&!opts.yes&&!await confirm11({message:`${here} has no squad-kit yet. Set it up here?`,default:!0})?null:(step(`Setting up squad-kit in ${here}`),here)}function takeKeeperFlag(){let underKeeper=process.env[KEEPER_ENV]==="1";return delete process.env[KEEPER_ENV],underKeeper}function olderBuild(info2){return!info2.build||info2.build<buildStamp()}async function runConsole(opts){let underKeeper=takeKeeperFlag(),root=await projectHere(opts);if(!root)return;let app=runningApp(),own2=runningConsole(boardPaths(buildPaths(root).squadDir));if(own2&&own2.pid!==app?.pid){if(!opts.restart){let link=own2.port&&own2.token?`http://127.0.0.1:${own2.port}/?t=${own2.token}`:null;banner(),blank(),info(`This project's board is already open${own2.startedAt?` (started ${new Date(own2.startedAt).toLocaleTimeString()})`:""}.`),link?info(`Open it: ${link}`):info(`It runs in another terminal (process ${own2.pid}).`),olderBuild(own2)?warning("It runs an older squad-kit. Restart it to use the new one: squad console --restart"):info("To restart it: squad console --restart"),link&&opts.open!==!1&&await open2(link).catch(()=>{});return}if(info("Stopping the board that is running now\u2026"),!await stopConsole(own2.pid))throw new Error(`The running board (process ${own2.pid}) did not stop. Close it with Ctrl-C in its terminal.`)}if(app&&!opts.restart&&(!opts.demo||own2)){await addToRunningApp(app,root,opts);return}app&&!opts.demo&&await stopApp(app);let ready=prepareProject(root);for(let line3 of ready.did)success(line3);let sv=setupView(ready.bp),follow4=sv&&!sv.ready&&!sv.approved?ready.bp:void 0,restartWith=opts.demo?["demo","--keep","--no-open"]:["console","--no-open","--project",root];await serveApp({first:ready.paths,follow:follow4,opts,underKeeper,title:opts.demo?"squad demo":"squad console",restartWith,...opts.demo?{demo:!0}:{}})}async function runApp(opts){let underKeeper=takeKeeperFlag(),app=runningApp();if(app&&!opts.restart){let link=`http://127.0.0.1:${app.port}/?t=${app.token}`;banner(),blank(),info(`squad-kit is open already${app.startedAt?` (started ${new Date(app.startedAt).toLocaleTimeString()})`:""}.`),info(`Open it: ${link}`),olderBuild(app)?warning("It runs an older squad-kit. Restart it to use the new one: squad --restart"):info("To restart it: squad --restart"),opts.open!==!1&&await open2(link).catch(()=>{});return}app&&await stopApp(app),await serveApp({opts,underKeeper,title:"squad-kit",restartWith:APP_COMMAND})}async function stopApp(app){if(info("Stopping squad-kit, which is running now\u2026"),!await stopConsole(app.pid))throw new Error(`squad-kit (process ${app.pid}) did not stop. Close it with Ctrl-C in its terminal.`)}async function addToRunningApp(app,root,opts){let base2=`http://127.0.0.1:${app.port}`,res=await fetch(`${base2}/api/v1/app/projects`,{method:"POST",headers:{authorization:`Bearer ${app.token}`,"content-type":"application/json"},body:JSON.stringify({folder:root})}).catch(()=>null);if(!res)throw new Error(`squad-kit runs (process ${app.pid}) but does not answer on port ${app.port}. Restart it: squad console --restart`);let data=await res.json().catch(()=>({}));if(!res.ok||!data.url)throw new Error(`${data.error??`squad-kit could not add this project (${res.status}).`}${olderBuild(app)?" It runs an older squad-kit. Restart it: squad console --restart":""}`);let bp=boardPaths(buildPaths(root).squadDir),setup=data.did?.length?setupView(bp):null,fresh=!!(setup&&!setup.ready&&!setup.approved),link=`${base2}${fresh?data.url.replace(/\/board$/,"/setup"):data.url}?t=${app.token}`;banner(),blank();for(let line3 of data.did??[])success(line3);info(data.added?`Added ${data.name} to squad-kit, which is open already.`:`${data.name} is open in squad-kit.`),info(`Open it: ${link}`),olderBuild(app)&&warning("squad-kit runs an older version. Restart it to use the new one: squad console --restart"),opts.open!==!1&&await open2(link).catch(()=>{}),fresh&&await followHere(bp)}async function followHere(bp){blank(),step("Getting to know the project (Ctrl-C stops watching; the setup keeps going):"),await new Promise(done2=>{let stop=followSetup(bp,()=>{stop(),done2()})})}async function serveApp(a){let{opts}=a,own2=a.first?readConsoleInfo(boardPaths(a.first.squadDir)):null,prior=a.demo?own2:readAppInfo()??own2,portOpt=opts.port,requestedPort=portOpt!==void 0&&!Number.isNaN(portOpt)&&portOpt>=0?portOpt:prior?.port??DEFAULT_PORT2,token=opts.token??prior?.token??randomBytes10(32).toString("hex"),build=buildStamp();printErrors(!0);let server=await startConsoleServer({paths:a.first,app:!a.demo,requestedPort,token,build,keepOnHangup:startedToOutlive()});installCrashNet();let info2={pid:process.pid,port:server.port,token,startedAt:new Date().toISOString(),version:readInstalledPackage().version,build};a.demo||writeAppInfo(info2),server.updates.watch();let firstId=a.first?server.projects.first:null,url=`http://127.0.0.1:${server.port}${firstId?`/p/${firstId}/${a.follow?"setup":"board"}`:"/"}?t=${token}`,rows2=server.projects.summaries();banner(),divider(a.title),summaryBox(` ${a.title} `,[...a.first?[{key:"project",value:resolve(a.first.root)}]:[],{key:"projects",value:String(rows2.filter(r=>r.open).length)},{key:"url",value:url},{key:"token",value:`${token.slice(0,8)}\u2026`},{key:"port",value:String(server.port)}]),server.port!==requestedPort&&warning(`port ${requestedPort} was busy \u2014 bound to ${server.port} instead`);for(let r of rows2.filter(x=>!x.open))warning(`${r.name} did not open: ${r.why}`);if(blank(),step("Next:"),info("1) Work from the board, or from this terminal: try `squad inbox` in a second tab."),info("2) Every button on the board shows its terminal command."),info("3) Add more projects from Home, or run `squad console` in their folder."),info("4) Press Ctrl-C here to stop squad-kit when you're done."),opts.open!==!1)try{await open2(url)}catch{warning("Could not open a browser automatically. Copy the URL above into your browser.")}else info("Browser auto-open disabled (--no-open). Copy the URL above into your browser.");let stopFollow=()=>{};if(a.follow&&(blank(),step("Getting to know the project:"),stopFollow=followSetup(a.follow,()=>stopFollow())),await server.done,stopFollow(),a.demo||writeAppInfo({...info2,pid:null}),server.restartRequested()){info("Restarting the board\u2026");let removed=!!(a.first&&!a.demo&&!server.projects.first);await restartBoard(a.underKeeper,removed?APP_COMMAND:a.restartWith,removed);return}await endRuns(),info("console stopped")}var RESTART_WAIT_MS=1e4,ASKED_MS=2e3;async function endRuns(waitMs=RESTART_WAIT_MS){return await allEnded(waitMs)||(askAllToEnd(),await allEnded(ASKED_MS))?!0:(killAll(),allEnded(1e3))}async function restartBoard(underKeeper,command,asApp=!1){let argsFor=c=>[...process.execArgv,process.argv[1],...c],args=argsFor(command);for(await endRuns(),await new Promise(done2=>process.stdout.write("",()=>done2())),typeof process.execve=="function"&&process.platform!=="win32"&&process.execve(process.execPath,[process.execPath,...args],process.env),underKeeper&&process.exit(asApp?RESTART_APP_EXIT:RESTART_EXIT),process.on("SIGINT",()=>{});;){let child=spawn17(process.execPath,args,{stdio:"inherit",env:{...process.env,[KEEPER_ENV]:"1"}}),forward=()=>child.kill("SIGTERM");process.on("SIGTERM",forward);let code=await new Promise(done2=>child.on("exit",c=>done2(c??0)));if(process.off("SIGTERM",forward),code===RESTART_APP_EXIT)args=argsFor(APP_COMMAND);else if(code!==RESTART_EXIT){process.exitCode=code;return}}}import fs168 from"fs";import os28 from"os";import path159 from"path";import kleur10 from"kleur";var out5=(s="")=>process.stdout.write(`${s}
2042
- `),home=p=>p.startsWith(os28.homedir())?`~${p.slice(os28.homedir().length)}`:p;function stateOf3(root){if(!fs168.existsSync(path159.join(root,SQUAD_DIR,"config.yaml")))return kleur10.red(fs168.existsSync(root)?"its squad-kit files are gone":"the folder is not there any more");let bp=boardPaths(path159.join(root,SQUAD_DIR));if(!boardExists(bp))return kleur10.gray("no board yet: run squad console in its folder");let meta=readMeta(bp),setup=setupView(bp);if(setup&&!setup.approved){let line3=setupLine(setup);return setup.steps.some(s=>s.state==="failed"&&s.id==="read")?kleur10.red(line3):kleur10.yellow(line3)}if(meta.engine==="live"&&meta.scan&&meta.scan.status!=="done")return meta.scan.status==="failed"?kleur10.red("the Architect could not read it"):kleur10.yellow("setting up: the Architect reads it");let open3=listTickets(bp).filter(t=>t.stage!=="done").length,waiting2=inbox(bp).length;return`${kleur10.gray(`${open3} open`)}${waiting2?kleur10.yellow(` \xB7 ${waiting2} waiting for you`):""}`}async function newProject(name,opts){if(opts.brd&&!fs168.existsSync(opts.brd))throw new Error(`There is no file at ${opts.brd}.`);let input7={name,...opts.where?{where:path159.resolve(opts.where)}:{},...opts.idea?{idea:opts.idea}:{},...opts.brd?{file:{name:path159.basename(opts.brd),data:fs168.readFileSync(opts.brd).toString("base64")}}:{}},app=runningApp(),root;if(app){let res=await fetch(`http://127.0.0.1:${app.port}/api/v1/app/projects/new`,{method:"POST",headers:{authorization:`Bearer ${app.token}`,"content-type":"application/json"},body:JSON.stringify(input7)}),made=await res.json();if(!res.ok||!made.root)throw new Error(made.error??"squad-kit could not make the project.");root=made.root}else{let made=makeNewProject(input7);rememberProject(made.bp.projectId,made.bp.root),root=made.bp.root}success(`${path159.basename(root)} is ready in ${home(root)}.`),out5(opts.idea?" The Architect reads what you want to build. Follow it in its folder with: squad setup --follow":" The Analyst reads your BRD. Follow it in its folder with: squad requirements"),app||out5(" Its squad starts when you open squad-kit: squad")}async function runProjects(opts){if(opts.new!==void 0)return newProject(opts.new,opts);let app=runningApp();if(opts.remove){let root=readProjects().find(p=>!fs168.existsSync(p.root)&&(p.id===opts.remove||p.root===path159.resolve(opts.remove)||path159.basename(p.root).toLowerCase()===opts.remove.toLowerCase()))?.root??findProject(opts.remove),entry=readProjects().find(p=>p.root===root);if(!entry)throw new Error(`${projectName(root)} is not in squad-kit.`);app?(await fetch(`http://127.0.0.1:${app.port}/api/v1/app/projects/${encodeURIComponent(entry.id)}`,{method:"DELETE",headers:{authorization:`Bearer ${app.token}`}}).catch(()=>null))?.ok||forgetProject(entry.id):forgetProject(entry.id),success(`${projectName(root)} is out of squad-kit. Its files stay in ${home(root)}. Add it again any time: squad console in its folder.`);return}let list6=readProjects();if(out5(),out5(` ${kleur10.bold("Your projects")} ${kleur10.gray(`(${list6.length})`)}${app?kleur10.gray(` squad-kit is open: http://127.0.0.1:${app.port}/?t=${app.token}`):""}`),out5(),!list6.length){out5(kleur10.gray(" No projects yet. Open a terminal in a project folder (or a new, empty one) and run: squad console")),out5();return}let names2=list6.map(p=>projectName(p.root)),w=Math.max(...names2.map(n=>n.length));list6.forEach((p,i)=>out5(` ${kleur10.bold(names2[i].padEnd(w))} ${kleur10.gray(home(p.root))} ${stateOf3(p.root)}`)),out5(),out5(kleur10.gray(app?" Add one: squad console in its folder, or Add project on Home.":" Open them all: squad")),out5()}import kleur11 from"kleur";var out6=(s="")=>process.stdout.write(`${s}
2041
+ `),out4(` Answer: ${cmd4('squad overview --wrong "\u2026"')} Read it again with this: ${cmd4("squad overview --again --with-talk")}`);return}await sleep10(1e3)}warning("No answer yet. It shows on the Overview when it comes.")}var DEFAULT_PORT2=4571,RESTART_EXIT=75,RESTART_APP_EXIT=76,KEEPER_ENV="SQUAD_CONSOLE_KEEPER",APP_COMMAND=["--no-open"];async function projectHere(opts){let ask2=addAsk(process.cwd());if(ask2){if(!isInteractive()||opts.yes)throw new Error(ask2.words);return info(ask2.words),await confirm11({message:`Use ${ask2.root}?`,default:!0})?ask2.root:ask2.kind==="inside-repo"?null:process.cwd()}let found2=findSquadRoot();if(found2)return found2;let here=process.cwd();if(!isOwnFolder(here))throw new Error("squad-kit works inside a project folder. Open the terminal in your project folder (or a new, empty one) and run: squad console. To open squad-kit with all your projects, run: squad");return isInteractive()&&!opts.yes&&!await confirm11({message:`${here} has no squad-kit yet. Set it up here?`,default:!0})?null:(step(`Setting up squad-kit in ${here}`),here)}function takeKeeperFlag(){let underKeeper=process.env[KEEPER_ENV]==="1";return delete process.env[KEEPER_ENV],underKeeper}function olderBuild(info2){return!info2.build||info2.build<buildStamp()}async function runConsole(opts){let underKeeper=takeKeeperFlag(),root=await projectHere(opts);if(!root)return;let app=runningApp(),own2=runningConsole(boardPaths(buildPaths(root).squadDir));if(own2&&own2.pid!==app?.pid){if(!opts.restart){let link=own2.port&&own2.token?`http://127.0.0.1:${own2.port}/?t=${own2.token}`:null;banner(),blank(),info(`This project's board is already open${own2.startedAt?` (started ${new Date(own2.startedAt).toLocaleTimeString()})`:""}.`),link?info(`Open it: ${link}`):info(`It runs in another terminal (process ${own2.pid}).`),olderBuild(own2)?warning("It runs an older squad-kit. Restart it to use the new one: squad console --restart"):info("To restart it: squad console --restart"),link&&opts.open!==!1&&await open2(link).catch(()=>{});return}if(info("Stopping the board that is running now\u2026"),!await stopConsole(own2.pid))throw new Error(`The running board (process ${own2.pid}) did not stop. Close it with Ctrl-C in its terminal.`)}if(app&&!opts.restart&&(!opts.demo||own2)){await addToRunningApp(app,root,opts);return}app&&!opts.demo&&await stopApp(app);let ready=prepareProject(root);for(let line3 of ready.did)success(line3);let sv=setupView(ready.bp),follow4=sv&&!sv.ready&&!sv.approved?ready.bp:void 0,restartWith=opts.demo?["demo","--keep","--no-open"]:["console","--no-open","--project",root];await serveApp({first:ready.paths,follow:follow4,opts,underKeeper,title:opts.demo?"squad demo":"squad console",restartWith,...opts.demo?{demo:!0}:{}})}async function runApp(opts){let underKeeper=takeKeeperFlag(),app=runningApp();if(app&&!opts.restart){let link=`http://127.0.0.1:${app.port}/?t=${app.token}`;banner(),blank(),info(`squad-kit is open already${app.startedAt?` (started ${new Date(app.startedAt).toLocaleTimeString()})`:""}.`),info(`Open it: ${link}`),olderBuild(app)?warning("It runs an older squad-kit. Restart it to use the new one: squad --restart"):info("To restart it: squad --restart"),opts.open!==!1&&await open2(link).catch(()=>{});return}app&&await stopApp(app),await serveApp({opts,underKeeper,title:"squad-kit",restartWith:APP_COMMAND})}async function stopApp(app){if(info("Stopping squad-kit, which is running now\u2026"),!await stopConsole(app.pid))throw new Error(`squad-kit (process ${app.pid}) did not stop. Close it with Ctrl-C in its terminal.`)}async function addToRunningApp(app,root,opts){let base2=`http://127.0.0.1:${app.port}`,res=await fetch(`${base2}/api/v1/app/projects`,{method:"POST",headers:{authorization:`Bearer ${app.token}`,"content-type":"application/json"},body:JSON.stringify({folder:root})}).catch(()=>null);if(!res)throw new Error(`squad-kit runs (process ${app.pid}) but does not answer on port ${app.port}. Restart it: squad console --restart`);let data=await res.json().catch(()=>({}));if(!res.ok||!data.url)throw new Error(`${data.error??`squad-kit could not add this project (${res.status}).`}${olderBuild(app)?" It runs an older squad-kit. Restart it: squad console --restart":""}`);let bp=boardPaths(buildPaths(root).squadDir),setup=data.did?.length?setupView(bp):null,fresh=!!(setup&&!setup.ready&&!setup.approved),link=`${base2}${fresh?data.url.replace(/\/board$/,"/setup"):data.url}?t=${app.token}`;banner(),blank();for(let line3 of data.did??[])success(line3);info(data.added?`Added ${data.name} to squad-kit, which is open already.`:`${data.name} is open in squad-kit.`),info(`Open it: ${link}`),olderBuild(app)&&warning("squad-kit runs an older version. Restart it to use the new one: squad console --restart"),opts.open!==!1&&await open2(link).catch(()=>{}),fresh&&await followHere(bp)}async function followHere(bp){blank(),step("Getting to know the project (Ctrl-C stops watching; the setup keeps going):"),await new Promise(done2=>{let stop=followSetup(bp,()=>{stop(),done2()})})}async function serveApp(a){let{opts}=a,own2=a.first?readConsoleInfo(boardPaths(a.first.squadDir)):null,prior=a.demo?own2:readAppInfo()??own2,portOpt=opts.port,requestedPort=portOpt!==void 0&&!Number.isNaN(portOpt)&&portOpt>=0?portOpt:prior?.port??DEFAULT_PORT2,token=opts.token??prior?.token??randomBytes10(32).toString("hex"),build=buildStamp();printErrors(!0);let server=await startConsoleServer({paths:a.first,app:!a.demo,requestedPort,token,build,keepOnHangup:startedToOutlive()});installCrashNet();let info2={pid:process.pid,port:server.port,token,startedAt:new Date().toISOString(),version:readInstalledPackage().version,build};a.demo||writeAppInfo(info2),server.updates.watch();let firstId=a.first?server.projects.first:null,url=`http://127.0.0.1:${server.port}${firstId?`/p/${firstId}/${a.follow?"setup":"board"}`:"/"}?t=${token}`,rows2=server.projects.summaries();banner(),divider(a.title),summaryBox(` ${a.title} `,[...a.first?[{key:"project",value:resolve(a.first.root)}]:[],{key:"projects",value:String(rows2.filter(r=>r.open).length)},{key:"url",value:url},{key:"token",value:`${token.slice(0,8)}\u2026`},{key:"port",value:String(server.port)}]),server.port!==requestedPort&&warning(`port ${requestedPort} was busy \u2014 bound to ${server.port} instead`);for(let r of rows2.filter(x=>!x.open))warning(`${r.name} did not open: ${r.why}`);if(blank(),step("Next:"),info("1) Work from the board, or from this terminal: try `squad inbox` in a second tab."),info("2) Every button on the board shows its terminal command."),info("3) Add more projects from Home, or run `squad console` in their folder."),info("4) Press Ctrl-C here to stop squad-kit when you're done."),opts.open!==!1)try{await open2(url)}catch{warning("Could not open a browser automatically. Copy the URL above into your browser.")}else info("Browser auto-open disabled (--no-open). Copy the URL above into your browser.");let stopFollow=()=>{};if(a.follow&&(blank(),step("Getting to know the project:"),stopFollow=followSetup(a.follow,()=>stopFollow())),await server.done,stopFollow(),a.demo||writeAppInfo({...info2,pid:null}),server.restartRequested()){info("Restarting the board\u2026");let removed=!!(a.first&&!a.demo&&!server.projects.first);await restartBoard(a.underKeeper,removed?APP_COMMAND:a.restartWith,removed);return}await endRuns(),info("console stopped")}var RESTART_WAIT_MS=1e4,ASKED_MS=2e3;async function endRuns(waitMs=RESTART_WAIT_MS){return await allEnded(waitMs)||(askAllToEnd(),await allEnded(ASKED_MS))?!0:(killAll(),allEnded(1e3))}async function restartBoard(underKeeper,command,asApp=!1){let argsFor=c=>[...process.execArgv,process.argv[1],...c],args=argsFor(command);for(await endRuns(),await new Promise(done2=>process.stdout.write("",()=>done2())),typeof process.execve=="function"&&process.platform!=="win32"&&process.execve(process.execPath,[process.execPath,...args],process.env),underKeeper&&process.exit(asApp?RESTART_APP_EXIT:RESTART_EXIT),process.on("SIGINT",()=>{});;){let child=spawn17(process.execPath,args,{stdio:"inherit",env:{...process.env,[KEEPER_ENV]:"1"}}),forward=()=>child.kill("SIGTERM");process.on("SIGTERM",forward);let code=await new Promise(done2=>child.on("exit",c=>done2(c??0)));if(process.off("SIGTERM",forward),code===RESTART_APP_EXIT)args=argsFor(APP_COMMAND);else if(code!==RESTART_EXIT){process.exitCode=code;return}}}import fs168 from"fs";import os29 from"os";import path159 from"path";import kleur10 from"kleur";var out5=(s="")=>process.stdout.write(`${s}
2042
+ `),home=p=>p.startsWith(os29.homedir())?`~${p.slice(os29.homedir().length)}`:p;function stateOf3(root){if(!fs168.existsSync(path159.join(root,SQUAD_DIR,"config.yaml")))return kleur10.red(fs168.existsSync(root)?"its squad-kit files are gone":"the folder is not there any more");let bp=boardPaths(path159.join(root,SQUAD_DIR));if(!boardExists(bp))return kleur10.gray("no board yet: run squad console in its folder");let meta=readMeta(bp),setup=setupView(bp);if(setup&&!setup.approved){let line3=setupLine(setup);return setup.steps.some(s=>s.state==="failed"&&s.id==="read")?kleur10.red(line3):kleur10.yellow(line3)}if(meta.engine==="live"&&meta.scan&&meta.scan.status!=="done")return meta.scan.status==="failed"?kleur10.red("the Architect could not read it"):kleur10.yellow("setting up: the Architect reads it");let open3=listTickets(bp).filter(t=>t.stage!=="done").length,waiting2=inbox(bp).length;return`${kleur10.gray(`${open3} open`)}${waiting2?kleur10.yellow(` \xB7 ${waiting2} waiting for you`):""}`}async function newProject(name,opts){if(opts.brd&&!fs168.existsSync(opts.brd))throw new Error(`There is no file at ${opts.brd}.`);let input7={name,...opts.where?{where:path159.resolve(opts.where)}:{},...opts.idea?{idea:opts.idea}:{},...opts.brd?{file:{name:path159.basename(opts.brd),data:fs168.readFileSync(opts.brd).toString("base64")}}:{}},app=runningApp(),root;if(app){let res=await fetch(`http://127.0.0.1:${app.port}/api/v1/app/projects/new`,{method:"POST",headers:{authorization:`Bearer ${app.token}`,"content-type":"application/json"},body:JSON.stringify(input7)}),made=await res.json();if(!res.ok||!made.root)throw new Error(made.error??"squad-kit could not make the project.");root=made.root}else{let made=makeNewProject(input7);rememberProject(made.bp.projectId,made.bp.root),root=made.bp.root}success(`${path159.basename(root)} is ready in ${home(root)}.`),out5(opts.idea?" The Architect reads what you want to build. Follow it in its folder with: squad setup --follow":" The Analyst reads your BRD. Follow it in its folder with: squad requirements"),app||out5(" Its squad starts when you open squad-kit: squad")}async function runProjects(opts){if(opts.new!==void 0)return newProject(opts.new,opts);let app=runningApp();if(opts.remove){let root=readProjects().find(p=>!fs168.existsSync(p.root)&&(p.id===opts.remove||p.root===path159.resolve(opts.remove)||path159.basename(p.root).toLowerCase()===opts.remove.toLowerCase()))?.root??findProject(opts.remove),entry=readProjects().find(p=>p.root===root);if(!entry)throw new Error(`${projectName(root)} is not in squad-kit.`);app?(await fetch(`http://127.0.0.1:${app.port}/api/v1/app/projects/${encodeURIComponent(entry.id)}`,{method:"DELETE",headers:{authorization:`Bearer ${app.token}`}}).catch(()=>null))?.ok||forgetProject(entry.id):forgetProject(entry.id),success(`${projectName(root)} is out of squad-kit. Its files stay in ${home(root)}. Add it again any time: squad console in its folder.`);return}let list6=readProjects();if(out5(),out5(` ${kleur10.bold("Your projects")} ${kleur10.gray(`(${list6.length})`)}${app?kleur10.gray(` squad-kit is open: http://127.0.0.1:${app.port}/?t=${app.token}`):""}`),out5(),!list6.length){out5(kleur10.gray(" No projects yet. Open a terminal in a project folder (or a new, empty one) and run: squad console")),out5();return}let names2=list6.map(p=>projectName(p.root)),w=Math.max(...names2.map(n=>n.length));list6.forEach((p,i)=>out5(` ${kleur10.bold(names2[i].padEnd(w))} ${kleur10.gray(home(p.root))} ${stateOf3(p.root)}`)),out5(),out5(kleur10.gray(app?" Add one: squad console in its folder, or Add project on Home.":" Open them all: squad")),out5()}import kleur11 from"kleur";var out6=(s="")=>process.stdout.write(`${s}
2043
2043
  `),cmd5=s=>kleur11.green(s);function whereOf(p){if(p.shared)return"shared by the parts";let repo=p.repo==="."?"the project folder":p.repo??"";return p.folder?`${repo}/${p.folder}`:repo}function printParts(parts2){out6(),out6(` ${kleur11.bold("What the product is made of")}`),out6();let wide=Math.max(...parts2.map(p=>p.id.length));for(let p of parts2){let needs=p.needs.map(n=>parts2.find(x=>x.id===n)?.name??n).join(", ");out6(` ${kleur11.cyan(p.id.padEnd(wide))} ${kleur11.bold(p.name)}${p.off?kleur11.yellow(" not used"):p.noRun?kleur11.gray(" nothing starts here"):""}`),out6(` ${" ".repeat(wide)} ${kleur11.gray([whereOf(p),p.url,needs?`needs ${needs}`:""].filter(Boolean).join(" \xB7 "))}`)}out6(),info(`Correct one: ${cmd5("squad parts --set api --needs db --url http://localhost:5180")} Not used: ${cmd5("squad parts --off <part>")}`)}async function runParts(opts){let bp=board3();if(opts.atOnce!==void 0){let n=Number(opts.atOnce);if(opts.atOnce!=="all"&&!(Number.isInteger(n)&&n>=1&&n<=5))throw new Error("Say a number from 1 to 5, or all for the limit every project shares.");let{atOnce:_old,...rest}=readMeta(bp);writeMeta(bp,opts.atOnce==="all"?rest:{...rest,atOnce:n}),success(opts.atOnce==="all"?"This project follows the limit every project shares.":`${n===1?"One story":`${n} stories`} of this project run at once.`);return}if(opts.delivery!==void 0){if(!["pr","push","auto"].includes(opts.delivery))throw new Error("Say pr, push, or auto: a pull request when a code host is signed in.");let{delivery:_old,...rest}=readMeta(bp);writeMeta(bp,opts.delivery==="auto"?rest:{...rest,delivery:opts.delivery}),success(opts.delivery==="push"?"Finished work goes out pushed to the branch, as one plain commit per story, with no pull request.":opts.delivery==="pr"?"Finished work goes out by pull request.":"Finished work goes out by pull request when a code host is signed in.");return}let change={...opts.name!==void 0?{name:opts.name}:{},...opts.shared?{shared:!0}:{},...opts.repo!==void 0?{repo:opts.repo}:{},...opts.folder!==void 0?{folder:opts.folder}:{},...opts.needs!==void 0?{needs:opts.needs.split(",").map(n=>n.trim()).filter(Boolean)}:{},...opts.url!==void 0?{url:opts.url}:{}};opts.add!==void 0?success(`Added ${addPart(bp,{...change,name:opts.add}).name}.`):opts.set!==void 0?success(`Saved ${changePart(bp,opts.set,change).name}.`):opts.off!==void 0?success(`${changePart(bp,opts.off,{off:!0}).name} is marked Not used. No story starts it.`):opts.on!==void 0?success(`${changePart(bp,opts.on,{off:!1}).name} is in use again.`):opts.remove!==void 0?(removePart(bp,opts.remove),success("Taken out.")):printParts(partsOf2(readMeta(bp)))}import kleur12 from"kleur";var out7=(s="")=>process.stdout.write(`${s}
2044
2044
  `),cmd6=s=>kleur12.green(s);async function runFiles(opts){let bp=board3();if(opts.local!==void 0){let f=addLocalFile(bp,opts.repo,opts.local);success(`${f.path} is a local file. No agent can read it.`),info(`Stories need their own values for it. Use test values: a test could read and send them. Start from yours: ${cmd6(`squad files --use-mine ${f.path}`)}`)}else if(opts.lock!==void 0)success(`${addLockedFile(bp,opts.repo,opts.lock).path} is locked. Agents can read it and never change it.`);else if(opts.lockDocker)success(`Locked ${lockDockerFiles(bp).map(f=>f.path).join(", ")}.`);else if(opts.useMine!==void 0)success(`Stories now use a copy of your ${useMyFile(bp,opts.repo,opts.useMine).path}. Change any real values in it to test values: ${projectFilesView(bp).local.find(f=>f.path===opts.useMine)?.storyFile??""}`);else if(opts.remove!==void 0)success(`${removeProjectFile(bp,opts.repo,opts.remove).path} is off the list.`);else{let v=projectFilesView(bp),shown2=(repo,file3)=>v.repos.length>1?`${repo}/${file3}`:file3;out7(),out7(` ${kleur12.bold("Local files")} ${kleur12.gray("git does not carry them, and no agent can read them")}`),v.local.length||out7(kleur12.gray(" None"));for(let f of v.local)out7(` ${shown2(f.repo,f.path)} ${f.ready?kleur12.green("ready"):kleur12.yellow("no values for stories yet")}${f.ready?kleur12.gray(` ${f.storyFile}`):""}`);out7(),out7(` ${kleur12.bold("Locked files")} ${kleur12.gray("agents can read them and never change them")}`),v.locked.length||out7(kleur12.gray(" None"));for(let f of v.locked)out7(` ${shown2(f.repo,f.path)}`);out7(),v.docker.length&&info(`${v.docker.length} Docker ${v.docker.length===1?"file is":"files are"} not locked: ${cmd6("squad files --lock-docker")}`),info(`Add one: ${cmd6("squad files --local .env.dev")} ${cmd6("squad files --lock docker-compose.yml")}`)}}import kleur13 from"kleur";var out8=(s="")=>process.stdout.write(`${s}
2045
2045
  `),cmd7=s=>kleur13.green(s),row2=(label3,value)=>out8(` ${kleur13.bold(label3.padEnd(14))}${value}`),when=iso2=>iso2.slice(0,16).replace("T"," ");function settingWords(s){return s.key&&s.file?`${s.key} in ${s.file}${s.env?kleur13.gray(` (or ${s.env} in the environment)`):""}`:s.env?`${s.env}${s.file?` in ${s.file}`:""}`:kleur13.yellow("not known yet")}function size(bytes){return bytes<1024?`${bytes} bytes`:bytes<1024*1024?`${(bytes/1024).toFixed(1)} KB`:`${(bytes/1024/1024).toFixed(1)} MB`}async function runDb(action,opts={}){let bp=board3(),what=(action??"show").trim().toLowerCase();if(what==="find"){let r=learnDatabase(bp,{again:!0});r.later?info(r.later):r.database?success(r.changed?"Found the database again. An engineer confirms it before stories get their own copy.":"Found the same database again."):info("squad-kit found no database in the project's files. Name it yourself: squad db confirm --kind postgres --env DATABASE_URL --address \u2026")}else if(what==="confirm"){let change=Object.fromEntries(Object.entries(opts).filter(([k,v])=>["kind","name","env","file","key","address","repo"].includes(k)&&typeof v=="string")),db=confirmDatabase(bp,readMeta(bp).user.name,change),blocked=databaseView(bp).blocked;blocked?success(`Confirmed: ${db.name??"the database"}. Stories use it as before, though: ${blocked}`):success(`Confirmed: ${db.name??"the database"}. Stories get their own copy once there is a template: ${cmd7("squad db template")}`)}else if(what==="template"){out8(" Making the template. It only reads your database.");let info2=await makeDbTemplate(bp,readMeta(bp).user.name),sampled=info2.tables.filter(t=>t.kept==="sampled").length;success(`Made the template of ${info2.sourceName}: ${size(info2.bytes)}, ${info2.tables.length} tables${sampled?`, ${sampled} of them sampled`:""}.`);for(let n of info2.notes)out8(` ${kleur13.gray(n)}`)}else if(what!=="show")throw new Error(`"${action}" is not something squad db does. Use show, find, confirm or template.`);show(bp)}function show(bp){let v=databaseView(bp);out8();let db=v.database;if(!db){out8(` ${v.words}`),out8(),info(`Look again: ${cmd7("squad db find")} Name it yourself: ${cmd7("squad db confirm --kind postgres --env DATABASE_URL --address \u2026")}`);return}if(row2("Database",`${db.kindName}${db.name?` ${db.name}`:""} ${db.confirmed?kleur13.green(`confirmed by ${db.confirmed.by}, ${when(db.confirmed.at)}`):kleur13.yellow("not confirmed yet")}`),db.where&&row2("Where",db.where),row2("Setting",settingWords(db.setting)),row2("Address",db.address?`${db.address}${db.secret?kleur13.gray(" (on this computer only)"):""}`:kleur13.yellow("not known yet")),db.from?.length&&row2("Learned from",db.from.join(", ")),v.blocked&&row2("Own copies",kleur13.yellow(v.blocked)),v.making)row2("Template",kleur13.yellow(`being made, since ${when(v.making.since)}`));else if(v.template){let sampled=v.template.tables.filter(t=>t.kept==="sampled").length;row2("Template",`${size(v.template.bytes)}, made ${when(v.template.madeAt)} \xB7 ${v.template.tables.length} tables${sampled?`, ${sampled} sampled`:""}${v.template.hidePersonal?", personal data hidden":""}`)}else row2("Template",kleur13.yellow("none yet"));v.failed&&row2("",kleur13.yellow(`The last one was not made: ${v.failed.text}`)),row2("Hide personal",db.hidePersonal?"on":"off"),row2("Sample size",db.sampleRows?`${db.sampleRows} rows`:"the default (1000 rows)"),v.missing.length&&row2("Tools",kleur13.yellow(`this computer has no ${v.missing.join(", ")}`)),out8(),out8(` ${kleur13.bold("Story databases now")}`),v.stories.length||out8(kleur13.gray(" None"));for(let s of v.stories)out8(` ${s.story} ${s.name} ${kleur13.gray(when(s.at))}`);if(v.notes.length){out8(),out8(` ${kleur13.bold("Notes for engineers")}`);for(let n of v.notes.slice(0,5))out8(` ${kleur13.gray(when(n.at))} ${n.text}`)}out8(),info(`${db.confirmed?"":`Confirm it: ${cmd7("squad db confirm")} `}${v.template?"Make it again":"Make the template"}: ${cmd7("squad db template")} Look again: ${cmd7("squad db find")}`)}async function runDbHide(setting){let bp=board3();if(setting===void 0){let on=!!readMeta(bp).database?.hidePersonal;out8(` ${kleur13.bold("Hide personal data in the database template")} ${on?"on":"off"}`),out8(` ${kleur13.gray("On: names, ID numbers, phones, emails and addresses become fake, by column or field name.")}`),info(`Change it: ${cmd7(`squad config set db-hide ${on?"off":"on"}`)}`);return}let s=setting.trim().toLowerCase();if(!["on","off"].includes(s))throw new Error(`"${setting}" is not a setting here. Use on or off.`);setDatabaseOptions(bp,{hidePersonal:s==="on"}),success(`Personal data is ${s==="on"?"hidden":"kept as it is"} in the next template. Make it again: ${cmd7("squad db template")}`)}async function runDbSample(rows2){let bp=board3();if(rows2===void 0){let n=readMeta(bp).database?.sampleRows;out8(` ${kleur13.bold("Rows the template keeps of a big table")} ${n||"the default (1000)"}`),info(`Change it: ${cmd7("squad config set db-sample 5000")} Back to the default: ${cmd7("squad config set db-sample default")}`);return}let back=rows2.trim().toLowerCase()==="default";setDatabaseOptions(bp,{sampleRows:back?null:Number(rows2)}),success(`${back?"The default sample size":`${Number(rows2)} rows`} from the next template. Make it again: ${cmd7("squad db template")}`)}import kleur15 from"kleur";import{confirm as confirm12}from"@inquirer/prompts";import kleur14 from"kleur";var out9=(s="")=>process.stdout.write(`${s}
2046
2046
  `),cmd8=s=>kleur14.green(s),sleep11=ms2=>new Promise(r=>setTimeout(r,ms2));function board4(){let{bp}=openBoard(buildPaths(requireSquadRoot()).squadDir);if(!boardExists(bp))throw new Error("This project has no board yet. Run: squad console");return bp}function boardApi(bp){let c=runningConsole(bp);if(!c?.port||!c.token)return null;let base2=`http://127.0.0.1:${c.port}/p/${encodeURIComponent(bp.projectId)}/api/v1`;return{async call(method,path165,payload){let res=await fetch(`${base2}${path165}`,{method,headers:{authorization:`Bearer ${c.token}`,"content-type":"application/json"},...payload!==void 0?{body:JSON.stringify(payload)}:{}}).catch(()=>null);if(!res)throw new Error(`squad-kit runs (process ${c.pid}) but does not answer on port ${c.port}. Restart it: squad console --restart`);let data=await res.json().catch(()=>({}));if(!res.ok)throw new Error(data.error??`squad-kit answered ${res.status}. It may run an older version: squad console --restart`);return data}}}function refOf(bp,name){return readMeta(bp).repos?.length?` ${shellArg(name)}`:""}var at2=url=>(url??"").replace(/^https?:\/\//,"");function minutes3(iso2){if(!iso2)return"";let m=Math.round((Date.now()-Date.parse(iso2))/6e4);return m<1?"just started":`for ${m} min`}function stateLine(s){switch(s.state){case"running":return kleur14.green(`Running at ${at2(s.address)}`)+kleur14.gray(`, ${minutes3(s.since)}${s.elsewhere?", started outside the board":""}`);case"starting":return kleur14.yellow("Starting\u2026");case"stopped":return"Stopped";case"failed":return kleur14.red(`Failed: ${s.error??"it did not start"}`);default:return s.up?`Not running ${kleur14.gray(`(something else answers at ${at2(s.url)})`)}`:"Not running"}}function stepLine(s){return s.step==="phone"?s.phone?.run?.line??"Getting the app onto squad-kit's iPhone":s.step==="copy"?"Making a copy of the code. On a big project this takes a while.":s.step==="setup"?`Setting it up first: ${s.setup}`:s.step==="start"?`Starting: ${s.start}`:`Getting the newest ${s.base}`}function busyLines(s,ref2){let b=s.busy;failure(s.error??"Another program is in the way."),s.detail&&out9(kleur14.gray(` ${s.detail}`)),b.ours?out9(` squad-kit started it. Stop it and start ${s.name}: ${cmd8(`squad app start${ref2} --stop-other`)}`):(out9(` Close that program, then start again: ${cmd8(`squad app start${ref2}`)}`),out9(` Or give ${s.name} another address: ${cmd8(`squad app set${ref2} --url http://localhost:${b.port+1}`)}`))}function report(bp,s,lines3=[]){let ref2=refOf(bp,s.name);if(s.state==="running"&&s.phone?.run?.state==="running")return success(`${s.name} is open on squad-kit's ${s.phone.run.device??"iPhone"}, from squad-kit's copy of ${s.base}. Close its window, or stop it: ${cmd8(`squad app stop${ref2}`)}`),!0;if(s.state==="stopped"&&s.phone?.run?.ended)return info(s.phone.run.ended),!1;if(s.state==="running")return success(`${s.name} runs at ${s.address}, from squad-kit's copy of ${s.base}.`),s.includes?.length&&out9(` Includes ${s.includes.map(t=>`${t.id} ${t.title}`).join(", ")}`),out9(` Open it: ${kleur14.bold(s.address??"")}`),out9(` Stop it: ${cmd8(`squad app stop${ref2}`)}`),!0;if(s.busy)return busyLines(s,ref2),!1;if(s.state==="stopped")return info(`${s.name} was stopped before it started.`),!1;failure(`${s.name} did not start: ${s.error??"it stopped"}`);let last=lines3.filter(l=>l.trim()).slice(-12);if(s.detail)for(let l of s.detail.split(`
2047
2047
  `).slice(-12))out9(kleur14.gray(` ${l}`));else if(last.length){out9(kleur14.gray(" The last lines it printed:"));for(let l of last)out9(kleur14.gray(` ${l}`))}return out9(` Try again: ${cmd8(`squad app start${ref2}`)}`),!1}async function follow(bp,api,name,first2){let said="",s=first2,enc2=encodeURIComponent(name);for(;s.state==="starting";){let line3=stepLine(s);line3!==said&&step(line3),said=line3,await sleep11(500),s=(await api.call("GET","/apps")).apps.find(a=>a.name===name)??s}let lines3=s.state==="running"?[]:(await api.call("GET",`/apps/${enc2}/output`)).lines;report(bp,s,lines3)||(process.exitCode=1)}async function runHere(bp,name,opts,again2){let runner4=new AppRunner(bp,{echo:text3=>process.stdout.write(kleur14.gray(text3))});info("squad-kit is not open, so the app runs here until you press Ctrl-C. With squad-kit open, it runs from the board.");let asked,stopAsked=new Promise(r=>asked=r);process.once("SIGINT",()=>asked()),process.once("SIGTERM",()=>asked());let first2=again2?await runner4.restart(name,opts):await runner4.startAs(name,opts);step(stepLine(first2));let s=await Promise.race([runner4.settled(name),stopAsked.then(()=>null)]);if(!s){await runner4.stop(name).catch(()=>{}),info(`Stopped ${name}.`);return}if(!report(bp,s,runner4.lines(name))){process.exitCode=1;return}if(info("Press Ctrl-C to stop it."),await Promise.race([stopAsked.then(()=>"stop"),runner4.exited(name).then(()=>"ended")])==="stop"){await runner4.stop(name).catch(()=>{}),info(`Stopped ${name}.`);return}let after=await runner4.statusOf(name);failure(after.error??`${name} stopped by itself.`),process.exitCode=1}async function startOrRestart(name,opts,again2){let bp=board4(),app=findApp(readMeta(bp),name),api=boardApi(bp),body10={nextPort:!!opts.nextPort,stopOther:!!opts.stopOther,...opts.phone?{phone:!0}:{},...opts.web?{web:!0}:{},...opts.language?{language:opts.language}:{}};if(!api)return runHere(bp,app.name,body10,again2);let first2=await api.call("POST",`/apps/${encodeURIComponent(app.name)}/${again2?"restart":"start"}`,body10);await follow(bp,api,app.name,first2)}async function runAppStart(name,opts){await startOrRestart(name,opts,!1)}async function runAppRestart(name,opts){await startOrRestart(name,opts,!0)}async function runAppStop(name){let bp=board4(),app=findApp(readMeta(bp),name),api=boardApi(bp);api?await api.call("POST",`/apps/${encodeURIComponent(app.name)}/stop`):await new AppRunner(bp).stop(app.name),success(`Stopped ${app.name}.`)}async function runAppStatus(){let bp=board4(),api=boardApi(bp),apps2=api?(await api.call("GET","/apps")).apps:await new AppRunner(bp).status();if(out9(),out9(` ${kleur14.bold("Your apps")}`),out9(),!apps2.length){out9(kleur14.gray(" This project has no app to run yet.")),out9();return}let w=Math.max(...apps2.map(a=>a.name.length)),pad2=" ".repeat(w+7);for(let a of apps2){let ref2=refOf(bp,a.name),dot=a.state==="running"?kleur14.green("\u25CF"):a.state==="failed"?kleur14.red("\u25CF"):a.state==="starting"?kleur14.yellow("\u25CF"):kleur14.gray("\u25CB");out9(` ${dot} ${kleur14.bold(a.name.padEnd(w))} ${stateLine(a)}`),a.start?a.state==="running"?(a.includes?.length&&out9(`${pad2}${kleur14.gray(`Includes ${a.includes.map(t=>`${t.id} ${t.title}`).join(", ")}`)}`),out9(`${pad2}Stop: ${cmd8(`squad app stop${ref2}`)} Restart: ${cmd8(`squad app restart${ref2}`)}`)):a.state!=="starting"&&out9(`${pad2}Start: ${cmd8(`squad app start${ref2}`)}`):out9(`${pad2}Start command unknown. Set it: ${cmd8(`squad app set${ref2} --start "npm run dev" --url http://localhost:3000`)} or ask the Architect: ${cmd8("squad app find")}`),a.fresh&&out9(`${pad2}${kleur14.green(`${a.fresh.id} is in ${a.fresh.base}`)} \xB7 Run the new version: ${cmd8(`squad app restart${ref2}`)}`)}out9(),api||out9(kleur14.gray(" squad-kit is not open. Apps run while it is open (squad console), or here with squad app start.")),api||out9()}async function runAppSet(name,opts){let bp=board4(),meta=readMeta(bp),app=findApp(meta,name),api=boardApi(bp);api?await api.call("PUT",`/apps/${encodeURIComponent(app.name)}/commands`,{start:opts.start,url:opts.url}):writeMeta(bp,withAppCommands(meta,app.repo,opts)),success(`Saved how ${app.name} starts, in the project's settings.`),out9(` Start it: ${cmd8(`squad app start${refOf(bp,app.name)}`)}`)}async function runAppFind(){let bp=board4(),meta=readMeta(bp);if(meta.engine!=="live")throw new Error('Only a live board reads the project. Set the start command yourself: squad app set --start "npm run dev"');let api=boardApi(bp);api?await api.call("POST","/scan"):writeMeta(bp,{...meta,scan:{status:"pending"}}),success(api?"The Architect is reading the project to find how to start it.":"The Architect reads the project as soon as squad-kit is open: squad console"),out9(` Then see what it found: ${cmd8("squad app status")}`)}var out10=(s="")=>process.stdout.write(`${s}
2048
- `),cmd9=s=>kleur15.green(s),sleep12=ms2=>new Promise(r=>setTimeout(r,ms2)),PRIORITY_WORDS={required:"Required",recommended:"Recommended",optional:"Optional"},left=s=>s>=90?`about ${Math.round(s/60)} min left`:`about ${s} s left`;function progress(g){if(g.step==="update")return`${(g.words??"updating").toLowerCase()}, ${left(g.secondsLeft)}`;let pct2=g.total?` ${Math.min(99,Math.floor(g.done/g.total*100))}%,`:"";return g.step==="unpack"?`unpacking, ${left(g.secondsLeft)}`:`downloading,${pct2} ${left(g.secondsLeft)}`}function stateWords(l){switch(l.state){case"ready":return kleur15.green(`Ready${l.version?`, ${l.version}`:""}`);case"too-old":return kleur15.yellow(`Too old: found ${l.version??"an old version"}, needs ${l.needs} or newer${l.why?` (${l.why})`:""}`);case"getting":return kleur15.cyan(`Getting it now${l.getting?`: ${progress(l.getting)}`:""}`);case"updating":return kleur15.cyan(`Updating it now${l.getting?`: ${progress(l.getting)}`:""}`);case"not-here":return kleur15.gray(`Not on this computer: it ${l.notHere??"needs another computer"}`);default:return kleur15.red("Missing")}}var MARK3={ready:kleur15.green("\u2713"),"too-old":kleur15.yellow("!"),getting:kleur15.cyan("\u2193"),updating:kleur15.cyan("\u2191"),"not-here":kleur15.gray("\u2013"),missing:kleur15.red("\u2717")};function printTools(v){out10(v.summary),out10(kleur15.gray(v.checkedAt?`Checked ${v.checkedAt.slice(0,16).replace("T"," ")} on this ${v.system==="mac"?"Mac":"computer"}.`:"Not checked yet."));for(let p of["required","recommended","optional"]){let lines3=v.lines.filter(l=>l.priority===p);if(lines3.length){out10(),out10(kleur15.bold(PRIORITY_WORDS[p]));for(let l of lines3)if(out10(` ${MARK3[l.state]} ${kleur15.bold(l.name)} ${stateWords(l)}`),out10(kleur15.gray(` ${l.gives}. Used for: ${l.usedFor}`)),l.blocks.length&&out10(kleur15.yellow(` Stops now: ${l.blocks.map(a=>ACTION_WORDS[a]).join(", ")}. Everything else keeps working.`)),l.note&&out10(` ${l.note}`),!(l.state==="ready"||l.state==="not-here"||l.state==="updating")){if(l.trouble&&out10(kleur15.red(` ${l.trouble}`)),l.installer==="squad-kit"){l.state!=="getting"&&out10(` squad-kit gets it: ${cmd9(l.command)}`);continue}l.update?(out10(` squad-kit can update it: ${cmd9(l.update.command)}`),out10(kleur15.gray(` ${l.update.warning}`)),out10(" Or do it yourself:")):l.cantUpdate?out10(` ${l.cantUpdate}`):l.reason&&out10(` You install it: squad-kit can't, as ${l.reason}.`),l.steps.forEach((s,i)=>out10(` ${i+1}. ${s}`)),out10(` Official page: ${kleur15.underline(l.link)}`)}}}out10(),out10(`After you install something: ${cmd9("squad tools check")}`)}async function follow2(read7,id2,again2=`squad tools get ${id2}`){let updating=again2.includes(" update "),last="";for(;;){let v=await read7(),l=v.lines.find(x=>x.id===id2),g=v.getting;if(g){let words2=`${g.step==="update"?"Updating":"Getting"} ${g.name}: ${progress(g)}`;words2!==last&&step(words2),last=words2,await sleep12(1e3);continue}let trouble=l?.trouble??(updating?void 0:v.lines.find(x=>x.trouble)?.trouble);if(l?.state==="ready"&&!trouble)return success(`${l.name} ${l.version??""} is ready.`.replace(" .",".")),!0;if(v.checking){await sleep12(500);continue}return!trouble&&l?.state==="too-old"?failure(`${l.name} ${l.version??""} is still too old: it needs ${l.needs} or newer. The steps: ${cmd9("squad tools")}`.replace(" "," ")):failure(trouble??`squad-kit could not ${updating?"update":"get"} it. Try again: ${cmd9(again2)}`),!1}}var askHere=message=>confirm12({message,default:!1});async function toolsWith(bp,api,action,tool5,opts={}){let what=(action??"show").trim().toLowerCase(),read7=()=>api?api.call("GET","/tools"):projectTools(bp);if(what==="show"||what==="list")return printTools(await read7()),!0;if(what==="check")return step("Checking this computer's tools\u2026"),api?printTools(await api.call("POST","/tools/check")):(await checkTools(),printTools(await projectTools(bp))),!0;if(what==="get"){if(!tool5)throw new Error("Say which tool, for example: squad tools get maestro");let id2=tool5.trim().toLowerCase();return api?(await api.call("POST",`/tools/${encodeURIComponent(id2)}/get`),follow2(read7,id2)):(getToolNow(id2),info("squad-kit is not open, so the download runs here. Keep this terminal open until it is done."),await follow2(async()=>{let v=await projectTools(bp);return{...v,getting:gettingNow(),lines:v.lines.map(l=>({...l,...lastTrouble(l.id)?{trouble:lastTrouble(l.id)}:{}}))}},id2))}if(what==="update"){if(!tool5)throw new Error("Say which tool, for example: squad tools update flutter");let id2=tool5.trim().toLowerCase(),l=(await read7()).lines.find(x=>x.id===id2),ok2=!!opts.yes;if(l?.update&&(out10(kleur15.yellow(l.update.warning)),!ok2)){if(!opts.ask&&!isInteractive())throw new Error(`squad-kit updates ${l.name} only after you say OK. To say it here: squad tools update ${id2} --yes`);if(ok2=await(opts.ask??askHere)(`Update ${l.name} for all projects?`),!ok2)return info(`Not now. ${l.name} stays as it is.`),!0}return api?(await api.call("POST",`/tools/${encodeURIComponent(id2)}/update`,{ok:ok2}),follow2(read7,id2,`squad tools update ${id2}`)):(await updateToolNow(bp,id2,{ok:ok2}),info("squad-kit is not open, so the update runs here. Keep this terminal open until it is done."),follow2(async()=>{let v=await projectTools(bp);return{...v,getting:gettingNow(),lines:v.lines.map(x=>({...x,...lastTrouble(x.id)?{trouble:lastTrouble(x.id)}:{}}))}},id2,`squad tools update ${id2}`))}throw new Error(`squad tools ${what}: there is no such action. Use squad tools, squad tools check, squad tools get <tool>, or squad tools update <tool>.`)}async function runTools(action,tool5,opts={}){let{bp}=openBoard(buildPaths(requireSquadRoot()).squadDir);await toolsWith(bp,boardApi(bp),action,tool5,{yes:!!opts.yes})||(process.exitCode=1)}import kleur16 from"kleur";var out11=(s="")=>process.stdout.write(`${s}
2048
+ `),cmd9=s=>kleur15.green(s),sleep12=ms2=>new Promise(r=>setTimeout(r,ms2)),PRIORITY_WORDS={required:"Required",recommended:"Recommended",optional:"Optional"},left=s=>s>=90?`about ${Math.round(s/60)} min left`:`about ${s} s left`;function progress(g){if(g.step==="update")return`${(g.words??"updating").toLowerCase()}, ${left(g.secondsLeft)}`;let pct2=g.total?` ${Math.min(99,Math.floor(g.done/g.total*100))}%,`:"";return g.step==="unpack"?`unpacking, ${left(g.secondsLeft)}`:`downloading,${pct2} ${left(g.secondsLeft)}`}function stateWords(l){switch(l.state){case"ready":return kleur15.green(`Ready${l.version?`, ${l.version}`:""}`);case"too-old":return kleur15.yellow(`Too old: found ${l.version??"an old version"}, needs ${l.needs} or newer${l.why?` (${l.why})`:""}`);case"getting":return kleur15.cyan(`Getting it now${l.getting?`: ${progress(l.getting)}`:""}`);case"updating":return kleur15.cyan(`Updating it now${l.getting?`: ${progress(l.getting)}`:""}`);case"not-here":return kleur15.gray(`Not on this computer: it ${l.notHere??"needs another computer"}`);default:return kleur15.red("Missing")}}var MARK3={ready:kleur15.green("\u2713"),"too-old":kleur15.yellow("!"),getting:kleur15.cyan("\u2193"),updating:kleur15.cyan("\u2191"),"not-here":kleur15.gray("\u2013"),missing:kleur15.red("\u2717")};function printTools(v){out10(v.summary),out10(kleur15.gray(v.checkedAt?`Checked ${v.checkedAt.slice(0,16).replace("T"," ")} on this ${v.system==="mac"?"Mac":"computer"}.`:"Not checked yet."));for(let p of["required","recommended","optional"]){let lines3=v.lines.filter(l=>l.priority===p);if(lines3.length){out10(),out10(kleur15.bold(PRIORITY_WORDS[p]));for(let l of lines3)if(out10(` ${MARK3[l.state]} ${kleur15.bold(l.name)} ${stateWords(l)}`),out10(kleur15.gray(` ${l.gives}. Used for: ${l.usedFor}`)),l.blocks.length&&out10(kleur15.yellow(` Stops now: ${l.blocks.map(a=>ACTION_WORDS[a]).join(", ")}. Everything else keeps working.`)),l.note&&out10(` ${l.note}`),!(l.state==="ready"&&!l.recommended||l.state==="not-here"||l.state==="updating")){if(l.trouble&&out10(kleur15.red(` ${l.trouble}`)),l.installer==="squad-kit"){l.state!=="getting"&&out10(` squad-kit gets it: ${cmd9(l.command)}`);continue}l.update?(out10(` squad-kit can update it: ${cmd9(l.update.command)}`),out10(kleur15.gray(` ${l.update.warning}`)),out10(" Or do it yourself:")):l.cantUpdate?out10(` ${l.cantUpdate}`):l.reason&&out10(` You ${l.recommended?"update":"install"} it: squad-kit can't, as ${l.reason}.`),l.steps.forEach((s,i)=>out10(` ${i+1}. ${s}`)),out10(` Official page: ${kleur15.underline(l.link)}`)}}}out10(),out10(`After you install something: ${cmd9("squad tools check")}`)}async function follow2(read7,id2,again2=`squad tools get ${id2}`){let updating=again2.includes(" update "),last="";for(;;){let v=await read7(),l=v.lines.find(x=>x.id===id2),g=v.getting;if(g){let words2=`${g.step==="update"?"Updating":"Getting"} ${g.name}: ${progress(g)}`;words2!==last&&step(words2),last=words2,await sleep12(1e3);continue}let trouble=l?.trouble??(updating?void 0:v.lines.find(x=>x.trouble)?.trouble);if(l?.state==="ready"&&!trouble)return success(`${l.name} ${l.version??""} is ready.`.replace(" .",".")),!0;if(v.checking){await sleep12(500);continue}return!trouble&&l?.state==="too-old"?failure(`${l.name} ${l.version??""} is still too old: it needs ${l.needs} or newer. The steps: ${cmd9("squad tools")}`.replace(" "," ")):failure(trouble??`squad-kit could not ${updating?"update":"get"} it. Try again: ${cmd9(again2)}`),!1}}var askHere=message=>confirm12({message,default:!1});async function toolsWith(bp,api,action,tool5,opts={}){let what=(action??"show").trim().toLowerCase(),read7=()=>api?api.call("GET","/tools"):projectTools(bp);if(what==="show"||what==="list")return printTools(await read7()),!0;if(what==="check")return step("Checking this computer's tools\u2026"),api?printTools(await api.call("POST","/tools/check")):(await checkTools(),printTools(await projectTools(bp))),!0;if(what==="get"){if(!tool5)throw new Error("Say which tool, for example: squad tools get maestro");let id2=tool5.trim().toLowerCase();return api?(await api.call("POST",`/tools/${encodeURIComponent(id2)}/get`),follow2(read7,id2)):(getToolNow(id2),info("squad-kit is not open, so the download runs here. Keep this terminal open until it is done."),await follow2(async()=>{let v=await projectTools(bp);return{...v,getting:gettingNow(),lines:v.lines.map(l=>({...l,...lastTrouble(l.id)?{trouble:lastTrouble(l.id)}:{}}))}},id2))}if(what==="update"){if(!tool5)throw new Error("Say which tool, for example: squad tools update flutter");let id2=tool5.trim().toLowerCase(),l=(await read7()).lines.find(x=>x.id===id2),ok2=!!opts.yes;if(l?.update&&(out10(kleur15.yellow(l.update.warning)),!ok2)){if(!opts.ask&&!isInteractive())throw new Error(`squad-kit updates ${l.name} only after you say OK. To say it here: squad tools update ${id2} --yes`);if(ok2=await(opts.ask??askHere)(`Update ${l.name} for all projects?`),!ok2)return info(`Not now. ${l.name} stays as it is.`),!0}return api?(await api.call("POST",`/tools/${encodeURIComponent(id2)}/update`,{ok:ok2}),follow2(read7,id2,`squad tools update ${id2}`)):(await updateToolNow(bp,id2,{ok:ok2}),info("squad-kit is not open, so the update runs here. Keep this terminal open until it is done."),follow2(async()=>{let v=await projectTools(bp);return{...v,getting:gettingNow(),lines:v.lines.map(x=>({...x,...lastTrouble(x.id)?{trouble:lastTrouble(x.id)}:{}}))}},id2,`squad tools update ${id2}`))}throw new Error(`squad tools ${what}: there is no such action. Use squad tools, squad tools check, squad tools get <tool>, or squad tools update <tool>.`)}async function runTools(action,tool5,opts={}){let{bp}=openBoard(buildPaths(requireSquadRoot()).squadDir);await toolsWith(bp,boardApi(bp),action,tool5,{yes:!!opts.yes})||(process.exitCode=1)}import kleur16 from"kleur";var out11=(s="")=>process.stdout.write(`${s}
2049
2049
  `),STATE_WORDS={booted:"running",booting:"starting",shutdown:"shut down","shutting-down":"shutting down",gone:"no longer on this computer",other:"not ready"},clock3=iso2=>new Date(iso2).toTimeString().slice(0,5);function printPhones(v){if(v.notHere){out11(v.notHere);return}v.devices.length||out11("squad-kit has not made its phones on this computer yet. It makes them the first time a story's pictures need one.");for(let d of v.devices){let state=d.state==="booted"?kleur16.green(STATE_WORDS[d.state]):kleur16.gray(STATE_WORDS[d.state]??d.state);out11(`${kleur16.bold(d.name)} ${d.model}, iOS ${d.runtime} ${state}`),d.inUse?out11(" Taking pictures for a story now."):d.keptUp?out11(" Something else keeps it open, like a Simulator window, so squad-kit leaves it."):d.offAt&&out11(kleur16.gray(` Shuts down by itself at ${clock3(d.offAt)}, when unused for ${v.idleMinutes} minutes.`)),out11(kleur16.gray(` Its id: ${d.id}`))}let t=v.times;out11(),out11(kleur16.gray(`On this computer: building the app about ${t.build} s, starting the phone about ${t.boot} s, putting the app on it about ${t.install} s.`)),v.devices.some(d=>d.state==="booted")&&out11(`To shut them down now: ${kleur16.green("squad phone stop")}`)}function openApi(){let root=findSquadRoot();if(!root)return null;try{return boardApi(boardPaths(buildPaths(root).squadDir))}catch{return null}}async function phoneWith(api,action){let what=(action??"status").trim().toLowerCase();if(what==="status"||what==="show")return printPhones(api?await api.call("GET","/phone"):await phoneView()),!0;if(what==="stop"){let r=api?await api.call("POST","/phone/stop"):await stopPhoneNow();return info(r.message),printPhones(r),!0}throw new Error(`squad phone ${what}: there is no such action. Use squad phone, or squad phone stop.`)}async function runPhone(action){await phoneWith(openApi(),action)||(process.exitCode=1)}import fs169 from"fs";import path160 from"path";import kleur17 from"kleur";var out12=(s="")=>process.stdout.write(`${s}
2050
2050
  `);async function runReport(ticket,opts={}){let root=findSquadRoot();if(ticket&&!root)throw new Error("Run this in the project's folder, or add --project <name>, so squad-kit knows whose ticket it is.");let input7=root?{bp:boardPaths(path160.join(root,SQUAD_DIR)),...ticket?{ticket}:{}}:{projects:readProjects().map(p=>{let open3=fs169.existsSync(path160.join(p.root,SQUAD_DIR,"config.yaml"));return{id:p.id,name:projectName(p.root),root:p.root,open:open3,...open3?{}:{why:"its folder or its .squad/ folder is gone"}}})},r=writeReport({...input7,...opts.note?{note:opts.note}:{},...opts.out?{dir:path160.resolve(opts.out)}:{}});success(`Saved ${r.name}`),out12(` ${r.file}`),out12(),out12(" It holds:");for(let h of r.holds)out12(` ${kleur17.green("\u2713")} ${h}`);out12(kleur17.gray(` ${r.never}`)),out12(),out12(` ${kleur17.bold("Send this file to whoever helps you with squad-kit.")}`)}async function runLogs(opts={}){if(opts.tail!==void 0&&opts.tail!==!1){let n=typeof opts.tail=="string"&&Number(opts.tail)>0?Math.min(2e3,Math.floor(Number(opts.tail))):40,lines3=tailLines(n);lines3.length||info("Nothing is logged yet.");for(let l of lines3)out12(l.includes(" error ")?kleur17.red(l):l.includes(" warn ")?kleur17.yellow(l):l);return}out12(` squad-kit's own log: ${logsDir()}`),out12(` Today: ${logFile()}${fs169.existsSync(logFile())?"":kleur17.gray(" (nothing yet)")}`),out12(kleur17.gray(" One file a day, kept 14 days. Never tokens, passwords or what was sent to an agent.")),out12(kleur17.gray(` The last lines: ${kleur17.green("squad logs --tail")} Send it all with your ticket: ${kleur17.green("squad report <ticket>")}`))}async function runTryNow(){let pause2=activePause();if(!pause2){info("Nothing waits for a usage limit right now.");return}out12(` ${limitText(pause2)}.`),endPause(),success("The squad carries on now. If the limit is still there, the first step finds it and waits again.")}import kleur18 from"kleur";function howOf(opts){return{...opts.phone?{phone:!0}:{},...opts.web?{web:!0}:{},...opts.lang?{language:opts.lang}:{}}}var out13=(s="")=>process.stdout.write(`${s}
2051
2051
  `),cmd10=s=>kleur18.green(s),sleep13=ms2=>new Promise(r=>setTimeout(r,ms2));function board5(){let{bp}=openBoard(buildPaths(requireSquadRoot()).squadDir);if(!boardExists(bp))throw new Error("This project has no board yet. Run: squad console");return bp}function report2(s){if(s.state==="running"&&s.phone?.run?.state==="running")return success(`${s.ticket}'s own copy of the app is open on squad-kit's ${s.phone.run.device??"iPhone"}. Nothing is merged.`),out13(` Close its window, or stop it: ${cmd10(`squad try ${s.ticket} --stop`)}`),!0;if(s.state==="running"){if(success(`${s.ticket}'s own copy of the app runs at ${s.url}. Nothing is merged.`),s.pages?.length){out13(" Where to look:");for(let p of s.pages)out13(` ${kleur18.bold(p.url)}`)}else out13(` Open it: ${kleur18.bold(s.url??"")}`);return out13(` Stop it: ${cmd10(`squad try ${s.ticket} --stop`)}`),!0}return s.state==="failed"?(failure(`${s.error??"The app did not start."} What it printed is in the ticket's activity: ${cmd10(`squad show ${s.ticket}`)}`),out13(` Try again: ${cmd10(`squad try ${s.ticket}`)}`),!1):s.state==="starting"?(step(`${s.ticket}'s own copy of the app is starting\u2026`),!1):(s.can?info(`${s.ticket}'s copy of the app is not running. Try it yourself: ${cmd10(`squad try ${s.ticket}`)}`):info(s.why??`${s.ticket} cannot be tried now.`),s.other&&out13(` ${s.other}'s copy runs now. Starting this one stops it.`),!1)}async function follow3(api,first2){let at3=`/tickets/${encodeURIComponent(first2.ticket)}/try`,s=first2;s.state==="starting"&&step(`Starting ${s.ticket}'s own copy of the app, with the change in it\u2026`);let said="";for(;s.state==="starting";){let line3=s.phone?.run?.line;line3&&line3!==said&&step(line3),line3&&(said=line3),await sleep13(500),s=await api.call("GET",at3)}return report2(s)}async function runHere2(runner4,id2,stopAsked,how2={}){info("squad-kit is not open, so the copy runs here until you press Ctrl-C. With squad-kit open, it runs from the board.");let first2=await runner4.startAs(id2,how2);step(`Starting ${first2.ticket}'s own copy of the app, with the change in it\u2026`);let s=await Promise.race([runner4.settled(id2),stopAsked.then(()=>null)]);if(!s)return await runner4.stop(id2),info("Stopped it."),!0;if(!report2(s))return!1;info("Press Ctrl-C to stop it.");let asked=!1;for(stopAsked.then(()=>asked=!0);!asked&&runner4.status(id2).state==="running";)await sleep13(500);let after=runner4.status(id2);return await runner4.stop(id2),asked?info(`Stopped ${after.ticket}'s copy of the app.`):after.state==="failed"?failure(after.error??"The app stopped by itself."):info(`${after.ticket} moved on, so its copy of the app stopped.`),asked||after.state!=="failed"}async function tryWith(bp,api,ticket,opts={},here){let id2=requireTicket(bp,ticket).id,at3=`/tickets/${encodeURIComponent(id2)}/try`;if(opts.stop)return api?(await api.call("POST",`${at3}/stop`),success(`Stopped ${id2}'s copy of the app.`),!0):(info("squad-kit is not open, so no copy of the app runs from the board. A copy you started here with squad try stops with Ctrl-C."),!0);if(opts.status){let s=api?await api.call("GET",at3):(here?.runner??new TryRunner(bp)).status(id2);return report2(s),api||out13(kleur18.gray(" squad-kit is not open. The copy runs while it is open, or here with squad try.")),!0}if(api)return follow3(api,await api.call("POST",at3,howOf(opts)));let asked,stopAsked=here?.stopAsked??new Promise(r=>{asked=r,process.once("SIGINT",()=>asked()),process.once("SIGTERM",()=>asked())});return runHere2(here?.runner??new TryRunner(bp),id2,stopAsked,howOf(opts))}async function runTry(ticket,opts){let bp=board5();!await tryWith(bp,boardApi(bp),ticket,opts)&&!opts.stop&&!opts.status&&(process.exitCode=1)}import kleur19 from"kleur";var out14=(s="")=>process.stdout.write(`${s}