@splinterzzz/ouro 0.1.2 → 0.1.3

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.
@@ -60,5 +60,5 @@ Error generating stack: `+s.message+`
60
60
  L40.2 10.3
61
61
  L33.8 12.6
62
62
  C36.6 14.6 39.8 15.0 42.8 14.8 Z`,fill:"currentColor",stroke:"none"}),o.jsx("circle",{cx:"45.8",cy:"9.2",r:"1.15",fill:"var(--bg, #0b0a12)",stroke:"none"})]})}const Vp=new Set(["inbox","analyzed","in_progress","staging"]);function da({icon:e,label:t,count:n,open:r,onToggleOpen:l,onActivate:s,active:i,children:a,collapsed:u}){return o.jsxs("div",{className:"nav-section",children:[o.jsxs("button",{className:`nav-head ${i?"active":""}`,onClick:()=>{s==null||s(),u||l()},"aria-expanded":u?void 0:r,"aria-label":u?t:void 0,title:u?t:void 0,children:[o.jsx(T,{name:e,size:15}),o.jsx("span",{className:"label",children:t}),n>0&&o.jsx("span",{className:"nav-count",children:n}),o.jsx(T,{name:"chevronRight",size:13,className:`chev ${r?"open":""}`})]}),o.jsx("div",{className:`nav-group ${r?"open":""}`,children:o.jsx("div",{className:"nav-group-inner",children:a})})]})}function Bp(){const e=W(y=>y.tickets),t=W(y=>y.selectedId),n=W(y=>y.select),r=W(y=>y.connected),l=Ke(y=>y.agents),s=Ke(y=>y.selectedId),i=Ke(y=>y.select),{view:a,railCollapsed:u,ticketsOpen:c,agentsOpen:v}=Ze(),h=Ze(y=>y.set),m=Ze(y=>y.toggle),x=e.filter(y=>Vp.has(y.status));return o.jsxs("aside",{className:`sidebar ${u?"collapsed":""}`,children:[o.jsxs("div",{className:"sidebar-head",children:[o.jsx(Up,{size:19,className:`brand-logo ${r?"":"offline"}`}),o.jsx("span",{className:"brand-mark",children:"ouro"}),o.jsx("span",{className:`status-dot ${r?"live":""}`,role:"status","aria-label":r?"Connected to the ouro server":"Disconnected — reconnecting",title:r?"connected":"reconnecting…"}),o.jsx("button",{className:"rail-toggle",onClick:()=>m("railCollapsed"),"aria-label":u?"Expand sidebar":"Collapse sidebar",title:u?"Expand sidebar":"Collapse sidebar",children:o.jsx(T,{name:u?"chevronsRight":"chevronsLeft",size:15})})]}),o.jsxs("div",{className:"sidebar-body wg-scroll",children:[o.jsxs(da,{icon:"inbox",label:"Tickets",count:x.length,open:c,collapsed:u,active:a==="board",onActivate:()=>h({view:"board"}),onToggleOpen:()=>m("ticketsOpen"),children:[x.map(y=>o.jsxs("button",{className:`nav-item ${t===y.id?"selected":""}`,onClick:()=>{h({view:"board"}),n(y.id)},title:y.title,children:[o.jsx("span",{className:`status-dot ${y.status==="in_progress"?"running":""}`,style:y.status==="in_progress"?void 0:{background:"var(--border-strong)"},"aria-hidden":"true"}),o.jsx("span",{className:"label",children:y.title})]},y.id)),x.length===0&&o.jsx("div",{className:"nav-empty",children:"No open tickets."})]}),o.jsxs(da,{icon:"agents",label:"Agents",count:l.length,open:v,collapsed:u,active:a==="agents",onActivate:()=>h({view:"agents"}),onToggleOpen:()=>m("agentsOpen"),children:[l.map(y=>o.jsxs("button",{className:`nav-item ${a==="agents"&&s===y.id?"selected":""}`,onClick:()=>{h({view:"agents"}),i(y.id)},title:y.description||y.name,children:[o.jsx("span",{className:"glyph","aria-hidden":"true",children:y.glyph}),o.jsx("span",{className:"label",children:y.name})]},y.id)),l.length===0&&o.jsx("div",{className:"nav-empty",children:"No agents in .ouro/agents/."})]}),o.jsx("div",{className:"nav-section",children:o.jsxs("button",{className:`nav-head ${a==="artifacts"?"active":""}`,onClick:()=>h({view:"artifacts"}),"aria-label":u?"Artifacts":void 0,title:u?"Artifacts":void 0,children:[o.jsx(T,{name:"file",size:15}),o.jsx("span",{className:"label",children:"Artifacts"})]})}),o.jsx("div",{className:"nav-section",children:o.jsxs("button",{className:`nav-head ${a==="logs"?"active":""}`,onClick:()=>h({view:"logs"}),"aria-label":u?"Logs":void 0,title:u?"Logs":void 0,children:[o.jsx(T,{name:"terminal",size:15}),o.jsx("span",{className:"label",children:"Logs"})]})}),o.jsx("div",{className:"nav-section",children:o.jsxs("button",{className:`nav-head ${a==="settings"?"active":""}`,onClick:()=>h({view:"settings"}),"aria-label":u?"Settings":void 0,title:u?"Settings":void 0,children:[o.jsx(T,{name:"settings",size:15}),o.jsx("span",{className:"label",children:"Settings"})]})})]}),o.jsxs("div",{className:"sidebar-foot",children:[o.jsx(T,{name:"file",size:12}),o.jsx("span",{children:".ouro/agents/*.md"})]})]})}function bs({value:e,options:t,onChange:n,ariaLabel:r}){const l=L.useRef(null),[s,i]=L.useState(null),a=t.findIndex(c=>c.value===e);L.useLayoutEffect(()=>{const c=l.current;if(!c||a<0){i(null);return}const v=()=>{const m=c.querySelectorAll("[data-opt]")[a];m&&i({left:m.offsetLeft,width:m.offsetWidth})};v();const h=new ResizeObserver(v);return h.observe(c),()=>h.disconnect()},[a,t]);function u(c){if(c.key!=="ArrowRight"&&c.key!=="ArrowLeft")return;c.preventDefault();const v=c.key==="ArrowRight"?1:-1,h=(a+v+t.length)%t.length;n(t[h].value)}return o.jsxs("div",{ref:l,className:"segmented",role:"radiogroup","aria-label":r,onKeyDown:u,children:[s&&o.jsx("span",{className:"segmented-pill",style:{transform:`translateX(${s.left}px)`,width:s.width}}),t.map(c=>{const v=c.value===e;return o.jsx("button",{"data-opt":!0,type:"button",role:"radio","aria-checked":v,tabIndex:v?0:-1,className:`segmented-opt ${v?"on":""}`,onClick:()=>n(c.value),title:c.title,children:c.label},c.value)})]})}const Hp=[{value:"human",label:"Human-in-loop",title:"Plan first, write only after you approve"},{value:"agent",label:"Agent loop",title:"Full autonomy — the agent writes without pausing"}],Wp=[{value:"claude-code",label:"Claude Code",title:"Run on your Claude Code subscription"},{value:"codex",label:"Codex",title:"Run on your Codex / ChatGPT subscription"}],fa={board:{h1:"Board",sub:"Tickets flow Inbox → Analyzed → In Progress → Staging → Shipped"},agents:{h1:"Agents",sub:"Markdown-configured — edit here or in .ouro/agents/"},artifacts:{h1:"Artifacts",sub:"Everything an agent sees as context — referenced in place, never copied"},logs:{h1:"Logs",sub:"One line per run — skimmable history from .ouro/context/ouro-log.md"},settings:{h1:"Settings",sub:"Credentials for the services that feed the board"}};function Qp({onNewTicket:e}){const t=W(a=>a.backend),n=W(a=>a.setBackend),r=W(a=>a.defaultMode),l=W(a=>a.setDefaultMode),s=Ze(a=>a.view),i=fa[s]??fa.board;return o.jsxs("header",{className:"topbar",children:[o.jsxs("div",{children:[o.jsx("h1",{children:i.h1}),o.jsx("div",{className:"sub",children:i.sub})]}),o.jsxs("div",{className:"topbar-right",children:[o.jsx("span",{className:"control-label",children:"Mode"}),o.jsx(bs,{ariaLabel:"Default run mode for new tickets",value:r,options:Hp,onChange:l}),t&&o.jsxs(o.Fragment,{children:[o.jsx("span",{className:"control-label",children:"Backend"}),o.jsx(bs,{ariaLabel:"Agent backend",value:t,options:Wp,onChange:n})]}),o.jsxs("button",{className:"btn primary",onClick:e,children:[o.jsx(T,{name:"plus",size:14}),"New ticket"]})]})]})}const Kp=new Set(["thinking","redacted_thinking"]);function Yp(e={}){const t=e.file_path??e.path??e.command??e.pattern??e.prompt??"";return String(t).slice(0,200)}function qp(e){return typeof e=="string"?e.slice(0,240):Array.isArray(e)?e.map(t=>typeof t=="string"?t:(t==null?void 0:t.text)??"").join(" ").trim().slice(0,240):""}function Gp(e,t){var n,r,l;if(e.type==="assistant"){const s=(n=e.message)==null?void 0:n.content;if(!Array.isArray(s))return;for(const i of s)Kp.has(i.type)||(i.type==="text"&&((r=i.text)!=null&&r.trim())?t.push({kind:"message",text:i.text.trim()}):i.type==="tool_use"&&t.push({kind:"tool",name:i.name,arg:Yp(i.input)}))}else if(e.type==="user"){const s=(l=e.message)==null?void 0:l.content;if(!Array.isArray(s))return;for(const i of s)i.type==="tool_result"&&t.push({kind:"result",text:qp(i.content)})}}function Xp(e,t){var l,s,i,a,u,c,v,h;const n=String(((l=e.item)==null?void 0:l.type)??((s=e.msg)==null?void 0:s.type)??e.type??"");if(/reason/i.test(n))return;if(/command|tool|exec|shell/i.test(n)){const m=String(((i=e.item)==null?void 0:i.command)??((a=e.item)==null?void 0:a.text)??((u=e.msg)==null?void 0:u.command)??"").slice(0,200);t.push({kind:"tool",name:n.replace(/^item\.?/,"")||"tool",arg:m});return}const r=((c=e.item)==null?void 0:c.text)??((v=e.msg)==null?void 0:v.text)??((h=e.msg)==null?void 0:h.message)??"";String(r).trim()&&/message|assistant|agent|text/i.test(n)&&t.push({kind:"message",text:String(r).trim()})}function Zp(e,t){const n=[];for(const r of e??[]){if(r.type==="error"){n.push({kind:"error",text:r.text,ts:r.ts});continue}if(r.type!=="agent_event"||!r.event)continue;const l=[];t==="codex"?Xp(r.event,l):Gp(r.event,l);for(const s of l)n.push({...s,ts:r.ts})}return n}function Jp({ticket:e}){var h,m;const[t,n]=L.useState(!1),r=W(x=>x.backend),{qaApprove:l,qaReject:s}=W(),i=Ke(x=>x.agents.find(y=>y.id===e.agentId)),a=L.useRef(null),u=Zp(e.log,r),c=e.awaitingQa&&!!e.qaVerdict;if(L.useEffect(()=>{t&&a.current&&(a.current.scrollTop=a.current.scrollHeight)},[t,u.length]),u.length===0&&!c)return null;const v=i?`${i.glyph} ${i.name}`:"Agent";return o.jsxs("div",{className:"exchange",onClick:x=>x.stopPropagation(),children:[o.jsxs("button",{className:"exchange-toggle",onClick:()=>n(x=>!x),"aria-expanded":t,children:[o.jsx(T,{name:"terminal",size:11}),t?"Hide exchange":u.length>0?`Exchange · ${u.length}`:"QA review"]}),t&&o.jsxs("div",{className:"exchange-panel",ref:a,children:[u.map((x,y)=>x.kind==="message"?o.jsxs("div",{className:"exchange-msg",children:[o.jsx("span",{className:"exchange-who",children:v}),o.jsx("span",{className:"exchange-text",children:x.text})]},y):x.kind==="tool"?o.jsxs("div",{className:"exchange-tool",children:[o.jsx("span",{className:"exchange-arrow",children:"▶"}),o.jsx("span",{className:"exchange-tname",children:x.name}),x.arg&&o.jsx("span",{className:"exchange-targ",children:x.arg})]},y):x.kind==="result"?o.jsxs("div",{className:"exchange-result",children:[o.jsx("span",{className:"exchange-arrow",children:"◂"}),o.jsx("span",{className:"exchange-text",children:x.text||"result"})]},y):o.jsx("div",{className:"exchange-err",children:x.text},y)),c&&o.jsxs("div",{className:"exchange-qa",children:[o.jsxs("div",{className:"qa-head",children:[o.jsx("span",{className:`qa-badge ${e.qaVerdict.ready?"ready":"not-ready"}`,children:e.qaVerdict.ready?"Ready":"Not ready"}),o.jsx("span",{className:"qa-summary",children:e.qaVerdict.summary})]}),((h=e.qaVerdict.reasons)==null?void 0:h.length)>0&&o.jsxs("div",{className:"qa-group",children:[o.jsx("div",{className:"qa-label",children:"reasons"}),o.jsx("ul",{className:"qa-list",children:e.qaVerdict.reasons.map((x,y)=>o.jsx("li",{children:x},y))})]}),e.qaVerdict.visualMethod==="html"&&o.jsx("div",{className:"qa-note",children:"reviewed via HTML — no screenshot"}),((m=e.qaVerdict.questions)==null?void 0:m.length)>0&&o.jsxs("div",{className:"qa-group",children:[o.jsx("div",{className:"qa-label",children:"questions"}),o.jsx("ul",{className:"qa-list",children:e.qaVerdict.questions.map((x,y)=>o.jsx("li",{children:x},y))})]}),o.jsxs("div",{className:"card-actions qa-actions",children:[o.jsxs("button",{className:"btn sm run",onClick:()=>l(e.id),children:[o.jsx(T,{name:"check",size:11}),"Approve & ship"]}),o.jsxs("button",{className:"btn sm danger",onClick:()=>s(e.id),children:[o.jsx(T,{name:"x",size:11}),"Reject — another pass"]})]})]})]})]})}function bp(e){var l,s;const t=(l=e.log)==null?void 0:l[e.log.length-1];if(!t)return null;if(t.type==="error")return`error: ${t.text}`;if(t.type==="cancelled")return t.text;const n=t.event??{};if(n.type==="raw"||n.type==="stderr")return(n.text??"").trim().slice(0,70);const r=(s=n.message)==null?void 0:s.content;if(Array.isArray(r)){const i=r.find(u=>u.type==="tool_use");if(i){const u=i.input??{},c=u.file_path??u.path??u.command??"";return`${i.name}${c?` ${String(c).slice(0,40)}`:""}`}const a=r.find(u=>{var c;return u.type==="text"&&((c=u.text)==null?void 0:c.trim())});if(a)return a.text.trim().slice(0,70)}return n.type??null}function eh(e){return e.ran?e.passed===!0?{tone:"pass",label:"tests passed"}:e.passed===!1?{tone:"fail",label:"tests failed"}:{tone:null,label:"tests ran"}:{tone:null,label:"no tests"}}function th({ticket:e,index:t}){var lt,st,xn,ur;const{setMode:n,setAgent:r,analyzeTicket:l,runTicket:s,approveTicket:i,cancelTicket:a,reopenTicket:u,deleteTicket:c,shipTicket:v,qaApprove:h,qaReject:m,select:x,selectedId:y}=W(),w=Ke(D=>D.agents),P=W(D=>D.defaultMode),[p,f]=L.useState(!1),d=y===e.id,g=e.status==="in_progress",S=!!e.analyzing,j=e.mode??P,C=w.find(D=>D.id===e.agentId),_=g?bp(e):null,V=e.testResult?eh(e.testResult):null,z=D=>Le=>{Le.stopPropagation(),D()};async function ye(){f(!0),await v(e.id).finally(()=>f(!1))}return o.jsxs("article",{className:`card ${d?"selected":""} ${g||S?"running":""}`,style:{"--i":t},onClick:()=>x(e.id),children:[o.jsx("div",{className:"card-title",children:e.title}),e.summary&&o.jsx("div",{className:"card-summary",children:e.summary}),o.jsxs("div",{className:"card-meta",children:[e.priority&&o.jsx("span",{className:`badge priority-${e.priority}`,children:e.priority}),o.jsx("span",{className:"badge",children:e.source}),C&&o.jsxs("span",{className:"badge agent",title:`Runs as ${C.name}`,children:[C.glyph," ",C.name]}),o.jsx("span",{className:"badge mono",children:e.id})]}),((lt=e.acceptanceCriteria)==null?void 0:lt.length)>0&&o.jsxs("details",{className:"card-criteria",onClick:D=>D.stopPropagation(),children:[o.jsxs("summary",{children:[e.acceptanceCriteria.length," acceptance criteria"]}),o.jsx("ul",{children:e.acceptanceCriteria.map((D,Le)=>o.jsx("li",{children:D},Le))})]}),S&&o.jsxs(o.Fragment,{children:[o.jsx("div",{className:"log-tail",children:"Analyzing…"}),o.jsx("div",{className:"card-actions",children:o.jsxs("button",{className:"btn sm danger",onClick:z(()=>a(e.id)),children:[o.jsx(T,{name:"stop",size:10}),"Cancel"]})})]}),!S&&(e.status==="analyzed"||e.status==="inbox")&&o.jsxs(o.Fragment,{children:[w.length>0&&o.jsxs("div",{className:"select-wrap",onClick:D=>D.stopPropagation(),children:[o.jsxs("select",{className:"select",style:{height:28,padding:"0 26px 0 9px",fontSize:12},value:e.agentId??"",onChange:D=>r(e.id,D.target.value||null),"aria-label":`Agent for ${e.title}`,children:[o.jsx("option",{value:"",children:"Default agent"}),w.map(D=>o.jsxs("option",{value:D.id,children:[D.glyph," ",D.name]},D.id))]}),o.jsx(T,{name:"chevronDown",size:12,className:"chev"})]}),o.jsxs("div",{className:"card-actions",children:[o.jsxs("button",{className:"btn sm",onClick:z(()=>n(e.id,j==="agent"?"human":"agent")),title:e.mode?`This ticket overrides the board default — click to switch to ${j==="agent"?"human-in-loop":"agent loop"}`:`Inherited from the board default (${P}) — click to override`,children:[j==="agent"?"Agent loop":"Human-in-loop",!e.mode&&o.jsx("span",{style:{color:"var(--text-mute)"},children:"·default"})]}),o.jsxs("button",{className:"btn sm",onClick:z(()=>l(e.id)),title:"Read-only Analyst pass — summary, priority, and acceptance criteria",children:[o.jsx(T,{name:"search",size:11}),e.status==="analyzed"?"Re-analyze":"Analyze"]}),o.jsxs("button",{className:"btn sm run",onClick:z(()=>s(e.id)),children:[o.jsx(T,{name:"play",size:11}),"Run"]}),o.jsx("button",{className:"btn sm ghost icon-only",onClick:z(()=>c(e.id)),"aria-label":`Delete ticket ${e.title}`,title:"Delete ticket",children:o.jsx(T,{name:"trash",size:12})})]})]}),g&&o.jsxs(o.Fragment,{children:[_&&o.jsx("div",{className:"log-tail",children:_}),o.jsx("div",{className:"card-actions",children:o.jsxs("button",{className:"btn sm danger",onClick:z(()=>a(e.id)),children:[o.jsx(T,{name:"stop",size:10}),"Cancel run"]})})]}),e.status==="staging"&&o.jsxs(o.Fragment,{children:[e.awaitingApproval&&e.plan&&o.jsx("pre",{className:"diff",children:e.plan.slice(0,600)}),e.diff&&o.jsx("pre",{className:"diff",children:e.diff.slice(0,600)}),!e.diff&&!e.plan&&o.jsx("div",{className:"cancel-note",children:"Run finished with no file changes."}),e.testResult&&o.jsxs("div",{className:"test-result",children:[o.jsx("span",{className:`test-badge ${V.tone??""}`,children:V.label}),e.testResult.command&&o.jsx("span",{className:"mono cmd",children:e.testResult.command})]}),((st=e.testResult)==null?void 0:st.output)&&o.jsxs("details",{className:"card-criteria",onClick:D=>D.stopPropagation(),children:[o.jsx("summary",{children:"output"}),o.jsx("pre",{className:"diff",children:e.testResult.output.slice(0,4e3)})]}),e.previewUrl&&o.jsxs("a",{className:"preview-link",href:e.previewUrl,target:"_blank",rel:"noreferrer",onClick:D=>D.stopPropagation(),children:[o.jsx(T,{name:"terminal",size:12}),o.jsx("span",{className:"label",children:e.previewUrl})]}),!e.previewUrl&&e.previewNote&&o.jsx("div",{className:"cancel-note",children:e.previewNote}),e.qaVerdict&&o.jsxs("div",{className:"qa-verdict",children:[o.jsxs("div",{className:"qa-head",children:[o.jsx("span",{className:`qa-badge ${e.qaVerdict.ready?"ready":"not-ready"}`,children:e.qaVerdict.ready?"Ready":"Not ready"}),o.jsx("span",{className:"qa-summary",children:e.qaVerdict.summary})]}),((xn=e.qaVerdict.reasons)==null?void 0:xn.length)>0&&o.jsxs("details",{className:"card-criteria",onClick:D=>D.stopPropagation(),children:[o.jsxs("summary",{children:[e.qaVerdict.reasons.length," reasons"]}),o.jsx("ul",{children:e.qaVerdict.reasons.map((D,Le)=>o.jsx("li",{children:D},Le))})]}),e.qaVerdict.visualMethod==="html"&&o.jsx("div",{className:"qa-note",children:"reviewed via HTML — no screenshot"}),((ur=e.qaVerdict.questions)==null?void 0:ur.length)>0&&o.jsxs("div",{className:"qa-group",children:[o.jsx("div",{className:"qa-label",children:"questions"}),o.jsx("ul",{className:"qa-list",children:e.qaVerdict.questions.map((D,Le)=>o.jsx("li",{children:D},Le))})]})]}),e.shipNote&&o.jsxs("div",{className:"cancel-note",children:[o.jsx(T,{name:"alert",size:12,style:{marginTop:2}}),o.jsx("span",{children:e.shipNote})]}),o.jsxs("div",{className:"card-actions",children:[e.awaitingApproval&&o.jsxs("button",{className:"btn sm run",onClick:z(()=>i(e.id)),children:[o.jsx(T,{name:"check",size:11}),"Approve & continue"]}),!e.awaitingApproval&&!e.awaitingQa&&e.worktree&&o.jsxs("button",{className:"btn sm run",onClick:z(ye),disabled:p,children:[o.jsx(T,{name:"gitBranch",size:11}),p?"Shipping…":e.shipNote?"Retry PR":"Create PR"]}),o.jsxs("button",{className:"btn sm",onClick:z(()=>a(e.id)),children:[o.jsx(T,{name:"x",size:11}),e.awaitingApproval?"Reject":"Cancel"]})]}),e.awaitingQa&&o.jsxs(o.Fragment,{children:[e.escalated&&o.jsxs("div",{className:"cancel-note",children:[o.jsx(T,{name:"alert",size:12,style:{marginTop:2}}),o.jsxs("span",{children:["Escalated after ",e.qaAttempts," QA failures — your decision."]})]}),o.jsxs("div",{className:"card-actions qa-actions",children:[o.jsxs("button",{className:"btn sm run",onClick:z(()=>h(e.id)),children:[o.jsx(T,{name:"check",size:11}),"Approve & ship"]}),o.jsxs("button",{className:"btn sm danger",onClick:z(()=>m(e.id)),children:[o.jsx(T,{name:"x",size:11}),"Reject — another pass"]})]})]})]}),e.status==="done"&&o.jsxs(o.Fragment,{children:[e.prUrl?o.jsxs("a",{className:"pr-link",href:e.prUrl,target:"_blank",rel:"noreferrer",onClick:D=>D.stopPropagation(),children:[o.jsx(T,{name:"gitBranch",size:12}),o.jsx("span",{className:"label",children:e.prUrl.replace(/^https:\/\/github\.com\//,"")})]}):o.jsxs("div",{className:"cancel-note",children:[o.jsx(T,{name:"check",size:12,style:{marginTop:2}}),o.jsx("span",{children:e.shipNote??"Done."})]}),e.branch&&o.jsxs("div",{className:"card-meta",children:[o.jsx("span",{className:"badge mono",children:e.branch}),e.baseBranch&&o.jsxs("span",{className:"badge mono",children:["→ ",e.baseBranch]})]}),o.jsx("div",{className:"card-actions",children:o.jsx("button",{className:"btn sm ghost icon-only",onClick:z(()=>c(e.id)),"aria-label":`Delete ticket ${e.title}`,title:"Remove from the board (the PR and branch stay)",children:o.jsx(T,{name:"trash",size:12})})})]}),e.status==="cancelled"&&o.jsxs(o.Fragment,{children:[o.jsxs("div",{className:"cancel-note",children:[o.jsx(T,{name:"alert",size:12,style:{marginTop:2}}),o.jsx("span",{children:e.cancelReason??"Cancelled."})]}),o.jsxs("div",{className:"card-actions",children:[o.jsxs("button",{className:"btn sm",onClick:z(()=>u(e.id)),children:[o.jsx(T,{name:"rotate",size:11}),"Reopen"]}),o.jsx("button",{className:"btn sm ghost icon-only",onClick:z(()=>c(e.id)),"aria-label":`Delete ticket ${e.title}`,title:"Delete ticket",children:o.jsx(T,{name:"trash",size:12})})]})]}),o.jsx(Jp,{ticket:e})]})}const nh=[{key:"inbox",label:"Inbox",statuses:["inbox"],dot:"var(--text-mute)"},{key:"analyzed",label:"Analyzed",statuses:["analyzed"],dot:"var(--chrome)"},{key:"in_progress",label:"In Progress",statuses:["in_progress"],dot:"var(--run)",live:!0},{key:"staging",label:"Staging",statuses:["staging","cancelled"],dot:"var(--brand)"},{key:"done",label:"Shipped",statuses:["done"],dot:"var(--brand)"}],rh={inbox:"Nothing new. Message the Telegram bot to file one.",analyzed:"Nothing analyzed yet.",in_progress:"No agent is running.",staging:"Nothing waiting on you.",done:"No PRs opened yet."};function Dc(){const e=W(t=>t.tickets);return o.jsx("div",{className:"board wg-scroll",children:nh.map(t=>{const n=e.filter(r=>t.statuses.includes(r.status));return o.jsxs("section",{className:"column","aria-label":t.label,children:[o.jsxs("div",{className:"column-header",children:[o.jsx("span",{className:`dot ${t.live&&n.length>0?"live":""}`,style:{background:t.dot},"aria-hidden":"true"}),o.jsx("span",{children:t.label}),o.jsx("span",{className:"count mono",children:n.length})]}),o.jsxs("div",{className:"column-body wg-scroll",children:[n.map((r,l)=>o.jsx(th,{ticket:r,index:l},r.id)),n.length===0&&o.jsx("div",{className:"empty",children:rh[t.key]})]})]},t.key)})})}const ls=["opus","sonnet","haiku"],lh=new Set(["Edit","Write","Bash","WebFetch","WebSearch"]),sh=[{value:"form",label:"Fields"},{value:"raw",label:"Raw .md"}];function ih(){const{agents:e,toolUniverse:t,selectedId:n,select:r,create:l,loading:s,error:i}=Ke(),[a,u]=L.useState(""),[c,v]=L.useState(!1),[h,m]=L.useState(null),x=e.find(w=>w.id===n)??e[0]??null;async function y(){const w=a.trim();if(!(!w||c)){v(!0),m(null);try{await l(w),u("")}catch(P){m(String(P.message||P))}finally{v(!1)}}}return s?o.jsx("div",{className:"agent-detail",children:o.jsx("div",{className:"empty",children:"Loading agents…"})}):o.jsxs(o.Fragment,{children:[i&&o.jsxs("div",{className:"banner bad",children:[o.jsx(T,{name:"alert",size:13}),"Couldn't load agents (",i,")."]}),o.jsxs("div",{className:"agents",children:[o.jsxs("div",{className:"agents-list wg-scroll",children:[o.jsxs("div",{className:"agents-list-head",children:[o.jsx("span",{children:"Agents"}),o.jsx("button",{className:"btn sm",onClick:y,disabled:!a.trim()||c,children:c?"Creating…":"+ New"})]}),o.jsx("input",{className:"input",style:{height:30,fontSize:12.5},value:a,onChange:w=>u(w.target.value),onKeyDown:w=>w.key==="Enter"&&y(),placeholder:"New agent name…","aria-label":"New agent name"}),h&&o.jsx("div",{className:"error-text",children:h}),e.map((w,P)=>o.jsxs("button",{className:`agent-row ${(x==null?void 0:x.id)===w.id?"selected":""}`,style:{"--i":P},onClick:()=>r(w.id),children:[o.jsxs("div",{className:"agent-row-top",children:[o.jsx("span",{className:"glyph","aria-hidden":"true",children:w.glyph}),o.jsx("span",{className:"name",children:w.name})]}),w.description&&o.jsx("div",{className:"desc",children:w.description}),o.jsxs("div",{className:"foot",children:[o.jsx("span",{children:w.model}),o.jsxs("span",{children:[w.tools.length," tools"]})]})]},w.id)),e.length===0&&o.jsx("div",{className:"empty",children:"No agents yet — create one above."})]}),o.jsx("div",{className:"agent-detail wg-scroll",children:x?o.jsx(oh,{agent:x,toolUniverse:t},x.id):o.jsx("div",{className:"empty",children:"Select an agent, or create one."})})]})]})}function oh({agent:e,toolUniverse:t}){const{save:n,remove:r,fetchRaw:l}=Ke(),[s,i]=L.useState("form"),[a,u]=L.useState(()=>({name:e.name,glyph:e.glyph,description:e.description,model:e.model,tools:[...e.tools],systemPrompt:e.systemPrompt})),[c,v]=L.useState(null),[h,m]=L.useState(null),[x,y]=L.useState(!1);L.useEffect(()=>{s!=="raw"||c!==null||l(e.id).then(v).catch(g=>m({error:String(g.message||g)}))},[s,c,e.id,l]);const w=ls.includes(a.model)?ls:[a.model,...ls],P=[...new Set([...t,...a.tools])];function p(g){u(S=>({...S,tools:S.tools.includes(g)?S.tools.filter(j=>j!==g):[...S.tools,g]}))}async function f(){y(!0),m(null);try{s==="raw"?await n(e.id,{raw:c}):(await n(e.id,a),v(null)),m({ok:!0}),setTimeout(()=>m(null),2400)}catch(g){m({error:String(g.message||g)})}finally{y(!1)}}async function d(){if(confirm(`Delete "${e.name}"? This removes .ouro/agents/${e.id}.md from disk.`))try{await r(e.id)}catch(g){m({error:String(g.message||g)})}}return o.jsxs("div",{className:"agent-detail-inner",children:[o.jsxs("div",{className:"agent-head",children:[o.jsx("span",{className:"glyph","aria-hidden":"true",children:e.glyph}),o.jsx("h2",{children:e.name}),o.jsxs("span",{className:"path",children:[o.jsx(T,{name:"file",size:11}),".ouro/agents/",e.id,".md"]})]}),o.jsx("div",{className:"agent-sub",children:e.description||"No description."}),o.jsxs("div",{style:{display:"flex",gap:8,marginBottom:16},children:[o.jsx(bs,{value:s,options:sh,onChange:i,ariaLabel:"Editor mode"}),o.jsxs("div",{style:{marginLeft:"auto",display:"flex",gap:8},children:[o.jsxs("button",{className:"btn danger sm",onClick:d,children:[o.jsx(T,{name:"trash",size:12}),"Delete"]}),o.jsxs("button",{className:"btn primary sm",onClick:f,disabled:x||s==="raw"&&c===null,children:[o.jsx(T,{name:"save",size:12}),x?"Saving…":"Save"]})]})]}),s==="raw"?o.jsxs("div",{className:"field",children:[o.jsx("label",{htmlFor:"raw-md",children:"File contents"}),o.jsx("textarea",{id:"raw-md",className:"textarea mono",style:{minHeight:420},value:c??"",onChange:g=>v(g.target.value),spellCheck:!1,placeholder:"Loading…"}),o.jsxs("div",{className:"hint",children:["YAML frontmatter (",o.jsx("code",{children:"name"}),", ",o.jsx("code",{children:"glyph"}),", ",o.jsx("code",{children:"description"}),", ",o.jsx("code",{children:"model"}),","," ",o.jsx("code",{children:"tools"}),") then the body, which is sent to the model as its system prompt."]})]}):o.jsxs(o.Fragment,{children:[o.jsxs("div",{className:"field-row",children:[o.jsxs("div",{className:"field",children:[o.jsx("label",{htmlFor:"a-name",children:"Name"}),o.jsx("input",{id:"a-name",className:"input",value:a.name,onChange:g=>u(S=>({...S,name:g.target.value}))})]}),o.jsxs("div",{className:"field",children:[o.jsx("label",{htmlFor:"a-glyph",children:"Glyph"}),o.jsx("input",{id:"a-glyph",className:"input mono",value:a.glyph,maxLength:2,onChange:g=>u(S=>({...S,glyph:g.target.value}))})]})]}),o.jsxs("div",{className:"field",children:[o.jsx("label",{htmlFor:"a-desc",children:"Description"}),o.jsx("input",{id:"a-desc",className:"input",value:a.description,onChange:g=>u(S=>({...S,description:g.target.value})),placeholder:"One line — shown on the card and in the sidebar."})]}),o.jsxs("div",{className:"field",children:[o.jsx("label",{htmlFor:"a-model",children:"Model"}),o.jsxs("div",{className:"select-wrap",style:{maxWidth:280},children:[o.jsx("select",{id:"a-model",className:"select mono",value:a.model,onChange:g=>u(S=>({...S,model:g.target.value})),children:w.map(g=>o.jsx("option",{value:g,children:g},g))}),o.jsx(T,{name:"chevronDown",size:13,className:"chev"})]}),o.jsxs("div",{className:"hint",children:["An alias (",o.jsx("code",{children:"opus"}),", ",o.jsx("code",{children:"sonnet"}),", ",o.jsx("code",{children:"haiku"}),") or a full model name."]})]}),o.jsxs("div",{className:"field",children:[o.jsxs("label",{children:["Allowed tools · ",o.jsx("span",{style:{color:"var(--warn)"},children:"⚠ can write or reach the network"})]}),o.jsx("div",{className:"tools",children:P.map(g=>{const S=a.tools.includes(g),j=lh.has(g);return o.jsxs("button",{type:"button",className:`tool ${S?"on":""} ${j?"danger":""}`,onClick:()=>p(g),"aria-pressed":S,children:[j&&"⚠"," ",g]},g)})}),o.jsxs("div",{className:"hint",children:["Enforced on the Claude Code backend via ",o.jsx("code",{children:"--allowedTools"}),". Codex has no per-tool grant — there the sandbox mode governs writes, so this list is advisory."]})]}),o.jsxs("div",{className:"field",children:[o.jsx("label",{htmlFor:"a-prompt",children:"System prompt"}),o.jsx("textarea",{id:"a-prompt",className:"textarea",style:{minHeight:200},value:a.systemPrompt,onChange:g=>u(S=>({...S,systemPrompt:g.target.value}))}),o.jsx("div",{className:"hint",children:"The markdown body of the file. Appended to the backend's own system prompt."})]})]}),(h==null?void 0:h.error)&&o.jsxs("div",{className:"error-text",children:["Couldn't save (",h.error,")."]}),(h==null?void 0:h.ok)&&o.jsxs("div",{className:"ok-text",children:["Saved to .ouro/agents/",e.id,".md"]})]})}function ah(e){return e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:`${(e/(1024*1024)).toFixed(1)} MB`}function uh(){const[e,t]=L.useState(null),[n,r]=L.useState(null);if(L.useEffect(()=>{let a=!0;return fetch("/api/artifacts").then(u=>u.json()).then(u=>a&&t(u)).catch(u=>a&&r(String(u.message||u))),()=>{a=!1}},[]),n)return o.jsxs("div",{className:"banner bad",children:[o.jsx(T,{name:"alert",size:13}),"Couldn't load artifacts (",n,")."]});if(!e)return o.jsx("div",{className:"empty",children:"Loading context…"});const{files:l,referenced:s,contextDir:i}=e;return o.jsxs("div",{className:"artifacts wg-scroll",children:[o.jsx("p",{className:"artifacts-intro",children:"Everything an agent can see as context — referenced in place, never copied. Files are listed for discovery; the agent reads what it needs."}),o.jsxs("section",{className:"artifact-group",children:[o.jsxs("div",{className:"artifact-group-head",children:[o.jsx(T,{name:"file",size:13}),o.jsx("h2",{children:"Referenced in place"}),o.jsx("span",{className:"count mono",children:s.length})]}),o.jsx("p",{className:"artifact-group-sub",children:"Root convention files the backend auto-reads. Shown at their real path — not duplicated into ouro."}),s.length===0?o.jsx("div",{className:"empty",children:"No CLAUDE.md / AGENTS.md at the repo root."}):o.jsx("ul",{className:"artifact-list",children:s.map(a=>o.jsxs("li",{className:"artifact-item",children:[o.jsx("span",{className:"artifact-name mono",children:a.path}),o.jsx("span",{className:"badge",children:"auto-read"})]},a.path))})]}),o.jsxs("section",{className:"artifact-group",children:[o.jsxs("div",{className:"artifact-group-head",children:[o.jsx(T,{name:"inbox",size:13}),o.jsxs("h2",{children:[i,"/"]}),o.jsx("span",{className:"count mono",children:l.length})]}),o.jsx("p",{className:"artifact-group-sub",children:"Droppable artifacts. Each run gets this list — names and descriptions, injected as a manifest — and reads contents on demand rather than having them dumped into the prompt."}),l.length===0?o.jsxs("div",{className:"empty",children:["Nothing yet. Drop files in ",i,"/ — they'll be offered to every run."]}):o.jsx("ul",{className:"artifact-list",children:l.map(a=>o.jsxs("li",{className:"artifact-item",children:[o.jsx("span",{className:"artifact-name mono",children:a.name}),a.description&&o.jsx("span",{className:"artifact-desc",children:a.description}),o.jsx("span",{className:"badge mono",children:ah(a.size)})]},a.name))})]})]})}function ch({text:e}){const t=[];let n=0;for(const r of e.split(`
