import{spawn as Z}from"child_process";import{writeFileSync as oe,mkdirSync as M,existsSync as I,readdirSync as D,readFileSync as J,unlinkSync as ae,createWriteStream as ce,statSync as le}from"fs";import{resolve as C,join as O}from"path";import{resolveMaxParallelRuns as Q}from"@zibby/core/utils/parallel-config.js";import{zibbyScratchSpecsDir as ue}from"@zibby/core/constants/zibby-scratch.js";var K="sessions",j=".zibby/output",$=process.env.ZIBBY_RUNNER_NODE_PROGRESS==="1",pe=process.env.ZIBBY_RUNNER_STATUS_STREAM==="1",X=process.env.ZIBBY_RUNNER_SPAWN_LOGS==="1",w=new Map,T=[],de=0,L=0,q=3e3;function ee(){return`run_${++de}_${Date.now().toString(36)}`}function G(s){let n=Math.floor(s/1e3);return n<60?`${n}s`:`${Math.floor(n/60)}m ${n%60}s`}function te(s){return s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g,"")}function N(s,n,e){if(!pe)return;let t=`
  ${n} [${s}] ${e}
`;try{process.stderr.write(t)}catch{}}function W(){T.length=0;for(let[,s]of w)if(s.status==="queued"&&(s.status="cancelled"),s.status==="running"&&s._child)try{s._child.kill("SIGTERM")}catch{}}process.on("exit",W);process.on("SIGINT",()=>{W(),process.exit(0)});process.on("SIGTERM",()=>{W(),process.exit(0)});var $e={id:"runner",description:"Run zibby test workflows from chat (parallel supported)",envKeys:[],promptFragment:`## Test Runner
You can run zibby test workflows directly from chat:

**CRITICAL: When user asks to test a ticket:**
1. Load the issue with your tracker tools (whatever is connected)
2. Check comments/description for test steps
3. IF steps found \u2192 Use inline format: run_test({ spec: "inline:<steps>", ticketKey: "KEY" })
4. IF NO steps AND local codebase \u2192 Use run_generate
5. Tell the user tests are running \u2014 they can ask for progress anytime

**Jira-shaped \`spec\` (e.g. SCRUM-408):** If the **jira** skill is active, the runner loads that issue (description + comments) and runs it as an inline spec. Otherwise use \`inline:\`+steps or a file path. Prefer explicit \`inline:\` when you want full control.

Tools:
- **run_test**: spec = file path, inline:+steps, or Jira KEY-123 (auto-loads issue when jira skill is on).
- **run_status**: Instant progress check (runId or "all")
- **run_generate**: Generate specs from codebase. ONLY use if NO steps in ticket AND local codebase exists.
- **run_artifacts**: Read test results/logs after completion
- **run_diagnose**: Diagnose failures
- **run_cancel**: Kill running test
- **list_specs**: List spec files

### MANDATORY: When User Asks to Test a Ticket

1. **ALWAYS load ticket first** using Jira/issue tools (jira_get_issue, jira_get_comments)
2. **Check comments AND description** for test steps
3. **If steps found** \u2192 IMMEDIATELY call run_test with inline format: run_test({ spec: "inline:Navigate to...\\nClick...\\nVerify...", ticketKey: "SCRUM-123" })
4. **DO NOT say "can't run"** until you've ACTUALLY fetched the ticket and confirmed NO steps exist
5. **If NO steps found** \u2192 Ask user to add steps OR use run_generate (only if local codebase)

### DECISION TREE: When User Says "Test This Ticket"

**STEP 1: Get full issue/ticket information**
ALWAYS use your connected issue tools to load the item and discussion (full description/body + comments). Tool names differ by integration (e.g. Jira vs GitHub vs Linear)\u2014use what you have.

**STEP 2: Check if testing steps exist**
Look for testing steps in:
- Comments (most common)
- Description field
- Keywords like "test steps", "testing steps", numbered lists (1. 2. 3.)

IF testing steps found:
  \u2192 GOTO Workflow A (Use Existing Steps)
ELSE:
  \u2192 GOTO Workflow B (Generate from Codebase)

### Workflow A: Use Existing Steps (NO CODEBASE NEEDED)
**MANDATORY when ticket has test steps in comments/description**

CRITICAL: If you see test steps in the ticket, DO NOT call run_generate. Use inline format instead.

Steps:
1. Extract steps from ticket (comments or description)
2. Format as inline spec: "inline:" + steps text
3. Call run_test({ spec: "inline:...", ticketKey: "SCRUM-123" })
4. Tell the user tests are running
5. Use run_status when they ask for progress

Example: If ticket SCRUM-408 comment has test steps, call: run_test({ spec: "inline:Navigate to URL\\nVerify checkboxes\\nCheck first\\nUncheck second", ticketKey: "SCRUM-408" })

### Workflow B: Generate from Codebase (REQUIRES CODEBASE)
**ONLY use when ALL these are true:**
- \u274C NO testing steps in ticket comments/description
- \u2705 Local codebase exists (not external URL like heroku)
- \u2705 Ticket describes NEW functionality to test

**STOP AND USE WORKFLOW A IF:**
  - Testing steps exist in ticket \u2192 NEVER call run_generate, use inline format
  - Ticket mentions external app (heroku, cloud, demo sites) \u2192 Use inline with URL
  - No local codebase \u2192 Ask user for steps

**BEFORE calling run_generate, ask yourself:**
  - Did I check ALL comments? (not just description)
  - Are there ANY step lists (numbered, bulleted)?
  - Does ticket mention external URLs?
  - If YES to any: DO NOT call run_generate!

**If checks pass:**
1. Call run_generate({ ticket: "SCRUM-123" })
   - Spawns Claude/Cursor with file access
   - Explores codebase (1-3 minutes)
   - Returns generated spec file paths
2. For EACH file: run_test({ spec: "test-specs/...", ticketKey: "SCRUM-123" })
3. Tell the user tests are running

### Example Decision Process
(Illustration uses Jira-shaped keys/tools; substitute your session's issue integration.)

**User:** "Test SCRUM-408"

Step-by-step:
1. jira_get_issue({ issueKey: "SCRUM-408" }) - get summary, description
2. jira_get_comments({ issueKey: "SCRUM-408" }) - get comments
3. Check: Comment has "Testing steps: 1. Go to /checkboxes 2. Verify..."
4. Decision: Steps exist \u2192 Use Workflow A
5. run_test({ 
     spec: "inline:Navigate to https://the-internet.herokuapp.com/checkboxes, verify two checkboxes, check first, uncheck second, verify states",
     ticketKey: "SCRUM-408"
   })

**User:** "Test SCRUM-999 (new feature)"

Step-by-step:
1. jira_get_issue({ issueKey: "SCRUM-999" })
2. jira_get_comments({ issueKey: "SCRUM-999" })
3. Check: No testing steps found in ticket
4. Check: Codebase exists (package.json, src/ in current dir)
5. Decision: No steps + codebase \u2192 Use Workflow B
6. run_generate({ ticket: "SCRUM-999" })
7. Wait for spec files...
8. run_test for each file
9. Tell the user tests are running

### After Starting Runs
- run_test starts async work \u2014 tell the user tests are running.
- Use run_status to check progress when asked.
- To poll for completion, use the general wait tool (e.g. wait 20s) then run_status. Repeat until done.
- If any run failed/error/cancelled and user asks "why", call run_diagnose.

\u26A0\uFE0F NEVER AUTO-CANCEL RUNS:
- Tests take 1-5 minutes to complete. A "running" status is NORMAL \u2014 it does NOT mean stuck.
- NEVER call run_cancel unless the USER explicitly asks you to cancel/stop.
- If a run is still "running" after polling, just TELL the user it's still in progress and WAIT.
- The workflow has multiple nodes (preflight \u2192 execute_live \u2192 generate_script). Each takes time. This is expected.
- DO NOT interpret "running" as "stuck". DO NOT cancel on your own.

### Parallelism
- Each test CASE should be its own run_test call
- Runs auto-queue past parallel.maxConcurrentRuns in .zibby.config.mjs (default 8; caps Studio Mission Control lanes too)
- If agent is Cursor and you see "Security process exited with code: 45", avoid parallel launch for that batch; run tests sequentially (one run_test + wait, then next).

### Artifacts
Each run generates:
- result.json: Pass/fail verdict
- recording.webm: Video of session
- events.json: All browser events
- raw_stream_output.txt: Agent log

Use run_artifacts({ runId, type }) and run_diagnose({ runId }) to inspect and explain failures.`,resolve(){return null},async handleToolCall(s,n,e){let t=e?.options?.workspace||process.cwd();try{switch(s){case"run_generate":return await fe(n,t);case"run_test":return await Se(n,t,e);case"run_status":return _e(n);case"run_cancel":return Ne(n);case"run_artifacts":return Oe(n,t);case"run_diagnose":return xe(n,t);case"list_specs":return Ie(n,t);default:return JSON.stringify({error:`Unknown tool: ${s}`})}}catch(i){return JSON.stringify({error:i.message})}},tools:[{name:"run_generate",description:"Generate specs from codebase. CRITICAL: DO NOT USE if ticket has test steps in comments. Only use when: (1) NO steps in ticket AND (2) local codebase exists (not external URLs). For tickets with steps, use run_test with inline format.",input_schema:{type:"object",properties:{ticket:{type:"string",description:"Jira ticket key (e.g. SCRUM-123). Auto-fetches ticket details."},description:{type:"string",description:"Ticket description text (use if no Jira key available)"},input:{type:"string",description:"Path to a file containing ticket/requirements text"},repo:{type:"string",description:"Path to the codebase (default: current directory)"},agent:{type:"string",description:"Optional agent override (cursor, gemini, claude, codex, assistant). Omit to use configured agent."},output:{type:"string",description:"Output directory for spec files (default: test-specs)"}}}},{name:"run_test",description:"Start a test (async, returns runId). spec = file path, or inline:+steps, or a Jira-shaped issue key (e.g. PROJ-123): when Jira is connected, the runner loads that issue's description+comments into an inline spec. After starting, tell the user and let them ask for progress via run_status.",input_schema:{type:"object",properties:{spec:{type:"string",description:"Workspace file path; or inline:+steps; or Jira issue key (KEY-123) to auto-fetch from Jira when the jira skill is active."},ticketKey:{type:"string",description:"Optional label (e.g. SCRUM-123). If spec is an issue key, this defaults to that key."},agent:{type:"string",description:"Optional agent override (cursor, gemini, claude, codex, assistant). Omit to use configured agent."},headless:{type:"boolean",description:"Run browser headless (default false)"},workflow:{type:"string",description:"Workflow override (e.g. quick-smoke)"}},required:["spec"]}},{name:"run_status",description:'Instant progress check \u2014 returns immediately. Use this whenever user asks about test progress. ALWAYS use runId="all".',input_schema:{type:"object",properties:{runId:{type:"string",description:'Use "all" to see all runs in this session (recommended). Or a specific run ID if known.'}},required:["runId"]}},{name:"run_cancel",description:'Cancel/kill a running test. ONLY use when the USER explicitly asks to cancel or stop a run. NEVER auto-cancel \u2014 tests take 1-5 minutes and "running" is normal. Use runId="all" to cancel all active runs.',input_schema:{type:"object",properties:{runId:{type:"string",description:'Run ID to cancel, or "all" to cancel all active runs'}},required:["runId"]}},{name:"run_artifacts",description:"Read artifacts from a test run session. Can list files, read results/events/logs, or search across all sessions.",input_schema:{type:"object",properties:{runId:{type:"string",description:"Run ID from run_test. Omit to search across all sessions."},type:{type:"string",enum:["list","result","events","log","search"],description:'What to retrieve: "list" = all files in session, "result" = result.json, "events" = events.json, "log" = raw output tail, "search" = search text across sessions'},node:{type:"string",description:'Node name to read from (e.g. "execute_live", "generate_script"). Default: "execute_live"'},query:{type:"string",description:'Search text (only for type="search"). Searches across all session logs/events.'},tail:{type:"number",description:"Number of characters from end of log to return (default: 3000)"}},required:["type"]}},{name:"run_diagnose",description:"Diagnose one or all runs, especially failed ones. Uses run logs + known error patterns and returns likely root cause with suggested next action.",input_schema:{type:"object",properties:{runId:{type:"string",description:'Run ID from run_test, or "all" (default) to diagnose all known runs'},tail:{type:"number",description:"Characters of run log tail to inspect (default: 2000)"}}}},{name:"list_specs",description:"List available test spec files in the project",input_schema:{type:"object",properties:{directory:{type:"string",description:'Directory to scan (default: "test-specs")'}}}}]};function U(){let s=0;for(let[,n]of w)n.status==="running"&&s++;return s}function F(){for(;T.length>0;){let s=Q(T[0]?.context?.options?.config);if(U()>=s)break;let{args:n,cwd:e,context:t}=T.shift();se(n,e,t)}}async function fe(s,n){let{ticket:e,description:t,input:i,repo:o,agent:d,output:u}=s,r=["generate"];e&&r.push("--ticket",e),t&&r.push("--description",t),i&&r.push("--input",i),o&&r.push("--repo",o),u&&r.push("--output",u);let l=["assistant","cursor","claude","codex","gemini"],c=d||process.env.AGENT_TYPE,p=c&&l.includes(c)?c:null;p&&r.push("--agent",p);let g=e||"generate";return N(g,"\u{1F9EA}","Starting test spec generation (real agent with codebase access)..."),new Promise(m=>{X&&console.error(`[zibby:spawn] skill=run_generate parentPid=${process.pid} \u2192 child zibby ${r.map(f=>/\s/.test(f)?JSON.stringify(f):f).join(" ")} cwd=${n}`);let b=Z("zibby",r,{cwd:n,env:{...process.env},stdio:["ignore","pipe","pipe"],detached:!1}),v="",R="";b.stdout.on("data",f=>{let _=f.toString();v+=_;for(let a of _.split(`
`)){let y=te(a).trim();y.startsWith("\u2705")?N(g,"\u2705",y.slice(2).trim()):y.startsWith("\u2713")&&N(g,"\u2714",y.slice(2).trim())}}),b.stderr.on("data",f=>{R+=f.toString()}),b.on("close",f=>{if(f!==0){N(g,"\u274C",`Generation failed (exit ${f})`),m(JSON.stringify({error:`zibby generate failed with exit code ${f}`,stderr:R.slice(-1e3)}));return}let _=C(n,u||"test-specs"),a=[];try{let y=e?e.toLowerCase().replace(/[^a-z0-9]+/g,"-"):"";a=D(_).filter(x=>x.endsWith(".txt")&&(!y||x.startsWith(y))).map(x=>O(_,x))}catch{}N(g,"\u2705",`Generated ${a.length} test spec files`),m(JSON.stringify({success:!0,ticketKey:e||null,specFiles:a.map(y=>y.replace(`${n}/`,"")),total:a.length,message:`Generated ${a.length} specs. Now call run_test for each file.`}))}),b.on("error",f=>{N(g,"\u274C",`Spawn error: ${f.message}`),m(JSON.stringify({error:f.message}))})})}var Y=1e5,z=/^[A-Z][A-Z0-9]+-\d+$/,ge=new Set(["paragraph","heading","bulletList","orderedList","listItem","blockquote","codeBlock","rule","table","tableRow","tableCell","tableHeader","mediaSingle","panel"]);function he(s,n){if(!n||!n.length)return s;let e=s;for(let t of n)t.type==="strong"?e=`**${e}**`:t.type==="em"?e=`_${e}_`:t.type==="code"?e=`\`${e}\``:t.type==="strike"?e=`~~${e}~~`:t.type==="link"&&t.attrs?.href&&(e=`[${e}](${t.attrs.href})`);return e}function E(s,n=0){if(!Array.isArray(s))return"";let e=[];for(let t of s){if(t.type==="text"){e.push(he(t.text||"",t.marks));continue}if(t.type==="hardBreak"){e.push(`
`);continue}if(t.type==="rule"){e.push(`
---
`);continue}let i=t.content?E(t.content,n+1):"";if(t.type==="listItem")e.push(i);else if(t.type==="bulletList"){let o=(t.content||[]).map(d=>`- ${E(d.content||[],n+1).trim()}`);e.push(`
${o.join(`
`)}
`)}else if(t.type==="orderedList"){let o=(t.content||[]).map((d,u)=>`${u+1}. ${E(d.content||[],n+1).trim()}`);e.push(`
${o.join(`
`)}
`)}else if(t.type==="heading"){let o=t.attrs?.level||2;e.push(`

${"#".repeat(o)} ${i.trim()}

`)}else ge.has(t.type)?e.push(`

${i}
`):e.push(i)}return e.join("").replace(/\n{3,}/g,`

`)}function me(s){return s==null||s===""?"":typeof s=="string"?s.trim():typeof s=="object"&&Array.isArray(s.content)?E(s.content).trim():""}async function ye(s){let{getSkill:n}=await import("@zibby/agent-workflow"),e=n("jira");if(!e||typeof e.handleToolCall!="function")return null;try{let t=await e.handleToolCall("jira_get_issue",{issueKey:s}),i=JSON.parse(t);if(i?.error)return null;let o=await e.handleToolCall("jira_get_comments",{issueKey:s,maxResults:50}),d=JSON.parse(o);if(d?.error)return null;let u=me(i.description),r=[];u&&r.push(u);let l=Array.isArray(d.comments)?d.comments:[];if(l.length>0){let p=l.map(g=>String(g.body||"").trim()).filter(Boolean).join(`

`);p&&r.push(p)}let c=r.join(`

`).trim();return c?(c.length>Y&&(c=`${c.slice(0,Y)}

...[truncated]`),{inlineSpec:`inline:${c}`,issueKey:s}):null}catch{return null}}function ke(s,n){try{let e=JSON.parse(s);return JSON.stringify({...e,...n})}catch{return s}}async function Se(s,n,e){let t={...s},i=String(t.spec??"").trim();if(!i)return JSON.stringify({error:"spec is required"});let o=null;if(z.test(i)&&!i.startsWith("inline:")){let c=await ye(i);c&&(i=c.inlineSpec,t.spec=i,String(t.ticketKey||"").trim()||(t.ticketKey=c.issueKey),o=c.issueKey)}let d=String(t.ticketKey||"").trim();if(d){for(let[c,p]of w.entries())if(p?.ticketKey===d&&!(p?.status!=="running"&&p?.status!=="queued"))return JSON.stringify({runId:c,ticketKey:d,status:p.status,reused:!0,message:`A run for ${d} is already ${p.status}. Reusing existing run instead of starting a duplicate.`})}if(!i.startsWith("inline:")){let c=C(n,i);if(!I(c))return z.test(i)?JSON.stringify({error:`Invalid run_test spec: "${i}" is an issue id, not a spec.`,reason:"Jira auto-load was attempted but did not return usable text, or Jira is not configured.",doNext:["Confirm the jira skill is active and authenticated.",'Or call tracker tools yourself, then run_test with spec: "inline:" + steps.'],validExample:{spec:"inline:1. Open https://example.com \u2026 2. Verify \u2026",ticketKey:i},invalidExample:{spec:i,ticketKey:i}}):JSON.stringify({error:`Test spec not found: ${i}`,hint:'If this should be issue steps, load the issue with your tracker tools first, then run_test with spec: "inline:" + steps. Otherwise use a real file path.'})}let u=Q(e?.options?.config);if(U()>=u){let c=ee(),p=t.ticketKey||c,g={runId:c,spec:t.ticketKey?`${t.ticketKey}: ${t.spec}`:t.spec,ticketKey:t.ticketKey||null,status:"queued",startTime:Date.now(),exitCode:null,output:"",error:""};w.set(c,g),T.push({args:{...t,_queuedRunId:c},cwd:n,context:e}),N(p,"\u23F3",`Queued (${U()}/${u} running, ${T.length} queued)`);let m={runId:c,spec:g.spec,ticketKey:g.ticketKey,status:"queued",message:`Queued \u2014 will start when a slot opens (max ${u} concurrent).`};return o&&(m.resolvedFromJiraIssue=o,m.message+=` (spec built from Jira ${o})`),JSON.stringify(m)}let r=Date.now()-L;r<q&&L>0&&await new Promise(c=>setTimeout(c,q-r)),L=Date.now();let l=se(t,n,e);return o?ke(l,{resolvedFromJiraIssue:o,message:`Spec was loaded from Jira issue ${o} (description + comments).`}):l}function se(s,n,e){let{spec:t,ticketKey:i,agent:o,headless:d,workflow:u,_queuedRunId:r}=s,l=r||ee(),c=t,p=!1;if(t.startsWith("inline:")){p=!0;let k=ue(n);M(k,{recursive:!0}),c=O(k,`${l}.txt`),oe(c,t.slice(7).trim(),"utf-8")}let g=C(n,".zibby","output","runs");M(g,{recursive:!0});let m=O(g,`${l}.log`),b=ce(m,{flags:"a"}),R=o&&["assistant","cursor","claude","codex","gemini"].includes(o)?o:null,f=["test",c];R&&f.push("--agent",R),d&&f.push("--headless"),u&&f.push("--workflow",u),X&&console.error(`[zibby:spawn] skill=run_test parentPid=${process.pid} \u2192 child zibby ${f.map(k=>/\s/.test(k)?JSON.stringify(k):k).join(" ")} cwd=${n}`);let _=Z("zibby",f,{cwd:n,env:{...process.env,ZIBBY_WORKFLOW_GRAPH_LOG_MARKERS:"1"},stdio:["ignore","pipe","pipe"],detached:!1}),a={runId:l,spec:i?`${i}: ${t}`:t,ticketKey:i||null,specPath:c,logPath:m,isInline:p,pid:_.pid,status:"running",output:"",error:"",startTime:Date.now(),exitCode:null,currentNode:null,completedNodes:[]},y=i||l,x="";function P(k){let h=te(k).trim();if(!h)return;if(h.startsWith("__WORKFLOW_GRAPH_LOG__")){try{let S=JSON.parse(h.slice(22));S.phase==="node_begin"?a.currentNode=S.node:S.phase==="node_end"&&(S.node&&!a.completedNodes.includes(S.node)&&a.completedNodes.push(S.node),a.currentNode===S.node&&(a.currentNode=null))}catch{}return}let A=h.match(/Session\s+(\S+)/);if(A&&!a.sessionId&&(a.sessionId=A[1],a.sessionPath=C(n,j,K,a.sessionId)),h.startsWith("\u250C ")||h.startsWith("\u250C ")){let S=h.slice(2).trim();a.currentNode=S,$&&N(y,"\u25B6",`${S}`)}else if(h.startsWith("\u2514 ")||h.startsWith("\u2514 ")){let S=h.slice(2).trim();S.startsWith("done")?(a.currentNode&&!a.completedNodes.includes(a.currentNode)&&a.completedNodes.push(a.currentNode),$&&N(y,"\u2714",`${a.currentNode||"node"} done ${S.replace("done","").trim()}`),a.currentNode=null):S.startsWith("failed")&&($&&N(y,"\u2718",`${a.currentNode||"node"} failed ${S.replace("failed","").trim()}`),a.currentNode=null)}else h.includes("Workflow completed")&&(a.currentNode=null,$&&N(y,"\u2714",`Workflow completed (${G(Date.now()-a.startTime)})`))}function re(k){let h=k.toString();a.output+=h,b.write(h),a.output.length>5e4&&(a.output=a.output.slice(-3e4)),x+=h;let A=x.split(`
`);x=A.pop();for(let S of A)P(S)}return _.stdout.on("data",re),_.stderr.on("data",k=>{let h=k.toString();a.error+=h,b.write(h),a.error.length>2e4&&(a.error=a.error.slice(-1e4))}),_.on("close",k=>{a.status=k===0?"passed":"failed",a.exitCode=k,a.endTime=Date.now(),x&&P(x),b.end();let h=G(Date.now()-a.startTime);if(k===0?N(y,"\u2705",`Passed (${h})`):N(y,"\u274C",`Failed (${h})`),a.isInline)try{ae(a.specPath)}catch{}F()}),_.on("error",k=>{a.status="error",a.error+=`
Spawn error: ${k.message}`,N(y,"\u274C",`Spawn error: ${k.message}`),b.end(),F()}),a._child=_,w.set(l,a),JSON.stringify({runId:l,spec:a.spec,ticketKey:a.ticketKey,status:"running",pid:_.pid,logFile:m})}function B(s){let n=Math.round(((s.endTime||Date.now())-s.startTime)/1e3),e=s.completedNodes||[],t=s.currentNode||null;if(s.status!=="running")return{elapsed:n,stage:s.status,completedNodes:e,currentNode:null};let i;return t?(i=`Actively executing node "${t}"`,e.length&&(i+=` (completed: ${e.join(", ")})`)):e.length?i=`Between nodes (completed: ${e.join(", ")})`:i="Starting up (initializing workflow)",i+=`. Elapsed: ${n}s. This is normal progress \u2014 do not cancel.`,{elapsed:n,stage:"running",currentNode:t,completedNodes:e,progress:i}}function _e(s){let{runId:n}=s;if(!n)return JSON.stringify({error:"runId is required"});if(n==="all"){let o=[...w.entries()].map(([c,p])=>{let g=B(p),m={runId:c,spec:p.spec,ticketKey:p.ticketKey,status:p.status,elapsed:g.elapsed,exitCode:p.exitCode,sessionId:p.sessionId||null};return p.status==="running"?(m.currentNode=g.currentNode,m.completedNodes=g.completedNodes,m.progress=g.progress):m.outputTail=p.output.slice(-500),m}),d=o.filter(c=>c.status==="running").length,u=o.filter(c=>c.status==="passed").length,r=o.filter(c=>c.status==="failed").length,l={total:o.length,running:d,passed:u,failed:r,runs:o};return d>0&&(l._hint="All running tests are progressing normally through their workflow nodes. Do NOT cancel, diagnose, or interpret as stuck. Just tell the user they are still running."),JSON.stringify(l)}let e=w.get(n);if(!e)return JSON.stringify({error:`Run not found: ${n}`});let t=B(e),i={runId:n,spec:e.spec,ticketKey:e.ticketKey,status:e.status,elapsed:t.elapsed,exitCode:e.exitCode,sessionId:e.sessionId||null};return e.status==="running"?(i.currentNode=t.currentNode,i.completedNodes=t.completedNodes,i.progress=t.progress):(i.outputTail=e.output.slice(-1e3),i.errorTail=e.error.slice(-500)),e.status==="running"&&(i._hint="This run is actively progressing. Do NOT cancel, diagnose, or assume stuck. Just tell the user it is still running."),JSON.stringify(i)}function V(s,n){if(n.status==="queued"){let e=T.findIndex(t=>t.args._queuedRunId===s);return e>=0&&T.splice(e,1),n.status="cancelled",n.endTime=Date.now(),{ok:!0,runId:s,status:"cancelled"}}if(n.status!=="running")return{ok:!1,runId:s,error:`Run is not active (status: ${n.status})`};try{return n._child.kill("SIGTERM"),n.status="cancelled",n.endTime=Date.now(),{ok:!0,runId:s,status:"cancelled"}}catch(e){return{ok:!1,runId:s,error:`Failed to cancel: ${e.message}`}}}function Ne(s){let{runId:n}=s;if(!n)return JSON.stringify({error:"runId is required"});if(n==="all"){let t=[];for(let[i,o]of w.entries())(o.status==="running"||o.status==="queued")&&t.push(V(i,o));return t.length===0?JSON.stringify({ok:!0,message:"No active runs to cancel"}):JSON.stringify({ok:!0,cancelled:t.length,results:t})}let e=w.get(n);return JSON.stringify(e?V(n,e):{error:`Run not found: ${n}`})}function we(s,n){let e=w.get(s);if(e?.sessionPath&&I(e.sessionPath))return e.sessionPath;if(e?.sessionId){let t=C(n,j,K,e.sessionId);if(I(t))return t}return null}function ne(s,n=""){let e=[];if(!I(s))return e;for(let t of D(s,{withFileTypes:!0})){let i=n?`${n}/${t.name}`:t.name;if(t.isDirectory())e.push(...ne(O(s,t.name),i));else{let o=le(O(s,t.name));e.push({path:i,size:o.size})}}return e}function H(s){if(!I(s))return null;try{return JSON.parse(J(s,"utf-8"))}catch{return null}}function ie(s,n=2e3){if(!s||!I(s))return"";try{return J(s,"utf-8").slice(-Math.max(200,Number(n)||2e3))}catch{return""}}function be({run:s,logTail:n,errorTail:e}){let i=`${n||""}
${e||""}`.toLowerCase(),o={runId:s?.runId||null,status:s?.status||null,exitCode:s?.exitCode??null,likelyCause:"Unknown failure",confidence:"low",nextStep:'Call run_artifacts({ runId, type: "log" }) with larger tail and inspect full logs.'};return s?.status==="running"||s?.status==="queued"?{...o,likelyCause:"Run is still active; no terminal failure to diagnose yet.",confidence:"high",nextStep:'Call run_status({ runId: "all" }) to check progress.'}:i.includes("test spec not found")?{...o,likelyCause:"Invalid spec input: run_test received a non-existent spec path.",confidence:"high",nextStep:"Use spec as inline:... or a real file path from list_specs. For ticket keys, fetch steps first via Jira then build inline spec."}:i.includes("unknown command")&&i.includes("'run'")?{...o,likelyCause:"CLI command mismatch (`zibby run` unsupported in current CLI).",confidence:"high",nextStep:"Use `zibby test ...` spawn path (runner should already do this)."}:i.includes("missing openai_api_key")||i.includes("didn't provide an api key")||i.includes("401")?{...o,likelyCause:"Provider authentication/config issue (API key/proxy auth missing or rejected).",confidence:"medium",nextStep:"Verify proxy/token env and auth mode, then retry once configuration is valid."}:i.includes("spawn error")||i.includes("enoent")?{...o,likelyCause:"Failed to spawn CLI process (binary/path/environment issue).",confidence:"medium",nextStep:"Confirm `zibby` is installed and available in PATH for the chat process."}:i.includes("security command failed")||i.includes("security process exited with code: 45")||i.includes("password not found for account")?{...o,likelyCause:"Cursor agent keychain/auth failed during preflight (often transient, more common under parallel starts).",confidence:"high",nextStep:'Retry failed ticket sequentially (not parallel), or run with a different agent via run_test({ ..., agent: "codex" }).'}:o}function Oe(s,n){let{runId:e,type:t,node:i="execute_live",query:o,tail:d=3e3}=s;if(t==="search"){if(!o)return JSON.stringify({error:'query is required for type="search"'});let r=C(n,j,K);if(!I(r))return JSON.stringify({matches:[],message:"No sessions found"});let l=[],c=o.toLowerCase();for(let p of D(r,{withFileTypes:!0})){if(!p.isDirectory())continue;let g=O(r,p.name),m=[{file:"execute_live/result.json",label:"result"},{file:"execute_live/events.json",label:"events"},{file:"execute_live/raw_stream_output.txt",label:"log"},{file:"generate_script/raw_stream_output.txt",label:"script_log"},{file:"title.txt",label:"title"}];for(let{file:b,label:v}of m){let R=O(g,b);if(I(R))try{let f=J(R,"utf-8");if(f.toLowerCase().includes(c)){let _=f.toLowerCase().indexOf(c),a=Math.max(0,_-100),y=Math.min(f.length,_+o.length+100);l.push({sessionId:p.name,artifact:v,snippet:f.slice(a,y)})}}catch{}}if(l.length>=20)break}return JSON.stringify({query:o,matches:l,total:l.length})}if(!e)return JSON.stringify({error:"runId is required for this type"});if(t==="log"){let r=w.get(e),l=ie(r?.logPath,d);if(l)return JSON.stringify({runId:e,source:"run-log",totalLength:l.length,tail:l})}let u=we(e,n);if(!u)return JSON.stringify({error:`No session found for run ${e}. The run may still be starting.`});switch(t){case"list":{let r=ne(u);return JSON.stringify({sessionId:u.split("/").pop(),files:r,total:r.length})}case"result":{let r=H(O(u,i,"result.json"));return JSON.stringify(r?{sessionId:u.split("/").pop(),node:i,result:r}:{error:`No result.json found in ${i}`})}case"events":{let r=H(O(u,i,"events.json"));if(!r)return JSON.stringify({error:`No events.json found in ${i}`});let l=Array.isArray(r)?r:r.events||[];return JSON.stringify({sessionId:u.split("/").pop(),node:i,totalEvents:l.length,events:l.slice(-50)})}case"log":{let r=O(u,i,"raw_stream_output.txt");if(!I(r))return JSON.stringify({error:`No log found in ${i}`});let l=J(r,"utf-8");return JSON.stringify({sessionId:u.split("/").pop(),node:i,totalLength:l.length,tail:l.slice(-d)})}default:return JSON.stringify({error:`Unknown artifact type: ${t}. Use: list, result, events, log, search`})}}function xe(s,n){let e=String(s?.runId||"all"),t=Number(s?.tail||2e3),i=e==="all"?[...w.keys()]:[e];if(i.length===0)return JSON.stringify({error:"No runs available to diagnose. Call run_test first."});let o=i.map(r=>{let l=w.get(r);if(!l)return{runId:r,error:`Run not found: ${r}`};let c=ie(l.logPath,t),p=String(l.error||"").slice(-Math.max(200,t));return{...be({run:l,logTail:c,errorTail:p}),ticketKey:l.ticketKey||null,spec:l.spec,logTail:c,errorTail:p}}),d=o.filter(r=>r.status==="failed"||r.status==="error"),u=o.filter(r=>r.status==="running"||r.status==="queued");return JSON.stringify({total:o.length,failed:d.length,active:u.length,diagnoses:o})}function Ie(s,n){let e=s?.directory||"test-specs",t=C(n,e);if(!I(t))return JSON.stringify({specs:[],directory:e,message:`Directory not found: ${e}`});try{let o=function(d,u){for(let r of D(d,{withFileTypes:!0})){let l=u?`${u}/${r.name}`:r.name;r.isDirectory()?o(O(d,r.name),l):(r.name.endsWith(".txt")||r.name.endsWith(".md"))&&i.push(l)}},i=[];return o(t,""),JSON.stringify({specs:i.map(d=>`${e}/${d}`),total:i.length,directory:e})}catch(i){return JSON.stringify({error:i.message})}}export{$e as testRunnerSkill};
