@splinterzzz/ouro 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard-dist/assets/{index-Btdawh-I.js → index-D-niaPon.js} +1 -1
- package/dashboard-dist/index.html +1 -1
- package/package.json +1 -1
- package/src/lib/claudeCodeExec.js +4 -1
- package/src/lib/codexExec.js +3 -1
- package/src/lib/config.js +4 -0
- package/src/lib/preview.js +2 -2
- package/src/lib/staging.js +63 -25
- package/src/lib/store.js +3 -1
- package/src/server/index.js +42 -23
|
@@ -53,7 +53,7 @@ Error generating stack: `+s.message+`
|
|
|
53
53
|
*
|
|
54
54
|
* This source code is licensed under the MIT license found in the
|
|
55
55
|
* LICENSE file in the root directory of this source tree.
|
|
56
|
-
*/var Tl=z,Sp=kp;function jp(e,t){return e===t&&(e!==0||1/e===1/t)||e!==e&&t!==t}var Np=typeof Object.is=="function"?Object.is:jp,Ep=Sp.useSyncExternalStore,Cp=Tl.useRef,_p=Tl.useEffect,Tp=Tl.useMemo,zp=Tl.useDebugValue;Tc.useSyncExternalStoreWithSelector=function(e,t,n,r,l){var s=Cp(null);if(s.current===null){var i={hasValue:!1,value:null};s.current=i}else i=s.current;s=Tp(function(){function u(x){if(!c){if(c=!0,v=x,x=r(x),l!==void 0&&i.hasValue){var y=i.value;if(l(y,x))return h=y}return h=x}if(y=h,Np(v,x))return y;var w=r(x);return l!==void 0&&l(y,w)?(v=x,y):(v=x,h=w)}var c=!1,v,h,m=n===void 0?null:n;return[function(){return u(t())},m===null?void 0:function(){return u(m())}]},[t,n,r,l]);var a=Ep(e,s[0],s[1]);return _p(function(){i.hasValue=!0,i.value=a},[a]),zp(a),a};_c.exports=Tc;var Pp=_c.exports;const Lp=pa(Pp),Lc={},{useDebugValue:Mp}=Na,{useSyncExternalStoreWithSelector:Rp}=Lp;let sa=!1;const Ip=e=>e;function Op(e,t=Ip,n){(Lc?"production":void 0)!=="production"&&n&&!sa&&(console.warn("[DEPRECATED] Use `createWithEqualityFn` instead of `create` or use `useStoreWithEqualityFn` instead of `useStore`. They can be imported from 'zustand/traditional'. https://github.com/pmndrs/zustand/discussions/1937"),sa=!0);const r=Rp(e.subscribe,e.getState,e.getServerState||e.getInitialState,t,n);return Mp(r),r}const ia=e=>{(Lc?"production":void 0)!=="production"&&typeof e!="function"&&console.warn("[DEPRECATED] Passing a vanilla store will be unsupported in a future version. Instead use `import { useStore } from 'zustand'`.");const t=typeof e=="function"?dp(e):e,n=(r,l)=>Op(t,r,l);return Object.assign(n,t),n},zl=e=>e?ia(e):ia,Qe=zl((e,t)=>({agents:[],toolUniverse:[],defaultAgentId:null,loading:!0,error:null,selectedId:null,async hydrate(){try{const n=await fetch("/api/agents");if(!n.ok)throw new Error(`GET /api/agents → ${n.status}`);const{agents:r,toolUniverse:l,defaultAgentId:s}=await n.json();e(i=>{var a;return{agents:r,toolUniverse:l,defaultAgentId:s,loading:!1,error:null,selectedId:r.some(u=>u.id===i.selectedId)?i.selectedId:((a=r[0])==null?void 0:a.id)??null}})}catch(n){e({loading:!1,error:String(n.message||n)})}},select(n){e({selectedId:n})},async create(n){const r=await fetch("/api/agents",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:n})});if(!r.ok)throw new Error((await r.json().catch(()=>({}))).error??`HTTP ${r.status}`);const l=await r.json();return await t().hydrate(),e({selectedId:l.id}),l},async save(n,r){const l=await fetch(`/api/agents/${n}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});if(!l.ok)throw new Error((await l.json().catch(()=>({}))).error??`HTTP ${l.status}`);const s=await l.json();return await t().hydrate(),s},async remove(n){const r=await fetch(`/api/agents/${n}`,{method:"DELETE"});if(!r.ok)throw new Error(`HTTP ${r.status}`);e({selectedId:null}),await t().hydrate()},async fetchRaw(n){const r=await fetch(`/api/agents/${n}`);if(!r.ok)throw new Error(`HTTP ${r.status}`);return(await r.json()).raw??""}}));let Ue=null,oa=null,zr=500;function Dp(e){if(e.type==="error")return"bad";if(e.type==="cancelled"||e.type==="warn")return"warn";if(e.type==="ship-done")return"good";if(e.type==="ship")return"run";const t=e.event??{};return t.type==="stderr"?"warn":t.type==="result"?t.is_error?"bad":"good":t.type==="raw"||t.type==="rate_limit_event"||t.type==="system"&&t.subtype!=="init"?"dim":"run"}function $p(e){var r,l,s;if(e.type==="error")return`error: ${e.text}`;if(e.type==="cancelled")return e.text||"cancelled";if(e.type==="ship"||e.type==="ship-done"||e.type==="warn")return e.text;const t=e.event;if(!t)return JSON.stringify(e).slice(0,200);if(t.type==="raw"||t.type==="stderr")return(t.text??"").trim();if(t.type==="system")return t.subtype==="init"?`session started · ${t.model??"ready"}`:`· ${t.subtype??"system"}`;if(t.type==="rate_limit_event")return"· rate limit notice";if(t.type==="result"){const i=[t.is_error?"failed":"done"];return t.num_turns&&i.push(`${t.num_turns} turns`),t.duration_ms&&i.push(`${(t.duration_ms/1e3).toFixed(1)}s`),`${t.is_error?"✗":"✓"} ${i.join(" · ")}`}const n=(r=t.message)==null?void 0:r.content;if(Array.isArray(n)){const i=[];for(const a of n)if(a.type==="tool_use"){const u=a.input??{},c=u.file_path??u.path??u.command??u.pattern??u.prompt??"";i.push(`▶ ${a.name}${c?` ${String(c).slice(0,90)}`:""}`)}else a.type==="text"&&((l=a.text)!=null&&l.trim())?i.push(a.text.trim().slice(0,160)):a.type==="thinking"?i.push("· thinking"):a.type==="tool_result"&&i.push("◂ result");if(i.length)return i.join(" ")}return(s=t.item)!=null&&s.text?`${t.type}: ${t.item.text.slice(0,140)}`:`· ${t.type??"event"}`}let aa=0;const B=zl((e,t)=>({tickets:[],connected:!1,backend:null,defaultMode:"human",feed:[],selectedId:null,select(n){e(r=>({selectedId:r.selectedId===n?null:n}))},async hydrate(){const r=await(await fetch("/api/tickets")).json(),l=r.flatMap(s=>(s.log??[]).map(i=>({...ua(s.id,i),id:++aa}))).sort((s,i)=>s.ts.localeCompare(i.ts)).slice(-600);e({tickets:r,feed:l})},async hydrateConfig(){const n=await fetch("/api/config"),{backend:r,defaultMode:l}=await n.json();e({backend:r,defaultMode:l??"human"})},async setBackend(n){const r=t().backend;e({backend:n});try{const l=await fetch("/api/config/backend",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({backend:n})});if(!l.ok)throw new Error(await l.text());const s=await l.json();e({backend:s.backend??n})}catch{e({backend:r})}},async setDefaultMode(n){const r=t().defaultMode;e({defaultMode:n});try{const l=await fetch("/api/config/mode",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({mode:n})});if(!l.ok)throw new Error(await l.text())}catch{e({defaultMode:r})}},connectSocket(){if(Ue&&Ue.readyState<=1)return;const n=location.protocol==="https:"?"wss":"ws";Ue=new WebSocket(`${n}://${location.host}/ws`),Ue.onopen=()=>{zr=500,e({connected:!0}),t().hydrate(),t().hydrateConfig()},Ue.onclose=()=>{e({connected:!1}),clearTimeout(oa),oa=setTimeout(()=>t().connectSocket(),zr),zr=Math.min(zr*2,8e3)},Ue.onerror=()=>Ue==null?void 0:Ue.close(),Ue.onmessage=r=>{let l;try{l=JSON.parse(r.data)}catch{return}t().applyEvent(l)}},applyEvent(n){if(n.type==="log"){const l={...ua(n.ticketId,n.entry),id:++aa};e(s=>({feed:[...s.feed,l].slice(-600),tickets:s.tickets.map(i=>i.id===n.ticketId?{...i,log:[...i.log??[],n.entry].slice(-400)}:i)}));return}if(n.type==="deleted"){e(l=>({tickets:l.tickets.filter(s=>s.id!==n.ticketId),selectedId:l.selectedId===n.ticketId?null:l.selectedId}));return}if(n.type==="config"){e({defaultMode:n.defaultMode});return}if(n.type==="agent"||n.type==="agent-deleted"){Qe.getState().hydrate();return}const r=n.ticket;r&&e(l=>({tickets:l.tickets.some(i=>i.id===r.id)?l.tickets.map(i=>i.id===r.id?r:i):[r,...l.tickets]}))},clearFeed(){e({feed:[]})},async setMode(n,r){await Me(`/api/tickets/${n}/mode`,{mode:r})},async setAgent(n,r){await Me(`/api/tickets/${n}/agent`,{agentId:r})},async analyzeTicket(n){await Me(`/api/tickets/${n}/analyze`)},async runTicket(n){await Me(`/api/tickets/${n}/run`)},async approveTicket(n){await Me(`/api/tickets/${n}/approve`)},async cancelTicket(n){await Me(`/api/tickets/${n}/cancel`)},async reopenTicket(n){await Me(`/api/tickets/${n}/reopen`)},async qaApprove(n){await Me(`/api/tickets/${n}/qa/approve`)},async qaReject(n){await Me(`/api/tickets/${n}/qa/reject`)},async shipTicket(n){const r=await Me(`/api/tickets/${n}/ship`);return r.json().catch(()=>({ok:!1,error:`HTTP ${r.status}`}))},async deleteTicket(n){await fetch(`/api/tickets/${n}`,{method:"DELETE"})},async createTicket(n){const r=await Me("/api/tickets",{source:"manual",...n});return r==null?void 0:r.json()}}));function ua(e,t){return{ticketId:e,ts:t.ts??new Date().toISOString(),tone:Dp(t),text:$p(t)}}function Me(e,t){return fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},...t?{body:JSON.stringify(t)}:{}})}const Mc="ouro.ui.v1",ca={view:"board",railCollapsed:!1,ticketsOpen:!0,agentsOpen:!0,terminalOpen:!0,terminalHeight:200,autoscroll:!0},Rc=90,Ic=560;function Fp(){try{return{...ca,...JSON.parse(localStorage.getItem(Mc)??"{}")}}catch{return{...ca}}}const Xe=zl((e,t)=>({...Fp(),set(n){e(n);const{view:r,railCollapsed:l,ticketsOpen:s,agentsOpen:i,terminalOpen:a,terminalHeight:u,autoscroll:c}=t();try{localStorage.setItem(Mc,JSON.stringify({view:r,railCollapsed:l,ticketsOpen:s,agentsOpen:i,terminalOpen:a,terminalHeight:u,autoscroll:c}))}catch{}},toggle(n){t().set({[n]:!t()[n]})},setTerminalHeight(n){t().set({terminalHeight:Math.min(Ic,Math.max(Rc,n))})}})),Ap={inbox:"M22 12h-6l-2 3h-4l-2-3H2 M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z",agents:"M12 8V4H8 M4 8h16v12H4z M2 14h2 M20 14h2 M15 13v2 M9 13v2",play:"m6 3 14 9-14 9V3z",stop:"M5 5h14v14H5z",check:"M20 6 9 17l-5-5",x:"M18 6 6 18 M6 6l12 12",chevronRight:"m9 18 6-6-6-6",chevronDown:"m6 9 6 6 6-6",chevronsLeft:"m11 17-5-5 5-5 M18 17l-5-5 5-5",chevronsRight:"m6 17 5-5-5-5 M13 17l5-5-5-5",plus:"M5 12h14 M12 5v14",terminal:"m4 17 6-6-6-6 M12 19h8",trash:"M3 6h18 M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",rotate:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8 M3 3v5h5",save:"M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z M17 21v-8H7v8 M7 3v5h8",file:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z M14 2v4a2 2 0 0 0 2 2h4",alert:"M12 9v4 M12 17h.01 M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z",search:"m21 21-4.34-4.34 M17 11a6 6 0 1 1-12 0 6 6 0 0 1 12 0z",gitBranch:"M6 3v12 M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6z M6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z M15 9a9 9 0 0 1-9 9",settings:"M20 7h-9 M14 17H5 M17 14a3 3 0 1 0 0 6 3 3 0 0 0 0-6z M7 4a3 3 0 1 0 0 6 3 3 0 0 0 0-6z",send:"m22 2-7 20-4-9-9-4Z M22 2 11 13"};function T({name:e,size:t=16,className:n="",...r}){const l=Ap[e];return l?o.jsx("svg",{className:`icon ${n}`,width:t,height:t,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",focusable:"false",...r,children:l.split(" M").map((s,i)=>o.jsx("path",{d:i===0?s:`M${s}`},i))}):null}const Up=[[100,4],[135,2.5],[170,4],[205,3],[240,4.5],[275,2.5],[310,4],[340,3]];function Vp({size:e=20,title:t,className:n=""}){return o.jsxs("svg",{className:`logo ${n}`,width:e,height:e,viewBox:"0 0 64 64",fill:"none",stroke:"currentColor",strokeLinecap:"round",strokeLinejoin:"round",role:t?"img":void 0,"aria-hidden":t?void 0:"true","aria-label":t,children:[t&&o.jsx("title",{children:t}),o.jsx("path",{d:"M43 12.95A22 22 0 1 1 28.18 10.33",strokeWidth:"4"}),o.jsx("path",{d:"M28.18 10.33A22 22 0 0 1 32 10",strokeWidth:"2.6"}),o.jsx("path",{d:"M32 10A22 22 0 0 1 33.92 10.08",strokeWidth:"1.5"}),o.jsx("path",{d:"M43 12.95A22 22 0 1 1 28.18 10.33",strokeWidth:"1.3",opacity:"0.7"}),o.jsxs("g",{opacity:"0.4",strokeWidth:"0.8",children:[Up.map(([r,l])=>o.jsxs("g",{transform:`rotate(${r} 32 32)`,children:[o.jsx("path",{d:`M32 13v${l}`}),o.jsx("circle",{cx:"32",cy:13+l,r:"0.85",fill:"currentColor",stroke:"none"})]},r)),o.jsx("path",{d:"M15.26 34.95A17 17 0 0 0 23.5 46.72"}),o.jsx("path",{d:"M43.4 43.4A17 17 0 0 0 48.4 36.1"})]}),o.jsx("path",{d:`M42.8 14.8
|
|
56
|
+
*/var Tl=z,Sp=kp;function jp(e,t){return e===t&&(e!==0||1/e===1/t)||e!==e&&t!==t}var Np=typeof Object.is=="function"?Object.is:jp,Ep=Sp.useSyncExternalStore,Cp=Tl.useRef,_p=Tl.useEffect,Tp=Tl.useMemo,zp=Tl.useDebugValue;Tc.useSyncExternalStoreWithSelector=function(e,t,n,r,l){var s=Cp(null);if(s.current===null){var i={hasValue:!1,value:null};s.current=i}else i=s.current;s=Tp(function(){function u(x){if(!c){if(c=!0,v=x,x=r(x),l!==void 0&&i.hasValue){var y=i.value;if(l(y,x))return h=y}return h=x}if(y=h,Np(v,x))return y;var w=r(x);return l!==void 0&&l(y,w)?(v=x,y):(v=x,h=w)}var c=!1,v,h,m=n===void 0?null:n;return[function(){return u(t())},m===null?void 0:function(){return u(m())}]},[t,n,r,l]);var a=Ep(e,s[0],s[1]);return _p(function(){i.hasValue=!0,i.value=a},[a]),zp(a),a};_c.exports=Tc;var Pp=_c.exports;const Lp=pa(Pp),Lc={},{useDebugValue:Mp}=Na,{useSyncExternalStoreWithSelector:Rp}=Lp;let sa=!1;const Ip=e=>e;function Op(e,t=Ip,n){(Lc?"production":void 0)!=="production"&&n&&!sa&&(console.warn("[DEPRECATED] Use `createWithEqualityFn` instead of `create` or use `useStoreWithEqualityFn` instead of `useStore`. They can be imported from 'zustand/traditional'. https://github.com/pmndrs/zustand/discussions/1937"),sa=!0);const r=Rp(e.subscribe,e.getState,e.getServerState||e.getInitialState,t,n);return Mp(r),r}const ia=e=>{(Lc?"production":void 0)!=="production"&&typeof e!="function"&&console.warn("[DEPRECATED] Passing a vanilla store will be unsupported in a future version. Instead use `import { useStore } from 'zustand'`.");const t=typeof e=="function"?dp(e):e,n=(r,l)=>Op(t,r,l);return Object.assign(n,t),n},zl=e=>e?ia(e):ia,Qe=zl((e,t)=>({agents:[],toolUniverse:[],defaultAgentId:null,loading:!0,error:null,selectedId:null,async hydrate(){try{const n=await fetch("/api/agents");if(!n.ok)throw new Error(`GET /api/agents → ${n.status}`);const{agents:r,toolUniverse:l,defaultAgentId:s}=await n.json();e(i=>{var a;return{agents:r,toolUniverse:l,defaultAgentId:s,loading:!1,error:null,selectedId:r.some(u=>u.id===i.selectedId)?i.selectedId:((a=r[0])==null?void 0:a.id)??null}})}catch(n){e({loading:!1,error:String(n.message||n)})}},select(n){e({selectedId:n})},async create(n){const r=await fetch("/api/agents",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:n})});if(!r.ok)throw new Error((await r.json().catch(()=>({}))).error??`HTTP ${r.status}`);const l=await r.json();return await t().hydrate(),e({selectedId:l.id}),l},async save(n,r){const l=await fetch(`/api/agents/${n}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});if(!l.ok)throw new Error((await l.json().catch(()=>({}))).error??`HTTP ${l.status}`);const s=await l.json();return await t().hydrate(),s},async remove(n){const r=await fetch(`/api/agents/${n}`,{method:"DELETE"});if(!r.ok)throw new Error(`HTTP ${r.status}`);e({selectedId:null}),await t().hydrate()},async fetchRaw(n){const r=await fetch(`/api/agents/${n}`);if(!r.ok)throw new Error(`HTTP ${r.status}`);return(await r.json()).raw??""}}));let Ue=null,oa=null,zr=500;function Dp(e){if(e.type==="error")return"bad";if(e.type==="cancelled"||e.type==="warn")return"warn";if(e.type==="ship-done")return"good";if(e.type==="ship")return"run";const t=e.event??{};return t.type==="stderr"?"warn":t.type==="result"?t.is_error?"bad":"good":t.type==="raw"||t.type==="rate_limit_event"||t.type==="system"&&t.subtype!=="init"?"dim":"run"}function $p(e){var r,l,s;if(e.type==="error")return`error: ${e.text}`;if(e.type==="cancelled")return e.text||"cancelled";if(e.type==="ship"||e.type==="ship-done"||e.type==="warn")return e.text;const t=e.event;if(!t)return JSON.stringify(e).slice(0,200);if(t.type==="raw"||t.type==="stderr")return(t.text??"").trim();if(t.type==="system")return t.subtype==="init"?`session started · ${t.model??"ready"}`:`· ${t.subtype??"system"}`;if(t.type==="rate_limit_event")return"· rate limit notice";if(t.type==="result"){const i=[t.is_error?"failed":"done"];return t.num_turns&&i.push(`${t.num_turns} turns`),t.duration_ms&&i.push(`${(t.duration_ms/1e3).toFixed(1)}s`),`${t.is_error?"✗":"✓"} ${i.join(" · ")}`}const n=(r=t.message)==null?void 0:r.content;if(Array.isArray(n)){const i=[];for(const a of n)if(a.type==="tool_use"){const u=a.input??{},c=u.file_path??u.path??u.command??u.pattern??u.prompt??"";i.push(`▶ ${a.name}${c?` ${String(c).slice(0,90)}`:""}`)}else a.type==="text"&&((l=a.text)!=null&&l.trim())?i.push(a.text.trim().slice(0,160)):a.type==="tool_result"&&i.push("◂ result");return i.length?i.join(" "):null}return(s=t.item)!=null&&s.text?`${t.type}: ${t.item.text.slice(0,140)}`:`· ${t.type??"event"}`}let aa=0;const B=zl((e,t)=>({tickets:[],connected:!1,backend:null,defaultMode:"human",feed:[],selectedId:null,select(n){e(r=>({selectedId:r.selectedId===n?null:n}))},async hydrate(){const r=await(await fetch("/api/tickets")).json(),l=r.flatMap(s=>(s.log??[]).map(i=>({...ua(s.id,i),id:++aa}))).filter(s=>s.text!=null).sort((s,i)=>s.ts.localeCompare(i.ts)).slice(-600);e({tickets:r,feed:l})},async hydrateConfig(){const n=await fetch("/api/config"),{backend:r,defaultMode:l}=await n.json();e({backend:r,defaultMode:l??"human"})},async setBackend(n){const r=t().backend;e({backend:n});try{const l=await fetch("/api/config/backend",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({backend:n})});if(!l.ok)throw new Error(await l.text());const s=await l.json();e({backend:s.backend??n})}catch{e({backend:r})}},async setDefaultMode(n){const r=t().defaultMode;e({defaultMode:n});try{const l=await fetch("/api/config/mode",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({mode:n})});if(!l.ok)throw new Error(await l.text())}catch{e({defaultMode:r})}},connectSocket(){if(Ue&&Ue.readyState<=1)return;const n=location.protocol==="https:"?"wss":"ws";Ue=new WebSocket(`${n}://${location.host}/ws`),Ue.onopen=()=>{zr=500,e({connected:!0}),t().hydrate(),t().hydrateConfig()},Ue.onclose=()=>{e({connected:!1}),clearTimeout(oa),oa=setTimeout(()=>t().connectSocket(),zr),zr=Math.min(zr*2,8e3)},Ue.onerror=()=>Ue==null?void 0:Ue.close(),Ue.onmessage=r=>{let l;try{l=JSON.parse(r.data)}catch{return}t().applyEvent(l)}},applyEvent(n){if(n.type==="log"){const l={...ua(n.ticketId,n.entry),id:++aa};e(s=>({feed:l.text==null?s.feed:[...s.feed,l].slice(-600),tickets:s.tickets.map(i=>i.id===n.ticketId?{...i,log:[...i.log??[],n.entry].slice(-400)}:i)}));return}if(n.type==="deleted"){e(l=>({tickets:l.tickets.filter(s=>s.id!==n.ticketId),selectedId:l.selectedId===n.ticketId?null:l.selectedId}));return}if(n.type==="config"){e({defaultMode:n.defaultMode});return}if(n.type==="agent"||n.type==="agent-deleted"){Qe.getState().hydrate();return}const r=n.ticket;r&&e(l=>({tickets:l.tickets.some(i=>i.id===r.id)?l.tickets.map(i=>i.id===r.id?r:i):[r,...l.tickets]}))},clearFeed(){e({feed:[]})},async setMode(n,r){await Me(`/api/tickets/${n}/mode`,{mode:r})},async setAgent(n,r){await Me(`/api/tickets/${n}/agent`,{agentId:r})},async analyzeTicket(n){await Me(`/api/tickets/${n}/analyze`)},async runTicket(n){await Me(`/api/tickets/${n}/run`)},async approveTicket(n){await Me(`/api/tickets/${n}/approve`)},async cancelTicket(n){await Me(`/api/tickets/${n}/cancel`)},async reopenTicket(n){await Me(`/api/tickets/${n}/reopen`)},async qaApprove(n){await Me(`/api/tickets/${n}/qa/approve`)},async qaReject(n){await Me(`/api/tickets/${n}/qa/reject`)},async shipTicket(n){const r=await Me(`/api/tickets/${n}/ship`);return r.json().catch(()=>({ok:!1,error:`HTTP ${r.status}`}))},async deleteTicket(n){await fetch(`/api/tickets/${n}`,{method:"DELETE"})},async createTicket(n){const r=await Me("/api/tickets",{source:"manual",...n});return r==null?void 0:r.json()}}));function ua(e,t){return{ticketId:e,ts:t.ts??new Date().toISOString(),tone:Dp(t),text:$p(t)}}function Me(e,t){return fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},...t?{body:JSON.stringify(t)}:{}})}const Mc="ouro.ui.v1",ca={view:"board",railCollapsed:!1,ticketsOpen:!0,agentsOpen:!0,terminalOpen:!0,terminalHeight:200,autoscroll:!0},Rc=90,Ic=560;function Fp(){try{return{...ca,...JSON.parse(localStorage.getItem(Mc)??"{}")}}catch{return{...ca}}}const Xe=zl((e,t)=>({...Fp(),set(n){e(n);const{view:r,railCollapsed:l,ticketsOpen:s,agentsOpen:i,terminalOpen:a,terminalHeight:u,autoscroll:c}=t();try{localStorage.setItem(Mc,JSON.stringify({view:r,railCollapsed:l,ticketsOpen:s,agentsOpen:i,terminalOpen:a,terminalHeight:u,autoscroll:c}))}catch{}},toggle(n){t().set({[n]:!t()[n]})},setTerminalHeight(n){t().set({terminalHeight:Math.min(Ic,Math.max(Rc,n))})}})),Ap={inbox:"M22 12h-6l-2 3h-4l-2-3H2 M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z",agents:"M12 8V4H8 M4 8h16v12H4z M2 14h2 M20 14h2 M15 13v2 M9 13v2",play:"m6 3 14 9-14 9V3z",stop:"M5 5h14v14H5z",check:"M20 6 9 17l-5-5",x:"M18 6 6 18 M6 6l12 12",chevronRight:"m9 18 6-6-6-6",chevronDown:"m6 9 6 6 6-6",chevronsLeft:"m11 17-5-5 5-5 M18 17l-5-5 5-5",chevronsRight:"m6 17 5-5-5-5 M13 17l5-5-5-5",plus:"M5 12h14 M12 5v14",terminal:"m4 17 6-6-6-6 M12 19h8",trash:"M3 6h18 M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",rotate:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8 M3 3v5h5",save:"M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z M17 21v-8H7v8 M7 3v5h8",file:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z M14 2v4a2 2 0 0 0 2 2h4",alert:"M12 9v4 M12 17h.01 M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z",search:"m21 21-4.34-4.34 M17 11a6 6 0 1 1-12 0 6 6 0 0 1 12 0z",gitBranch:"M6 3v12 M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6z M6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z M15 9a9 9 0 0 1-9 9",settings:"M20 7h-9 M14 17H5 M17 14a3 3 0 1 0 0 6 3 3 0 0 0 0-6z M7 4a3 3 0 1 0 0 6 3 3 0 0 0 0-6z",send:"m22 2-7 20-4-9-9-4Z M22 2 11 13"};function T({name:e,size:t=16,className:n="",...r}){const l=Ap[e];return l?o.jsx("svg",{className:`icon ${n}`,width:t,height:t,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",focusable:"false",...r,children:l.split(" M").map((s,i)=>o.jsx("path",{d:i===0?s:`M${s}`},i))}):null}const Up=[[100,4],[135,2.5],[170,4],[205,3],[240,4.5],[275,2.5],[310,4],[340,3]];function Vp({size:e=20,title:t,className:n=""}){return o.jsxs("svg",{className:`logo ${n}`,width:e,height:e,viewBox:"0 0 64 64",fill:"none",stroke:"currentColor",strokeLinecap:"round",strokeLinejoin:"round",role:t?"img":void 0,"aria-hidden":t?void 0:"true","aria-label":t,children:[t&&o.jsx("title",{children:t}),o.jsx("path",{d:"M43 12.95A22 22 0 1 1 28.18 10.33",strokeWidth:"4"}),o.jsx("path",{d:"M28.18 10.33A22 22 0 0 1 32 10",strokeWidth:"2.6"}),o.jsx("path",{d:"M32 10A22 22 0 0 1 33.92 10.08",strokeWidth:"1.5"}),o.jsx("path",{d:"M43 12.95A22 22 0 1 1 28.18 10.33",strokeWidth:"1.3",opacity:"0.7"}),o.jsxs("g",{opacity:"0.4",strokeWidth:"0.8",children:[Up.map(([r,l])=>o.jsxs("g",{transform:`rotate(${r} 32 32)`,children:[o.jsx("path",{d:`M32 13v${l}`}),o.jsx("circle",{cx:"32",cy:13+l,r:"0.85",fill:"currentColor",stroke:"none"})]},r)),o.jsx("path",{d:"M15.26 34.95A17 17 0 0 0 23.5 46.72"}),o.jsx("path",{d:"M43.4 43.4A17 17 0 0 0 48.4 36.1"})]}),o.jsx("path",{d:`M42.8 14.8
|
|
57
57
|
C46.6 14.6 49.6 12.3 49.4 8.8
|
|
58
58
|
C49.2 6.0 46.8 4.5 43.6 5.0
|
|
59
59
|
L33.6 7.7
|
|
@@ -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-
|
|
12
|
+
<script type="module" crossorigin src="/assets/index-D-niaPon.js"></script>
|
|
13
13
|
<link rel="stylesheet" crossorigin href="/assets/index-C21ZXjPS.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body>
|
package/package.json
CHANGED
|
@@ -55,7 +55,10 @@ function runClaude(args, { cwd, onEvent, signal } = {}) {
|
|
|
55
55
|
return new Promise((resolve, reject) => {
|
|
56
56
|
// `signal` wires cancellation straight to the child: aborting SIGTERMs the
|
|
57
57
|
// CLI rather than leaving it running detached. See lib/runs.js.
|
|
58
|
-
|
|
58
|
+
// windowsHide: the dashboard daemon spawns this per run — without it, each
|
|
59
|
+
// run flashes an empty `claude` console window on Windows. Output is piped
|
|
60
|
+
// and parsed here, so the child never needs a visible console of its own.
|
|
61
|
+
const proc = spawn(CLAUDE_BIN, args, { cwd, env: process.env, signal, windowsHide: true });
|
|
59
62
|
|
|
60
63
|
const rl = readline.createInterface({ input: proc.stdout });
|
|
61
64
|
let sessionId = null;
|
package/src/lib/codexExec.js
CHANGED
|
@@ -63,7 +63,9 @@ function agentFlags(agent) {
|
|
|
63
63
|
function runCodexExec(args, { cwd, onEvent, signal } = {}) {
|
|
64
64
|
return new Promise((resolve, reject) => {
|
|
65
65
|
// `signal` makes cancellation kill the child process. See lib/runs.js.
|
|
66
|
-
|
|
66
|
+
// windowsHide: keep the per-run child from flashing an empty console window
|
|
67
|
+
// on Windows — output is piped and parsed here, not shown in a console.
|
|
68
|
+
const proc = spawn(CODEX_BIN, args, { cwd, env: process.env, signal, windowsHide: true });
|
|
67
69
|
|
|
68
70
|
const rl = readline.createInterface({ input: proc.stdout });
|
|
69
71
|
let sessionId = null;
|
package/src/lib/config.js
CHANGED
|
@@ -30,6 +30,8 @@ const DEFAULTS = {
|
|
|
30
30
|
// preview is expected to listen (used to build the clickable URL).
|
|
31
31
|
staging: {
|
|
32
32
|
testCommand: null,
|
|
33
|
+
lintCommand: null,
|
|
34
|
+
buildCommand: null,
|
|
33
35
|
previewCommand: null,
|
|
34
36
|
previewPort: null,
|
|
35
37
|
},
|
|
@@ -108,6 +110,8 @@ export function getStaging() {
|
|
|
108
110
|
const s = readConfig().staging ?? {};
|
|
109
111
|
return {
|
|
110
112
|
testCommand: s.testCommand ?? null,
|
|
113
|
+
lintCommand: s.lintCommand ?? null,
|
|
114
|
+
buildCommand: s.buildCommand ?? null,
|
|
111
115
|
previewCommand: s.previewCommand ?? null,
|
|
112
116
|
previewPort: s.previewPort ?? null,
|
|
113
117
|
};
|
package/src/lib/preview.js
CHANGED
|
@@ -45,7 +45,7 @@ function killTree(proc) {
|
|
|
45
45
|
if (!proc?.pid) return;
|
|
46
46
|
try {
|
|
47
47
|
if (process.platform === "win32") {
|
|
48
|
-
spawn("taskkill", ["/pid", String(proc.pid), "/T", "/F"], { stdio: "ignore" });
|
|
48
|
+
spawn("taskkill", ["/pid", String(proc.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
|
|
49
49
|
} else {
|
|
50
50
|
proc.kill("SIGTERM");
|
|
51
51
|
}
|
|
@@ -112,7 +112,7 @@ export async function startPreview(ticketId, { cwd, signal } = {}) {
|
|
|
112
112
|
|
|
113
113
|
let proc;
|
|
114
114
|
try {
|
|
115
|
-
proc = spawn(command, { cwd, shell: true });
|
|
115
|
+
proc = spawn(command, { cwd, shell: true, windowsHide: true });
|
|
116
116
|
} catch (err) {
|
|
117
117
|
return { started: false, reason: `spawn failed: ${err.message || err}`, source };
|
|
118
118
|
}
|
package/src/lib/staging.js
CHANGED
|
@@ -2,37 +2,57 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { getStaging } from "./config.js";
|
|
3
3
|
import { askJson } from "./agentBackend.js";
|
|
4
4
|
|
|
5
|
-
// Staging
|
|
6
|
-
// QA agent judges the result — so the
|
|
7
|
-
//
|
|
5
|
+
// Staging checks. ouro runs the deterministic checks (test, lint, build) and the
|
|
6
|
+
// QA agent judges the result — so the commands have to be resolved and run here,
|
|
7
|
+
// not left to the agent's Bash tool (QA is read-only by design).
|
|
8
8
|
//
|
|
9
|
-
// Resolution order: config.staging
|
|
10
|
-
// agent inference from the repo
|
|
11
|
-
//
|
|
9
|
+
// Resolution order per check: config.staging.<kind>Command if set, else a cheap
|
|
10
|
+
// read-only agent inference from the repo. If nothing resolves, that check is
|
|
11
|
+
// reported as not-run — the QA gate treats a check with no command in the repo
|
|
12
|
+
// as N/A, never a failure, so a repo without a linter or build step still ships.
|
|
13
|
+
|
|
14
|
+
// One entry per deterministic check. `configKey` is the config.staging field
|
|
15
|
+
// that pins the command; `infer` is the read-only prompt used when it isn't set.
|
|
16
|
+
const CHECKS = [
|
|
17
|
+
{
|
|
18
|
+
kind: "test",
|
|
19
|
+
configKey: "testCommand",
|
|
20
|
+
infer:
|
|
21
|
+
"Infer the single shell command that runs this project's automated tests, from its config (package.json scripts, Makefile, pyproject, etc.). " +
|
|
22
|
+
'Respond with ONLY JSON: {"command": string|null}. Use null if the repo has no test setup. No prose, no fences.',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
kind: "lint",
|
|
26
|
+
configKey: "lintCommand",
|
|
27
|
+
infer:
|
|
28
|
+
'Infer the single shell command that lints this project, from its config (a package.json "lint" script, eslint/ruff/flake8 config, etc.). ' +
|
|
29
|
+
'Respond with ONLY JSON: {"command": string|null}. Use null if the repo has no linter. No prose, no fences.',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
kind: "build",
|
|
33
|
+
configKey: "buildCommand",
|
|
34
|
+
infer:
|
|
35
|
+
'Infer the single shell command that builds or compiles this project, from its config (a package.json "build" script, tsc, a bundler config, etc.). ' +
|
|
36
|
+
'Respond with ONLY JSON: {"command": string|null}. Use null if the repo has no build step. No prose, no fences.',
|
|
37
|
+
},
|
|
38
|
+
];
|
|
12
39
|
|
|
13
40
|
/**
|
|
14
|
-
* Resolve
|
|
41
|
+
* Resolve one check's command. Returns { command, source } where source is
|
|
15
42
|
* "config" | "inferred" | "none".
|
|
16
43
|
*/
|
|
17
|
-
|
|
18
|
-
const configured = getStaging()
|
|
44
|
+
async function resolveCommand({ configKey, infer, cwd, signal }) {
|
|
45
|
+
const configured = getStaging()[configKey];
|
|
19
46
|
if (configured) return { command: String(configured), source: "config" };
|
|
20
47
|
|
|
21
|
-
const inferred = await askJson({
|
|
22
|
-
prompt:
|
|
23
|
-
"Infer the single shell command that runs this project's automated tests, from its config (package.json scripts, Makefile, pyproject, etc.). " +
|
|
24
|
-
'Respond with ONLY JSON: {"command": string|null}. Use null if the repo has no test setup. No prose, no fences.',
|
|
25
|
-
cwd,
|
|
26
|
-
signal,
|
|
27
|
-
}).catch(() => null);
|
|
28
|
-
|
|
48
|
+
const inferred = await askJson({ prompt: infer, cwd, signal }).catch(() => null);
|
|
29
49
|
const command = typeof inferred?.command === "string" ? inferred.command.trim() : "";
|
|
30
50
|
return { command: command || null, source: command ? "inferred" : "none" };
|
|
31
51
|
}
|
|
32
52
|
|
|
33
53
|
/**
|
|
34
54
|
* Run a shell command, capturing output. Bounded by a timeout so a hanging
|
|
35
|
-
*
|
|
55
|
+
* check can't wedge the stage. Never rejects — a spawn error resolves as a
|
|
36
56
|
* failed result the QA agent can read.
|
|
37
57
|
*/
|
|
38
58
|
export function runShell(command, { cwd, signal, timeoutMs = 10 * 60 * 1000 } = {}) {
|
|
@@ -40,7 +60,9 @@ export function runShell(command, { cwd, signal, timeoutMs = 10 * 60 * 1000 } =
|
|
|
40
60
|
let stdout = "";
|
|
41
61
|
let stderr = "";
|
|
42
62
|
// shell:true so a full command string ("npm test", "pytest -q") runs as-is.
|
|
43
|
-
|
|
63
|
+
// windowsHide: the shell child is cmd.exe on win32 — without it every check
|
|
64
|
+
// run (and every QA loop-back retry) flashes a console window.
|
|
65
|
+
const proc = spawn(command, { cwd, shell: true, signal, windowsHide: true });
|
|
44
66
|
|
|
45
67
|
const timer = setTimeout(() => {
|
|
46
68
|
try {
|
|
@@ -75,18 +97,18 @@ function tail(text, lines = 40) {
|
|
|
75
97
|
}
|
|
76
98
|
|
|
77
99
|
/**
|
|
78
|
-
* Resolve + run
|
|
79
|
-
*
|
|
80
|
-
* absent, never failing on the absence itself.
|
|
100
|
+
* Resolve + run one check. `ran: false` means no command resolved — the QA gate
|
|
101
|
+
* reads that as N/A for this repo, never a failure.
|
|
81
102
|
*/
|
|
82
|
-
|
|
83
|
-
const { command, source } = await
|
|
103
|
+
async function runOne(check, { cwd, signal }) {
|
|
104
|
+
const { command, source } = await resolveCommand({ ...check, cwd, signal });
|
|
84
105
|
if (!command) {
|
|
85
|
-
return { ran: false, command: null, source, passed: null, output:
|
|
106
|
+
return { kind: check.kind, ran: false, command: null, source, passed: null, output: `No ${check.kind} command resolved for this repo.` };
|
|
86
107
|
}
|
|
87
108
|
|
|
88
109
|
const res = await runShell(command, { cwd, signal });
|
|
89
110
|
return {
|
|
111
|
+
kind: check.kind,
|
|
90
112
|
ran: true,
|
|
91
113
|
command,
|
|
92
114
|
source,
|
|
@@ -95,3 +117,19 @@ export async function runTests({ cwd, signal }) {
|
|
|
95
117
|
output: tail(`${res.stdout}\n${res.stderr}`),
|
|
96
118
|
};
|
|
97
119
|
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Run every deterministic check (test, lint, build), in order. Sequential so
|
|
123
|
+
* build and test don't race on the same worktree. Returns { test, lint, build },
|
|
124
|
+
* each a structured result for the card and the QA agent. Honors `signal` —
|
|
125
|
+
* once aborted, remaining checks are skipped as not-run.
|
|
126
|
+
*/
|
|
127
|
+
export async function runChecks({ cwd, signal }) {
|
|
128
|
+
const out = {};
|
|
129
|
+
for (const check of CHECKS) {
|
|
130
|
+
out[check.kind] = signal?.aborted
|
|
131
|
+
? { kind: check.kind, ran: false, command: null, source: "none", passed: null, output: "cancelled" }
|
|
132
|
+
: await runOne(check, { cwd, signal });
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
package/src/lib/store.js
CHANGED
|
@@ -126,7 +126,8 @@ class TicketStore extends EventEmitter {
|
|
|
126
126
|
acceptanceCriteria: [],
|
|
127
127
|
analyzing: false, // transient: the read-only Analyze pass is in flight
|
|
128
128
|
// Staging stage (Feature 9).
|
|
129
|
-
testResult: null, // { ran, command, source, passed, code, output }
|
|
129
|
+
testResult: null, // { ran, command, source, passed, code, output } — the test check
|
|
130
|
+
checks: null, // { test, lint, build } — every deterministic check ouro ran for QA
|
|
130
131
|
previewUrl: null, // clickable local-preview link while one is running
|
|
131
132
|
previewNote: null, // why there's no preview, when there isn't
|
|
132
133
|
qaVerdict: null, // { ready, summary, reasons, uiChange, visualMethod, questions }
|
|
@@ -210,6 +211,7 @@ class TicketStore extends EventEmitter {
|
|
|
210
211
|
awaitingQa: false,
|
|
211
212
|
escalated: false,
|
|
212
213
|
testResult: null,
|
|
214
|
+
checks: null,
|
|
213
215
|
qaVerdict: null,
|
|
214
216
|
qaAttempts: 0,
|
|
215
217
|
previewUrl: null,
|
package/src/server/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { analyze, runAgent, planTicket, executeTicket, qaReview, getBackendName,
|
|
|
8
8
|
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
|
-
import {
|
|
11
|
+
import { runChecks } from "../lib/staging.js";
|
|
12
12
|
import { startPreview, stopPreview, previewInfo } from "../lib/preview.js";
|
|
13
13
|
import { readConfig, writeConfig, getDefaultMode, setDefaultMode, getAutoShip, getMaxQaAttempts, telegramTokenVar } from "../lib/config.js";
|
|
14
14
|
import { readEnvVars, writeEnvVars } from "../lib/env.js";
|
|
@@ -60,7 +60,7 @@ export function shipOutcome(result) {
|
|
|
60
60
|
|
|
61
61
|
// The full context the Senior QA Engineer agent judges against. ouro has already
|
|
62
62
|
// run the tests; the agent validates the running result and the visual review.
|
|
63
|
-
export function qaPrompt(ticket,
|
|
63
|
+
export function qaPrompt(ticket, checks, preview) {
|
|
64
64
|
const parts = [
|
|
65
65
|
`You are the QA gate validating ticket [#${ticket.id}] in its git worktree, after it was implemented. You have READ-ONLY tools (Read/Grep/Glob) — you validate, you never modify or execute.`,
|
|
66
66
|
"",
|
|
@@ -78,16 +78,19 @@ export function qaPrompt(ticket, testResult, preview) {
|
|
|
78
78
|
parts.push("", "No explicit acceptance criteria were recorded — validate against the ticket intent above.");
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
parts.push("", "
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
81
|
+
parts.push("", "Checks (ouro already ran these — you cannot and need not re-run them):");
|
|
82
|
+
for (const [label, r] of [
|
|
83
|
+
["Tests", checks?.test],
|
|
84
|
+
["Lint", checks?.lint],
|
|
85
|
+
["Build", checks?.build],
|
|
86
|
+
]) {
|
|
87
|
+
if (r?.ran) {
|
|
88
|
+
parts.push(`- ${label}: ${r.passed ? "PASSED" : "FAILED"} (exit ${r.code}) — ${r.command}`);
|
|
89
|
+
// Only the failing output earns space in the prompt; a PASSED line is enough.
|
|
90
|
+
if (!r.passed) parts.push(" output tail:", r.output || "(no output)");
|
|
91
|
+
} else {
|
|
92
|
+
parts.push(`- ${label}: not run — no such command in this repo (N/A, not a failure).`);
|
|
93
|
+
}
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
parts.push("", "Change under review (git diff of the worktree):");
|
|
@@ -102,8 +105,8 @@ export function qaPrompt(ticket, testResult, preview) {
|
|
|
102
105
|
"2. So if it IS a UI change, read the changed UI files and any rendered/built HTML with your Read tool and assess visually from those — do NOT silently skip UI validation.",
|
|
103
106
|
"3. If it is not a UI change, tests-only is fine.",
|
|
104
107
|
"",
|
|
105
|
-
"Judge the running result against the acceptance criteria and the
|
|
106
|
-
'Respond with ONLY a JSON object, no prose and no fences: {"ready": boolean, "summary": string, "reasons": string[], "ui_change": boolean, "visual_method": "screenshot"|"html"|"none", "questions": string[]}. When not ready, `reasons` must be concrete and actionable. `questions` are
|
|
108
|
+
"Judge the running result against the acceptance criteria and the check results above. A check that RAN and FAILED is grounds to send this back. A check listed as not run (no such command in this repo) is N/A — never block, and never raise a question, solely because a check was not run or could not be verified; ouro runs every check it can and you cannot run more. Never call something ready just because it was asked for.",
|
|
109
|
+
'Respond with ONLY a JSON object, no prose and no fences: {"ready": boolean, "summary": string, "reasons": string[], "ui_change": boolean, "visual_method": "screenshot"|"html"|"none", "questions": string[]}. When not ready, `reasons` must be concrete and actionable. `questions` are open items to resolve before shipping — in an agent loop the engineer answers them itself, in human-in-loop the person does.'
|
|
107
110
|
);
|
|
108
111
|
return parts.join("\n");
|
|
109
112
|
}
|
|
@@ -139,10 +142,25 @@ export function qaSummaryLine(v) {
|
|
|
139
142
|
}
|
|
140
143
|
|
|
141
144
|
export function qaFeedbackBlock(v) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
.
|
|
145
|
-
|
|
145
|
+
const blocks = [];
|
|
146
|
+
if (v?.reasons?.length) {
|
|
147
|
+
blocks.push(
|
|
148
|
+
`The QA gate sent this back. Address every point before it can ship:\n${v.reasons
|
|
149
|
+
.map((r, i) => `${i + 1}. ${r}`)
|
|
150
|
+
.join("\n")}`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
// Agent loop: no human will answer QA's questions, so the engineer resolves
|
|
154
|
+
// them itself. Only reached on the agent-loop loop-back path (enterStaging),
|
|
155
|
+
// so framing them as "yours to answer" is always accurate here.
|
|
156
|
+
if (v?.questions?.length) {
|
|
157
|
+
blocks.push(
|
|
158
|
+
`QA raised these open questions. This is an agent loop — no human will answer them, so resolve each one yourself: investigate the code, decide, act on it, and state the answer in your final message:\n${v.questions
|
|
159
|
+
.map((q, i) => `${i + 1}. ${q}`)
|
|
160
|
+
.join("\n")}`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return blocks.length ? `\n\n${blocks.join("\n\n")}` : "";
|
|
146
164
|
}
|
|
147
165
|
|
|
148
166
|
/**
|
|
@@ -305,21 +323,22 @@ export function createServer() {
|
|
|
305
323
|
while (!signal.aborted) {
|
|
306
324
|
const ticket = store.get(ticketId);
|
|
307
325
|
|
|
308
|
-
// 1.
|
|
309
|
-
|
|
310
|
-
|
|
326
|
+
// 1. Checks — ouro runs test, lint, and build deterministically. testResult
|
|
327
|
+
// stays the test result (the card renders it); checks carries all three.
|
|
328
|
+
const checks = await runChecks({ cwd: ticket.worktree, signal });
|
|
329
|
+
store.update(ticketId, { testResult: checks.test, checks });
|
|
311
330
|
if (signal.aborted) return;
|
|
312
331
|
|
|
313
332
|
// 2. QA agent judges the running result against the acceptance criteria.
|
|
314
333
|
const verdict = normalizeVerdict(
|
|
315
334
|
await qaReview({
|
|
316
|
-
prompt: qaPrompt(ticket,
|
|
335
|
+
prompt: qaPrompt(ticket, checks, previewInfo(ticketId)),
|
|
317
336
|
cwd: ticket.worktree,
|
|
318
337
|
signal,
|
|
319
338
|
onEvent,
|
|
320
339
|
agent: getAgent("senior-qa-engineer"),
|
|
321
340
|
}).catch(() => null),
|
|
322
|
-
|
|
341
|
+
checks.test
|
|
323
342
|
);
|
|
324
343
|
if (signal.aborted) return;
|
|
325
344
|
const attempt = (store.get(ticketId).qaAttempts ?? 0) + 1;
|