63
- `)){const l=r.replace(/\r$/,"");/^##\s+/.test(l)?t.push(o.jsx("h3",{className:"log-date",children:l.replace(/^##\s+/,"")},n++)):/^#\s+/.test(l)||(/^\s*→/.test(l)?t.push(o.jsx("div",{className:"log-outcome",children:l.trim()},n++)):/^-\s+/.test(l)?t.push(o.jsx("div",{className:"log-head",children:l.replace(/^-\s+/,"").replace(/\*\*/g,"")},n++)):l.trim()&&t.push(o.jsx("p",{className:"log-note",children:l.trim()},n++)))}return o.jsx("div",{className:"log-view",children:t})}function dh(){const[e,t]=L.useState(null),[n,r]=L.useState(null);return L.useEffect(()=>{let l=!0;return fetch("/api/log").then(s=>s.json()).then(s=>l&&t(s.content??"")).catch(s=>l&&r(String(s.message||s))),()=>{l=!1}},[]),n?o.jsxs("div",{className:"banner bad",children:[o.jsx(T,{name:"alert",size:13}),"Couldn't load the log (",n,")."]}):e===null?o.jsx("div",{className:"empty",children:"Loading log…"}):e.trim()?o.jsxs("div",{className:"logs wg-scroll",children:[o.jsxs("p",{className:"logs-intro",children:["One line per run, appended at run end regardless of outcome. Templated from ticket state — not an LLM call — and committed as team memory in ",o.jsx("span",{className:"mono",children:".ouro/context/ouro-log.md"}),"."]}),o.jsx(ch,{text:e})]}):o.jsx("div",{className:"logs",children:o.jsxs("div",{className:"empty",children:["No runs logged yet. Every run appends a line to ",o.jsx("span",{className:"mono",children:".ouro/context/ouro-log.md"})," — shipped, failed, or cancelled."]})})}const fh=zl((e,t)=>({telegram:null,loading:!0,error:null,async hydrate(){try{const n=await fetch("/api/config/telegram");if(!n.ok)throw new Error(`GET /api/config/telegram → ${n.status}`);e({telegram:await n.json(),loading:!1,error:null})}catch(n){e({loading:!1,error:String(n.message||n)})}},async saveToken(n){const r=await ss("/api/config/telegram","POST",{botToken:n});return e({telegram:r,error:null}),r},async testToken(){const n=await ss("/api/config/telegram/test","POST");return e({telegram:n,error:null}),n},async clearToken(){const n=await ss("/api/config/telegram","DELETE");return e({telegram:n,error:null}),n}}));async function ss(e,t,n){const r=await fetch(e,{method:t,headers:{"Content-Type":"application/json"},...n?{body:JSON.stringify(n)}:{}}),l=await r.json().catch(()=>({}));if(!r.ok)throw new Error(l.error??`HTTP ${r.status}`);return l}function ph(){var p;const{telegram:e,loading:t,error:n,hydrate:r,saveToken:l,testToken:s,clearToken:i}=fh(),[a,u]=L.useState(""),[c,v]=L.useState(null),[h,m]=L.useState(null);L.useEffect(()=>{r();const f=setInterval(r,5e3);return()=>clearInterval(f)},[r]);async function x(f,d){v(f),m(null);try{return await d()}catch(g){return m({error:String(g.message||g)}),null}finally{v(null)}}async function y(){var g,S;const f=a.trim();if(!f)return;const d=await x("save",()=>l(f));d&&(u(""),m(((g=d.restart)==null?void 0:g.ok)===!1?{restart:d.restart}:{ok:`Saved. Intake is live as @${(S=d.bot)==null?void 0:S.username}.`}))}async function w(){var d;const f=await x("test",()=>s());f&&m({ok:`Telegram knows this token as @${(d=f.bot)==null?void 0:d.username}.`})}async function P(){if(!confirm("Remove the bot token from .ouro/.env and stop Telegram intake?"))return;await x("clear",()=>i())&&m({ok:"Token removed. Telegram intake is off."})}return t?o.jsx("div",{className:"agent-detail",children:o.jsx("div",{className:"empty",children:"Loading settings…"})}):o.jsxs(o.Fragment,{children:[n&&o.jsxs("div",{className:"banner bad",children:[o.jsx(T,{name:"alert",size:13}),"Couldn't load settings (",n,")."]}),(e==null?void 0:e.configError)&&o.jsxs("div",{className:"banner bad settings-config-error",children:[o.jsx(T,{name:"alert",size:13}),o.jsx("span",{children:e.configError})]}),o.jsx("div",{className:"agent-detail wg-scroll",children:o.jsxs("div",{className:"agent-detail-inner",children:[o.jsxs("div",{className:"agent-head",children:[o.jsx("span",{className:"glyph","aria-hidden":"true",children:o.jsx(T,{name:"send",size:16})}),o.jsx("h2",{children:"Telegram intake"}),o.jsxs("span",{className:"path",children:[o.jsx(T,{name:"file",size:11}),".ouro/.env"]})]}),o.jsx("div",{className:"agent-sub",children:"The bot that interviews whoever messages it and files the result on your board."}),o.jsx(hh,{telegram:e}),o.jsxs("div",{className:"field",children:[o.jsx("label",{htmlFor:"tg-token",children:"Bot token"}),o.jsxs("div",{className:"save-row",style:{margin:0},children:[o.jsx("input",{id:"tg-token",className:"input mono",type:"password",value:a,onChange:f=>u(f.target.value),onKeyDown:f=>f.key==="Enter"&&y(),placeholder:e!=null&&e.configured?"Paste a new token to replace the current one…":"123456789:AAE…",spellCheck:!1,autoComplete:"off"}),o.jsxs("button",{className:"btn primary",onClick:y,disabled:!a.trim()||!!c,children:[o.jsx(T,{name:"save",size:12}),c==="save"?"Verifying…":"Save & restart"]})]}),o.jsxs("div",{className:"hint",children:["Written to ",o.jsx("code",{children:".ouro/.env"})," as ",o.jsx("code",{children:e==null?void 0:e.tokenVar})," — gitignored, and read back on every boot, so the bot survives closing this terminal. Saving checks the token against Telegram, then restarts intake to pick it up."]})]}),o.jsxs("div",{className:"settings-actions",children:[o.jsxs("button",{className:"btn sm",onClick:w,disabled:!(e!=null&&e.configured)||!!c,children:[o.jsx(T,{name:"rotate",size:12}),c==="test"?"Checking…":"Test connection"]}),o.jsxs("button",{className:"btn danger sm",onClick:P,disabled:!(e!=null&&e.configured)||!!c,children:[o.jsx(T,{name:"trash",size:12}),c==="clear"?"Removing…":"Remove token"]})]}),(h==null?void 0:h.error)&&o.jsx("div",{className:"error-text",children:h.error}),(h==null?void 0:h.ok)&&o.jsx("div",{className:"ok-text",children:h.ok}),(h==null?void 0:h.restart)&&o.jsxs("div",{className:"settings-failure",children:[o.jsx("div",{className:"error-text",style:{marginTop:0},children:"Token saved, but the intake service didn't stay up. Its log says:"}),(p=h.restart.log)!=null&&p.length?h.restart.log.map((f,d)=>o.jsx("div",{className:"log-tail",children:f},d)):o.jsx("div",{className:"log-tail",children:"(nothing — try: ouro logs listen)"})]}),o.jsxs("div",{className:"field",style:{marginTop:26},children:[o.jsx("label",{children:"Getting a token"}),o.jsxs("ol",{className:"settings-steps",children:[o.jsxs("li",{children:["Message ",o.jsx("a",{className:"ext",href:"https://t.me/BotFather",target:"_blank",rel:"noreferrer",children:"@BotFather"})," on Telegram and send ",o.jsx("code",{children:"/newbot"}),"."]}),o.jsxs("li",{children:["Pick a display name, then a username ending in ",o.jsx("code",{children:"bot"}),"."]}),o.jsx("li",{children:"Copy the token it replies with and paste it above."}),o.jsx("li",{children:"Message your new bot — the ticket it writes lands on the board."})]})]})]})})]})}function hh({telegram:e}){var s,i;const t=!!((s=e==null?void 0:e.listener)!=null&&s.running),n=!!(e!=null&&e.configured),r=n?t?`Listening${(i=e.bot)!=null&&i.username?` as @${e.bot.username}`:""}`:"Token saved — intake is not running":"Not configured",l=n?t?"Anyone who messages the bot gets interviewed, and their ticket lands on the board.":"Save the token again to restart it, or run ouro logs listen to see why it stopped.":"Paste a bot token below to turn on Telegram intake.";return o.jsxs("div",{className:`settings-status ${t?"live":""}`,children:[o.jsx("span",{className:`status-dot ${t?"live":n?"":"idle"}`,"aria-hidden":"true"}),o.jsxs("div",{children:[o.jsx("div",{className:"state",children:r}),o.jsx("div",{className:"detail",children:l})]}),n&&o.jsxs("div",{className:"meta",children:[o.jsx("div",{children:e.tokenHint}),!e.persisted&&o.jsx("div",{className:"warn-note",children:"shell export only — not in .env"}),t&&e.listener.uptime&&o.jsxs("div",{children:["up ",e.listener.uptime]})]})]})}function mh(e){let t=0;for(let n=0;n<e.length;n++)t=t*31+e.charCodeAt(n)>>>0;return t%355}function vh(e){const t=new Date(e);return Number.isNaN(t.getTime())?"--:--:--":t.toTimeString().slice(0,8)}function gh(){const e=W(d=>d.feed),t=W(d=>d.tickets),n=W(d=>d.clearFeed),r=W(d=>d.selectedId),l=W(d=>d.select),{terminalOpen:s,terminalHeight:i,autoscroll:a}=Ze(),u=Ze(d=>d.set),c=Ze(d=>d.toggle),v=Ze(d=>d.setTerminalHeight),h=L.useRef(null),[m,x]=L.useState(!1),[y,w]=L.useState(!1),P=t.filter(d=>d.status==="in_progress"),p=y&&r?e.filter(d=>d.ticketId===r):e;L.useLayoutEffect(()=>{if(!a||!s)return;const d=h.current;d&&(d.scrollTop=d.scrollHeight)},[p.length,a,s]);const f=L.useCallback(()=>{const d=h.current;if(!d)return;const g=d.scrollHeight-d.scrollTop-d.clientHeight<24;g!==a&&u({autoscroll:g})},[a,u]);return L.useEffect(()=>{if(!m)return;const d=S=>{v(window.innerHeight-S.clientY),S.preventDefault()},g=()=>x(!1);return window.addEventListener("pointermove",d),window.addEventListener("pointerup",g),document.body.style.userSelect="none",document.body.style.cursor="ns-resize",()=>{window.removeEventListener("pointermove",d),window.removeEventListener("pointerup",g),document.body.style.userSelect="",document.body.style.cursor=""}},[m,v]),o.jsxs("section",{className:"terminal",style:{height:s?i:"auto"},"aria-label":"Agent output",children:[s&&o.jsx("div",{className:`terminal-resize ${m?"dragging":""}`,onPointerDown:d=>{d.preventDefault(),x(!0)},role:"separator","aria-orientation":"horizontal","aria-label":"Resize terminal",tabIndex:0,onKeyDown:d=>{d.key==="ArrowUp"&&v(Math.min(Ic,i+24)),d.key==="ArrowDown"&&v(Math.max(Rc,i-24))}}),o.jsxs("div",{className:"terminal-head",children:[o.jsxs("span",{className:"terminal-title",children:[o.jsx(T,{name:"terminal",size:13}),"Terminal"]}),P.length>0&&o.jsxs("span",{className:"running-pill",title:P.map(d=>d.title).join(`
63
+ `)){const l=r.replace(/\r$/,"");/^##\s+/.test(l)?t.push(o.jsx("h3",{className:"log-date",children:l.replace(/^##\s+/,"")},n++)):/^#\s+/.test(l)||(/^\s*→/.test(l)?t.push(o.jsx("div",{className:"log-outcome",children:l.trim()},n++)):/^-\s+/.test(l)?t.push(o.jsx("div",{className:"log-head",children:l.replace(/^-\s+/,"").replace(/\*\*/g,"")},n++)):l.trim()&&t.push(o.jsx("p",{className:"log-note",children:l.trim()},n++)))}return o.jsx("div",{className:"log-view",children:t})}function dh(){const[e,t]=L.useState(null),[n,r]=L.useState(null);return L.useEffect(()=>{let l=!0;return fetch("/api/log").then(s=>s.json()).then(s=>l&&t(s.content??"")).catch(s=>l&&r(String(s.message||s))),()=>{l=!1}},[]),n?o.jsxs("div",{className:"banner bad",children:[o.jsx(T,{name:"alert",size:13}),"Couldn't load the log (",n,")."]}):e===null?o.jsx("div",{className:"empty",children:"Loading log…"}):e.trim()?o.jsxs("div",{className:"logs wg-scroll",children:[o.jsxs("p",{className:"logs-intro",children:["One line per run, appended at run end regardless of outcome. Templated from ticket state — not an LLM call — and committed as team memory in ",o.jsx("span",{className:"mono",children:".ouro/context/ouro-log.md"}),"."]}),o.jsx(ch,{text:e})]}):o.jsx("div",{className:"logs",children:o.jsxs("div",{className:"empty",children:["No runs logged yet. Every run appends a line to ",o.jsx("span",{className:"mono",children:".ouro/context/ouro-log.md"})," — shipped, failed, or cancelled."]})})}const fh=zl(e=>({telegram:null,loading:!0,error:null,async hydrate(){try{const t=await fetch("/api/config/telegram");if(!t.ok)throw new Error(`GET /api/config/telegram → ${t.status}`);e({telegram:await t.json(),loading:!1,error:null})}catch(t){e({loading:!1,error:String(t.message||t)})}},async saveToken(t){const n=await ss("/api/config/telegram","POST",{botToken:t});return e({telegram:n,error:null}),n},async testToken(){const t=await ss("/api/config/telegram/test","POST");return e({telegram:t,error:null}),t},async clearToken(){const t=await ss("/api/config/telegram","DELETE");return e({telegram:t,error:null}),t}}));async function ss(e,t,n){const r=await fetch(e,{method:t,headers:{"Content-Type":"application/json"},...n?{body:JSON.stringify(n)}:{}}),l=await r.json().catch(()=>({}));if(!r.ok)throw new Error(l.error??`HTTP ${r.status}`);return l}function ph(){var p;const{telegram:e,loading:t,error:n,hydrate:r,saveToken:l,testToken:s,clearToken:i}=fh(),[a,u]=L.useState(""),[c,v]=L.useState(null),[h,m]=L.useState(null);L.useEffect(()=>{r();const f=setInterval(r,5e3);return()=>clearInterval(f)},[r]);async function x(f,d){v(f),m(null);try{return await d()}catch(g){return m({error:String(g.message||g)}),null}finally{v(null)}}async function y(){var g,S;const f=a.trim();if(!f)return;const d=await x("save",()=>l(f));d&&(u(""),m(((g=d.restart)==null?void 0:g.ok)===!1?{restart:d.restart}:{ok:`Saved. Intake is live as @${(S=d.bot)==null?void 0:S.username}.`}))}async function w(){var d;const f=await x("test",()=>s());f&&m({ok:`Telegram knows this token as @${(d=f.bot)==null?void 0:d.username}.`})}async function P(){if(!confirm("Remove the bot token from .ouro/.env and stop Telegram intake?"))return;await x("clear",()=>i())&&m({ok:"Token removed. Telegram intake is off."})}return t?o.jsx("div",{className:"agent-detail",children:o.jsx("div",{className:"empty",children:"Loading settings…"})}):o.jsxs(o.Fragment,{children:[n&&o.jsxs("div",{className:"banner bad",children:[o.jsx(T,{name:"alert",size:13}),"Couldn't load settings (",n,")."]}),(e==null?void 0:e.configError)&&o.jsxs("div",{className:"banner bad settings-config-error",children:[o.jsx(T,{name:"alert",size:13}),o.jsx("span",{children:e.configError})]}),o.jsx("div",{className:"agent-detail wg-scroll",children:o.jsxs("div",{className:"agent-detail-inner",children:[o.jsxs("div",{className:"agent-head",children:[o.jsx("span",{className:"glyph","aria-hidden":"true",children:o.jsx(T,{name:"send",size:16})}),o.jsx("h2",{children:"Telegram intake"}),o.jsxs("span",{className:"path",children:[o.jsx(T,{name:"file",size:11}),".ouro/.env"]})]}),o.jsx("div",{className:"agent-sub",children:"The bot that interviews whoever messages it and files the result on your board."}),o.jsx(hh,{telegram:e}),o.jsxs("div",{className:"field",children:[o.jsx("label",{htmlFor:"tg-token",children:"Bot token"}),o.jsxs("div",{className:"save-row",style:{margin:0},children:[o.jsx("input",{id:"tg-token",className:"input mono",type:"password",value:a,onChange:f=>u(f.target.value),onKeyDown:f=>f.key==="Enter"&&y(),placeholder:e!=null&&e.configured?"Paste a new token to replace the current one…":"123456789:AAE…",spellCheck:!1,autoComplete:"off"}),o.jsxs("button",{className:"btn primary",onClick:y,disabled:!a.trim()||!!c,children:[o.jsx(T,{name:"save",size:12}),c==="save"?"Verifying…":"Save & restart"]})]}),o.jsxs("div",{className:"hint",children:["Written to ",o.jsx("code",{children:".ouro/.env"})," as ",o.jsx("code",{children:e==null?void 0:e.tokenVar})," — gitignored, and read back on every boot, so the bot survives closing this terminal. Saving checks the token against Telegram, then restarts intake to pick it up."]})]}),o.jsxs("div",{className:"settings-actions",children:[o.jsxs("button",{className:"btn sm",onClick:w,disabled:!(e!=null&&e.configured)||!!c,children:[o.jsx(T,{name:"rotate",size:12}),c==="test"?"Checking…":"Test connection"]}),o.jsxs("button",{className:"btn danger sm",onClick:P,disabled:!(e!=null&&e.configured)||!!c,children:[o.jsx(T,{name:"trash",size:12}),c==="clear"?"Removing…":"Remove token"]})]}),(h==null?void 0:h.error)&&o.jsx("div",{className:"error-text",children:h.error}),(h==null?void 0:h.ok)&&o.jsx("div",{className:"ok-text",children:h.ok}),(h==null?void 0:h.restart)&&o.jsxs("div",{className:"settings-failure",children:[o.jsx("div",{className:"error-text",style:{marginTop:0},children:"Token saved, but the intake service didn't stay up. Its log says:"}),(p=h.restart.log)!=null&&p.length?h.restart.log.map((f,d)=>o.jsx("div",{className:"log-tail",children:f},d)):o.jsx("div",{className:"log-tail",children:"(nothing — try: ouro logs listen)"})]}),o.jsxs("div",{className:"field",style:{marginTop:26},children:[o.jsx("label",{children:"Getting a token"}),o.jsxs("ol",{className:"settings-steps",children:[o.jsxs("li",{children:["Message ",o.jsx("a",{className:"ext",href:"https://t.me/BotFather",target:"_blank",rel:"noreferrer",children:"@BotFather"})," on Telegram and send ",o.jsx("code",{children:"/newbot"}),"."]}),o.jsxs("li",{children:["Pick a display name, then a username ending in ",o.jsx("code",{children:"bot"}),"."]}),o.jsx("li",{children:"Copy the token it replies with and paste it above."}),o.jsx("li",{children:"Message your new bot — the ticket it writes lands on the board."})]})]})]})})]})}function hh({telegram:e}){var s,i;const t=!!((s=e==null?void 0:e.listener)!=null&&s.running),n=!!(e!=null&&e.configured),r=n?t?`Listening${(i=e.bot)!=null&&i.username?` as @${e.bot.username}`:""}`:"Token saved — intake is not running":"Not configured",l=n?t?"Anyone who messages the bot gets interviewed, and their ticket lands on the board.":"Save the token again to restart it, or run ouro logs listen to see why it stopped.":"Paste a bot token below to turn on Telegram intake.";return o.jsxs("div",{className:`settings-status ${t?"live":""}`,children:[o.jsx("span",{className:`status-dot ${t?"live":n?"":"idle"}`,"aria-hidden":"true"}),o.jsxs("div",{children:[o.jsx("div",{className:"state",children:r}),o.jsx("div",{className:"detail",children:l})]}),n&&o.jsxs("div",{className:"meta",children:[o.jsx("div",{children:e.tokenHint}),!e.persisted&&o.jsx("div",{className:"warn-note",children:"shell export only — not in .env"}),t&&e.listener.uptime&&o.jsxs("div",{children:["up ",e.listener.uptime]})]})]})}function mh(e){let t=0;for(let n=0;n<e.length;n++)t=t*31+e.charCodeAt(n)>>>0;return t%355}function vh(e){const t=new Date(e);return Number.isNaN(t.getTime())?"--:--:--":t.toTimeString().slice(0,8)}function gh(){const e=W(d=>d.feed),t=W(d=>d.tickets),n=W(d=>d.clearFeed),r=W(d=>d.selectedId),l=W(d=>d.select),{terminalOpen:s,terminalHeight:i,autoscroll:a}=Ze(),u=Ze(d=>d.set),c=Ze(d=>d.toggle),v=Ze(d=>d.setTerminalHeight),h=L.useRef(null),[m,x]=L.useState(!1),[y,w]=L.useState(!1),P=t.filter(d=>d.status==="in_progress"),p=y&&r?e.filter(d=>d.ticketId===r):e;L.useLayoutEffect(()=>{if(!a||!s)return;const d=h.current;d&&(d.scrollTop=d.scrollHeight)},[p.length,a,s]);const f=L.useCallback(()=>{const d=h.current;if(!d)return;const g=d.scrollHeight-d.scrollTop-d.clientHeight<24;g!==a&&u({autoscroll:g})},[a,u]);return L.useEffect(()=>{if(!m)return;const d=S=>{v(window.innerHeight-S.clientY),S.preventDefault()},g=()=>x(!1);return window.addEventListener("pointermove",d),window.addEventListener("pointerup",g),document.body.style.userSelect="none",document.body.style.cursor="ns-resize",()=>{window.removeEventListener("pointermove",d),window.removeEventListener("pointerup",g),document.body.style.userSelect="",document.body.style.cursor=""}},[m,v]),o.jsxs("section",{className:"terminal",style:{height:s?i:"auto"},"aria-label":"Agent output",children:[s&&o.jsx("div",{className:`terminal-resize ${m?"dragging":""}`,onPointerDown:d=>{d.preventDefault(),x(!0)},role:"separator","aria-orientation":"horizontal","aria-label":"Resize terminal",tabIndex:0,onKeyDown:d=>{d.key==="ArrowUp"&&v(Math.min(Ic,i+24)),d.key==="ArrowDown"&&v(Math.max(Rc,i-24))}}),o.jsxs("div",{className:"terminal-head",children:[o.jsxs("span",{className:"terminal-title",children:[o.jsx(T,{name:"terminal",size:13}),"Terminal"]}),P.length>0&&o.jsxs("span",{className:"running-pill",title:P.map(d=>d.title).join(`
64
64
  `),children:[o.jsx("span",{className:"pulse","aria-hidden":"true"}),P.length," running"]}),o.jsx("span",{className:"spacer"}),r&&o.jsxs("button",{className:`btn sm ${y?"":"ghost"}`,onClick:()=>w(d=>!d),title:"Show only the selected ticket's output",children:[o.jsx(T,{name:"search",size:11}),y?"Selected only":"All tickets"]}),o.jsxs("button",{className:`btn sm ${a?"":"ghost"}`,onClick:()=>u({autoscroll:!a}),"aria-pressed":a,title:"Follow new output as it arrives",children:[o.jsx(T,{name:"chevronDown",size:11}),"Follow"]}),o.jsx("button",{className:"btn sm ghost",onClick:n,title:"Clear the view (agent logs are kept on disk)",children:"Clear"}),o.jsx("button",{className:"btn sm ghost icon-only",onClick:()=>c("terminalOpen"),"aria-label":s?"Collapse terminal":"Expand terminal","aria-expanded":s,children:o.jsx(T,{name:s?"chevronDown":"chevronRight",size:13})})]}),s&&o.jsx("div",{className:"terminal-body wg-scroll",ref:h,onScroll:f,"aria-live":"off",children:p.length===0?o.jsxs("div",{className:"terminal-empty",children:["No agent output yet. Pick a ticket and hit ",o.jsx("span",{className:"kbd",children:"Run"})," — every tool call it makes streams here."]}):p.map(d=>{const g=mh(d.ticketId);return o.jsxs("div",{className:`term-line tone-${d.tone}`,children:[o.jsx("span",{className:"term-ts",children:vh(d.ts)}),o.jsxs("span",{className:"term-tag",style:{color:`hsl(${g} 62% 62%)`},onClick:()=>l(d.ticketId),title:"Select this ticket on the board",children:["[",d.ticketId,"]"]}),o.jsx("span",{className:"term-msg",children:d.text})]},d.id)})})]})}function yh({onClose:e}){const t=W(w=>w.createTicket),n=Ke(w=>w.agents),[r,l]=L.useState(""),[s,i]=L.useState(""),[a,u]=L.useState(""),[c,v]=L.useState(!1),[h,m]=L.useState(null),x=L.useRef(null);L.useEffect(()=>{var P;(P=x.current)==null||P.focus();const w=p=>{p.key==="Escape"&&e()};return window.addEventListener("keydown",w),()=>window.removeEventListener("keydown",w)},[e]);async function y(w){if(w.preventDefault(),!(!r.trim()||c)){v(!0),m(null);try{await t({title:r.trim(),body:s.trim(),agentId:a||null}),e()}catch(P){m(String(P.message||P)),v(!1)}}}return o.jsx("div",{className:"modal-backdrop",onClick:e,children:o.jsxs("form",{className:"modal",onClick:w=>w.stopPropagation(),onSubmit:y,role:"dialog","aria-modal":"true","aria-labelledby":"new-ticket-title",children:[o.jsx("h3",{id:"new-ticket-title",children:"New ticket"}),o.jsxs("div",{className:"field",style:{margin:0},children:[o.jsx("label",{htmlFor:"nt-title",children:"Title"}),o.jsx("input",{id:"nt-title",ref:x,className:"input",value:r,onChange:w=>l(w.target.value),placeholder:"Fix the misaligned login button on mobile"})]}),o.jsxs("div",{className:"field",style:{margin:0},children:[o.jsx("label",{htmlFor:"nt-body",children:"Description"}),o.jsx("textarea",{id:"nt-body",className:"textarea",value:s,onChange:w=>i(w.target.value),rows:4,placeholder:"What's the observed behaviour, what did you expect, and how do you reproduce it?"})]}),n.length>0&&o.jsxs("div",{className:"field",style:{margin:0},children:[o.jsx("label",{htmlFor:"nt-agent",children:"Agent"}),o.jsxs("div",{className:"select-wrap",children:[o.jsxs("select",{id:"nt-agent",className:"select",value:a,onChange:w=>u(w.target.value),children:[o.jsx("option",{value:"",children:"Default agent"}),n.map(w=>o.jsxs("option",{value:w.id,children:[w.glyph," ",w.name]},w.id))]}),o.jsx(T,{name:"chevronDown",size:13,className:"chev"})]})]}),h&&o.jsx("div",{className:"error-text",children:h}),o.jsxs("div",{className:"modal-actions",children:[o.jsx("button",{type:"button",className:"btn",onClick:e,children:"Cancel"}),o.jsx("button",{type:"submit",className:"btn primary",disabled:!r.trim()||c,children:c?"Creating…":"Create ticket"})]})]})})}const xh={board:Dc,agents:ih,artifacts:uh,logs:dh,settings:ph};function wh(){const e=W(c=>c.hydrate),t=W(c=>c.hydrateConfig),n=W(c=>c.connectSocket),r=W(c=>c.connected),l=Ke(c=>c.hydrate),s=Ze(c=>c.view),[i,a]=L.useState(!1),u=xh[s]??Dc;return L.useEffect(()=>{e(),t(),l(),n()},[]),o.jsxs("div",{className:"app",children:[o.jsx(Bp,{}),o.jsxs("div",{className:"main",children:[o.jsx(Qp,{onNewTicket:()=>a(!0)}),!r&&o.jsxs("div",{className:"banner warn",role:"status",children:[o.jsx(T,{name:"alert",size:13}),"Disconnected from the ouro server — retrying. Is ",o.jsx("code",{children:"ouro dashboard"})," still running?"]}),o.jsx("div",{className:"workspace",children:o.jsx(u,{})}),o.jsx(gh,{})]}),i&&o.jsx(yh,{onClose:()=>a(!1)})]})}is.createRoot(document.getElementById("root")).render(o.jsx(Na.StrictMode,{children:o.jsx(wh,{})}));
@@ -9,7 +9,7 @@
9
9
  flash white around a near-black app on load. -->
10
10
  <meta name="theme-color" content="#0b0a12" />
11
11
  <title>ouro</title>
12
- <script type="module" crossorigin src="/assets/index-B-lZ4RcX.js"></script>
12
+ <script type="module" crossorigin src="/assets/index-DERJjla9.js"></script>
13
13
  <link rel="stylesheet" crossorigin href="/assets/index-C21ZXjPS.css">
14
14
  </head>
15
15
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splinterzzz/ouro",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Loop engineering CLI: repo-rooted kanban + agent loop, powered by Claude Code / Codex headless mode (no API key required).",
5
5
  "type": "module",
6
6
  "keywords": [
package/src/lib/agents.js CHANGED
@@ -187,16 +187,22 @@ const SEEDS = [
187
187
  name: "Senior Engineer",
188
188
  glyph: "◆",
189
189
  description: "Ships production changes with minimal, well-tested diffs.",
190
- model: DEFAULT_MODEL,
191
- tools: ["Read", "Grep", "Glob", "Edit", "Write", "Bash"],
190
+ model: "opus",
191
+ tools: ["Read", "Grep", "Glob", "Edit", "Write", "Bash", "*"],
192
192
  },
193
- body: `You are a senior engineer working in an isolated git worktree.
193
+ body: `You are a senior engineer working in an isolated git worktree, and you orchestrate — you do not do every edit yourself.
194
+
195
+ Orchestration, before you touch anything:
196
+ 1. Break the ticket into subtasks. Independent ones (separate components, unrelated modules — nothing that shares state or a file with anything else in flight) are delegation candidates; anything ambiguous, architectural, or touching shared state is yours to do directly.
197
+ 2. Dispatch each independent subtask to a Task subagent rather than editing it yourself. If the Task tool exposes a model choice, request sonnet or haiku for these — applying a known, scoped change to one file is mechanical execution, not a judgment call, and doesn't need your tier. If it doesn't expose model choice, dispatch anyway — the parallelism is the primary win, the cost saving is secondary.
198
+ 3. Do the judgment work yourself: resolving the ticket's ambiguity, anything touching shared state, and the final integration once subagents land.
199
+ 4. Keep verification (typecheck/lint/tests) serial, after every subagent lands — never per-subtask, and never delegated.
194
200
 
195
201
  Work to these standards:
196
- - Read the surrounding code before you edit it. Match its idiom, naming, and comment density.
202
+ - Read the surrounding code before you edit it (yourself, or brief a subagent to). Match its idiom, naming, and comment density.
197
203
  - Prefer the smallest diff that fully solves the ticket. No drive-by refactors.
198
204
  - Never delete or weaken a test to make something pass.
199
- - Run the relevant tests before you call the work done, and report what you ran.
205
+ - Verify with the fastest check that actually proves correctness: typecheck + lint first. Only run a full production build if the ticket specifically needs to validate build output — tsc and lint already catch what a build would, slower.
200
206
  - If the ticket is ambiguous, state the assumption you made in your final message rather than guessing silently.`,
201
207
  },
202
208
  {
@@ -205,17 +211,18 @@ Work to these standards:
205
211
  name: "Bug Fixer",
206
212
  glyph: "▲",
207
213
  description: "Reproduces first, then fixes the root cause — not the symptom.",
208
- model: DEFAULT_MODEL,
209
- tools: ["Read", "Grep", "Glob", "Edit", "Write", "Bash"],
214
+ model: "opus",
215
+ tools: ["Read", "Grep", "Glob", "Edit", "Write", "Bash", "*"],
210
216
  },
211
- body: `You are a debugging specialist working in an isolated git worktree.
217
+ body: `You are a debugging specialist working in an isolated git worktree, and you orchestrate — evidence-gathering delegates, diagnosis doesn't.
212
218
 
213
219
  Method, in order:
214
220
  1. Reproduce the bug and state the exact failing behaviour you observed.
215
- 2. Find the root cause. Trace it — do not pattern-match a plausible-looking fix.
216
- 3. Fix the cause, not the symptom. If the real fix is out of scope, say so explicitly.
217
- 4. Add or extend a test that fails before your change and passes after it.
218
- 5. Report the reproduction, the cause, and the fix separately in your final message.`,
221
+ 2. Find the root cause. Trace it — do not pattern-match a plausible-looking fix. If more than one cause is plausible, dispatch a Task subagent per hypothesis to gather evidence for or against it in parallel, rather than chasing them one at a time. If the Task tool exposes a model choice, request sonnet or haiku for these — each is a bounded, scoped investigation, not the diagnosis itself.
222
+ 3. Weigh the evidence and decide the real cause yourself a subagent reports what it found, it does not get to conclude the diagnosis.
223
+ 4. Fix the cause, not the symptom. If the real fix is out of scope, say so explicitly.
224
+ 5. Add or extend a test that fails before your change and passes after it.
225
+ 6. Report the reproduction, the cause, and the fix separately in your final message.`,
219
226
  },
220
227
  {
221
228
  id: "reviewer",
@@ -34,10 +34,16 @@ function agentFlags(agent, restrictTo) {
34
34
  }
35
35
 
36
36
  const granted = agent.tools?.length ? agent.tools : DEFAULT_WRITE_TOOLS;
37
+ // "*" means fully unrestricted — every native Claude Code tool (including
38
+ // Task, for subagent fan-out) plus any project skills and connected
39
+ // MCP/plugin tools, none of which ouro's fixed TOOL_UNIVERSE enumerates.
40
+ // Only honored outside a read-only phase — Analyze/Plan/QA must never
41
+ // widen past restrictTo no matter what the agent itself requests.
42
+ const unrestricted = granted.includes("*") && !restrictTo;
37
43
  const allowed = restrictTo ? granted.filter((t) => restrictTo.includes(t)) : granted;
38
44
 
39
45
  const flags = [];
40
- if (allowed.length) flags.push("--allowedTools", allowed.join(","));
46
+ if (!unrestricted && allowed.length) flags.push("--allowedTools", allowed.join(","));
41
47
  if (agent.model) flags.push("--model", agent.model);
42
48
  // Append rather than replace: Claude Code's own system prompt carries the
43
49
  // tool contract, so blowing it away with --system-prompt breaks tool use.
@@ -73,15 +73,41 @@ function waitForPort(port, timeoutMs = 15000) {
73
73
  });
74
74
  }
75
75
 
76
+ // Most dev servers (Next, Vite, CRA, ...) print the URL they actually bound
77
+ // once they're up — which can differ from the guessed port if it was taken
78
+ // (Next silently increments to the next free one). Sniffing stdout for that
79
+ // line is the only way to know the real port rather than trusting the guess.
80
+ const URL_IN_OUTPUT = /https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0)(?::(\d+))?/i;
81
+
82
+ function sniffUrl(proc, timeoutMs) {
83
+ return new Promise((resolve) => {
84
+ let done = false;
85
+ const finish = (url) => {
86
+ if (done) return;
87
+ done = true;
88
+ proc.stdout?.off("data", onData);
89
+ resolve(url);
90
+ };
91
+ const onData = (chunk) => {
92
+ const match = URL_IN_OUTPUT.exec(chunk.toString());
93
+ if (match) finish(match[0].replace("0.0.0.0", "localhost"));
94
+ };
95
+ proc.stdout?.on("data", onData);
96
+ setTimeout(() => finish(null), timeoutMs);
97
+ });
98
+ }
99
+
76
100
  /**
77
101
  * Start (or restart) the preview for a ticket. Returns a status object; never
78
102
  * throws. `started:false` means nothing resolved — the card shows "no preview"
79
- * and the stage goes on.
103
+ * and the stage goes on. `reachable:false` means it launched but never came up
104
+ * (or came up on a port that never got confirmed) — callers must not surface
105
+ * `url` to the UI in that case, it points at nothing trustworthy.
80
106
  */
81
107
  export async function startPreview(ticketId, { cwd, signal } = {}) {
82
108
  stopPreview(ticketId); // never two for one ticket
83
109
 
84
- const { command, port, source } = await resolvePreview({ cwd, signal });
110
+ const { command, port: guessedPort, source } = await resolvePreview({ cwd, signal });
85
111
  if (!command) return { started: false, reason: "no preview command resolved", source };
86
112
 
87
113
  let proc;
@@ -91,13 +117,21 @@ export async function startPreview(ticketId, { cwd, signal } = {}) {
91
117
  return { started: false, reason: `spawn failed: ${err.message || err}`, source };
92
118
  }
93
119
  proc.on("error", () => {}); // a preview that fails to launch must not crash the stage
94
- proc.stdout?.on("data", () => {});
95
120
  proc.stderr?.on("data", () => {});
96
121
 
97
- const url = port ? `http://localhost:${port}` : null;
98
- previews.set(ticketId, { proc, url, command, port });
122
+ const timeoutMs = 15000;
123
+ const [sniffed, portOpen] = await Promise.all([
124
+ sniffUrl(proc, timeoutMs),
125
+ guessedPort ? waitForPort(guessedPort, timeoutMs) : Promise.resolve(false),
126
+ ]);
127
+ // stdout gave us the real bound URL — trust it over the pre-spawn guess,
128
+ // since that's the only way to catch Next.js et al. auto-incrementing off
129
+ // an already-occupied port.
130
+ const url = sniffed ?? (portOpen ? `http://localhost:${guessedPort}` : null);
131
+ const port = sniffed ? Number(URL_IN_OUTPUT.exec(sniffed)?.[1] ?? guessedPort) : guessedPort;
132
+ const reachable = Boolean(sniffed) || portOpen;
99
133
 
100
- const reachable = port ? await waitForPort(port) : false;
134
+ previews.set(ticketId, { proc, url, command, port });
101
135
  return { started: true, url, command, port, source, reachable };
102
136
  }
103
137
 
@@ -9,7 +9,7 @@ import { createTicketWorktree, diffWorktree } from "../lib/worktree.js";
9
9
  import { contextManifest, listContextFiles, listReferencedFiles } from "../lib/artifacts.js";
10
10
  import { appendRunLog, readRunLog } from "../lib/ouroLog.js";
11
11
  import { runTests } from "../lib/staging.js";
12
- import { startPreview, stopPreview, previewInfo, stopAllPreviews } from "../lib/preview.js";
12
+ import { startPreview, stopPreview, previewInfo } from "../lib/preview.js";
13
13
  import { readConfig, writeConfig, getDefaultMode, setDefaultMode, getAutoShip, telegramTokenVar } from "../lib/config.js";
14
14
  import { readEnvVars, writeEnvVars } from "../lib/env.js";
15
15
  import { looksLikeToken, maskToken, verifyBotToken } from "../lib/telegram.js";
@@ -289,9 +289,17 @@ export function createServer() {
289
289
 
290
290
  // Preview once, reused across QA attempts; torn down when the ticket leaves.
291
291
  const previewStatus = await startPreview(ticketId, { cwd: base.worktree, signal }).catch(() => null);
292
+ // Only surface a URL once it's confirmed reachable — an unreachable one
293
+ // (or a guessed port that was never actually confirmed) is worse than no
294
+ // link, since it silently points at nothing or, worse, someone else's
295
+ // server that happened to already be on that port.
292
296
  store.update(ticketId, {
293
- previewUrl: previewStatus?.url ?? null,
294
- previewNote: previewStatus?.started ? null : "no preview configured",
297
+ previewUrl: previewStatus?.reachable ? previewStatus.url : null,
298
+ previewNote: !previewStatus?.started
299
+ ? "no preview configured"
300
+ : previewStatus.reachable
301
+ ? null
302
+ : "preview didn't come up in time",
295
303
  });
296
304
 
297
305
  while (!signal.aborted) {