@splinterzzz/ouro 0.1.0 → 0.1.2

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.
@@ -0,0 +1 @@
1
+ :root{--bg: #0b0a12;--surface: #12101b;--surface-2: #191627;--surface-3: #201c31;--border: #241f38;--border-strong: #332b4d;--text: #e8e6f0;--text-dim: #a5a0b8;--text-mute: #8b85a8;--chrome: #6d4fd6;--brand: #8b5cf6;--brand-soft: rgba(139, 92, 246, .13);--brand-line: rgba(139, 92, 246, .36);--brand-text: #a78bfa;--brand-deep: #7c3aed;--brand-lift: #8f4ff5;--run: #c4b5fd;--run-soft: rgba(196, 181, 253, .12);--run-line: rgba(196, 181, 253, .38);--glow: 0 0 20px rgba(139, 92, 246, .35);--glow-run: 0 0 16px rgba(196, 181, 253, .45);--warn: #f0b429;--warn-soft: rgba(240, 180, 41, .12);--bad: #f2555a;--bad-soft: rgba(242, 85, 90, .12);--font: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;--mono: "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;--t-fast: .12s;--t: .18s;--t-slow: .28s;--ease: cubic-bezier(.32, .72, 0, 1);--ease-out: cubic-bezier(.4, 0, 1, 1);--radius: 10px;--radius-sm: 7px;--rail: 56px;--sidebar: 264px;color-scheme:dark;font-family:var(--font)}*{box-sizing:border-box}html,body,#root{height:100%}body{margin:0;background:var(--bg);color:var(--text);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased}.mono{font-family:var(--mono);font-variant-numeric:tabular-nums}::selection{background:var(--brand-line);color:#fff}:focus-visible{outline:2px solid var(--brand);outline-offset:2px;border-radius:4px;box-shadow:var(--glow)}:focus:not(:focus-visible){outline:none}.wg-scroll{scrollbar-width:thin;scrollbar-color:var(--border-strong) transparent}.wg-scroll::-webkit-scrollbar{width:9px;height:9px}.wg-scroll::-webkit-scrollbar-thumb{background:var(--border-strong);border-radius:99px;border:2px solid transparent;background-clip:content-box}.wg-scroll::-webkit-scrollbar-thumb:hover{background-color:#3d4552;background-clip:content-box}.wg-scroll::-webkit-scrollbar-track{background:transparent}.icon{flex-shrink:0;display:block}.app{display:grid;grid-template-columns:auto 1fr;height:100vh;height:100dvh;overflow:hidden}.main{display:flex;flex-direction:column;min-width:0;min-height:0}.workspace{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden}.sidebar{width:var(--sidebar);background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;min-height:0;transition:width var(--t-slow) var(--ease);overflow:hidden}.sidebar.collapsed{width:var(--rail)}.sidebar-head{display:flex;align-items:center;gap:9px;height:53px;padding:0 12px;border-bottom:1px solid var(--border);flex-shrink:0}.brand-logo{color:var(--brand);filter:drop-shadow(0 0 5px rgba(139,92,246,.55));flex-shrink:0;transition:color var(--t) var(--ease),filter var(--t) var(--ease)}.brand-logo.offline{color:var(--bad);filter:drop-shadow(0 0 5px rgba(242,85,90,.5))}.brand-mark{font-weight:700;font-size:16px;letter-spacing:-.01em;background:linear-gradient(135deg,#fff 20%,var(--brand));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;white-space:nowrap}.status-dot{width:7px;height:7px;border-radius:50%;background:var(--bad);flex-shrink:0;transition:background var(--t) var(--ease)}.status-dot.live{background:var(--chrome);animation:breathe 2.4s ease-in-out infinite}.status-dot.running{background:var(--run);box-shadow:var(--glow-run)}@keyframes breathe{0%,to{box-shadow:0 0 #6d4fd65c}50%{box-shadow:0 0 0 4px transparent}}.rail-toggle{margin-left:auto;display:grid;place-items:center;width:28px;height:28px;border-radius:var(--radius-sm);border:1px solid transparent;background:transparent;color:var(--text-mute);cursor:pointer;transition:color var(--t-fast),background var(--t-fast),border-color var(--t-fast)}.rail-toggle:hover{color:var(--text);background:var(--surface-3);border-color:var(--border)}.sidebar-body{flex:1;overflow-y:auto;overflow-x:hidden;padding:8px;min-height:0}.nav-section+.nav-section{margin-top:4px}.nav-head{display:flex;align-items:center;gap:9px;width:100%;padding:8px 9px;border:none;background:transparent;color:var(--text-dim);border-radius:var(--radius-sm);cursor:pointer;font:inherit;font-size:12px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;transition:color var(--t-fast),background var(--t-fast)}.nav-head:hover{color:var(--text);background:var(--surface-2)}.nav-head.active{color:var(--text)}.nav-head .chev{margin-left:auto;color:var(--text-mute);transition:transform var(--t) var(--ease)}.nav-head .chev.open{transform:rotate(90deg)}.nav-count{font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:10px;color:var(--text-mute);background:var(--surface-3);border:1px solid var(--border);border-radius:99px;padding:1px 6px;letter-spacing:0}.nav-group{display:grid;grid-template-rows:0fr;transition:grid-template-rows var(--t-slow) var(--ease)}.nav-group.open{grid-template-rows:1fr}.nav-group>.nav-group-inner{overflow:hidden;min-height:0}.nav-item{display:flex;align-items:center;gap:8px;width:100%;padding:7px 9px 7px 12px;border:none;border-left:2px solid transparent;background:transparent;color:var(--text-dim);cursor:pointer;font:inherit;font-size:12.5px;text-align:left;border-radius:0 var(--radius-sm) var(--radius-sm) 0;transition:color var(--t-fast),background var(--t-fast),border-color var(--t-fast)}.nav-item:hover{color:var(--text);background:var(--surface-2)}.nav-item.selected{color:var(--text);background:var(--brand-soft);border-left-color:var(--brand)}.nav-item .label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.nav-item .glyph{color:var(--text-mute);font-family:var(--mono);font-size:11px}.nav-item.selected .glyph{color:var(--brand)}.nav-empty{padding:6px 12px 10px;font-size:11.5px;color:var(--text-mute)}.sidebar.collapsed .nav-head .label,.sidebar.collapsed .nav-head .chev,.sidebar.collapsed .nav-count,.sidebar.collapsed .brand-mark,.sidebar.collapsed .status-dot,.sidebar.collapsed .nav-group,.sidebar.collapsed .nav-empty,.sidebar.collapsed .sidebar-foot{display:none}.sidebar.collapsed .nav-head{justify-content:center;padding:8px}.sidebar.collapsed .sidebar-head{justify-content:center;padding:0;gap:6px}.sidebar.collapsed .rail-toggle{margin:0;width:26px}.sidebar-foot{border-top:1px solid var(--border);padding:9px 12px;font-size:10.5px;color:var(--text-mute);flex-shrink:0;display:flex;align-items:center;gap:6px}.topbar{display:flex;align-items:center;gap:12px;height:53px;padding:0 16px;border-bottom:1px solid var(--border);background:var(--surface);flex-shrink:0}.topbar h1{margin:0;font-size:14px;font-weight:600;letter-spacing:-.01em}.topbar .sub{font-size:11.5px;color:var(--text-mute)}.topbar-right{margin-left:auto;display:flex;align-items:center;gap:10px}.control-label{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--text-mute);margin-right:-4px}.segmented{position:relative;display:flex;align-items:center;height:32px;padding:3px;background:var(--surface-2);border:1px solid var(--border);border-radius:var(--radius-sm)}.segmented-pill{position:absolute;top:3px;bottom:3px;border-radius:5px;background:var(--brand-soft);border:1px solid var(--brand-line);transition:transform var(--t) var(--ease),width var(--t) var(--ease);pointer-events:none}.segmented-opt{position:relative;z-index:1;display:inline-flex;align-items:center;gap:5px;height:100%;padding:0 11px;border:none;background:transparent;color:var(--text-mute);font:inherit;font-size:12px;font-weight:500;white-space:nowrap;cursor:pointer;border-radius:5px;transition:color var(--t-fast) var(--ease)}.segmented-opt:hover{color:var(--text-dim)}.segmented-opt.on{color:var(--text)}.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:32px;padding:0 12px;background:var(--surface-2);border:1px solid var(--border);color:var(--text);border-radius:var(--radius-sm);cursor:pointer;font:inherit;font-size:12.5px;font-weight:500;white-space:nowrap;transition:background var(--t-fast),border-color var(--t-fast),color var(--t-fast),transform var(--t-fast)}.btn:hover{background:var(--surface-3);border-color:var(--border-strong)}.btn:active{transform:scale(.97)}.btn:disabled{opacity:.42;cursor:not-allowed;transform:none}.btn:disabled:hover{background:var(--surface-2);border-color:var(--border)}.btn.sm{height:27px;padding:0 9px;font-size:11.5px}.btn.primary{background:var(--brand-deep);border-color:var(--brand-deep);color:#fff;box-shadow:var(--glow)}.btn.primary:hover{background:var(--brand-lift);border-color:var(--brand-lift)}.btn.ghost{background:transparent;border-color:transparent;color:var(--text-dim)}.btn.ghost:hover{background:var(--surface-3);color:var(--text)}.btn.run{background:var(--brand-soft);border-color:var(--brand-line);color:var(--brand-text)}.btn.run:hover{background:#8b5cf638;border-color:var(--brand)}.btn.danger{background:var(--bad-soft);border-color:#f2555a4d;color:var(--bad)}.btn.danger:hover{background:#f2555a33}.btn.icon-only{width:32px;padding:0}.btn.sm.icon-only{width:27px}.board{flex:1;display:grid;grid-template-columns:repeat(5,minmax(205px,1fr));gap:12px;padding:14px 16px;overflow:auto;min-height:0}.column{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);display:flex;flex-direction:column;min-height:0;overflow:hidden}.column-header{display:flex;align-items:center;gap:8px;padding:10px 12px;border-bottom:1px solid var(--border);font-size:11px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:.05em;flex-shrink:0}.column-header .dot{width:6px;height:6px;border-radius:50%;transition:box-shadow var(--t) var(--ease)}.column-header .dot.live{box-shadow:var(--glow-run);animation:pulse 1.2s ease-in-out infinite}.column-header .count{margin-left:auto}.column-body{flex:1;overflow-y:auto;padding:9px;display:flex;flex-direction:column;gap:9px;min-height:0}.empty{color:var(--text-mute);font-size:11.5px;padding:14px 8px;text-align:center;border:1px dashed var(--border);border-radius:var(--radius-sm)}.card{position:relative;background:var(--surface-2);border:1px solid var(--border);border-radius:var(--radius-sm);padding:11px;display:flex;flex-direction:column;gap:8px;cursor:pointer;animation:card-in var(--t-slow) var(--ease) backwards;animation-delay:calc(var(--i, 0) * 35ms);transition:border-color var(--t-fast),background var(--t-fast),transform var(--t-fast)}.card:hover{border-color:var(--border-strong);background:var(--surface-3)}.card.selected{border-color:var(--brand-line);background:var(--surface-3);box-shadow:var(--glow)}.card.running{border-color:var(--run-line);box-shadow:var(--glow-run)}@keyframes card-in{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}.card.running:before{content:"";position:absolute;inset:0 0 auto 0;height:2px;border-radius:var(--radius-sm) var(--radius-sm) 0 0;background:linear-gradient(90deg,transparent,var(--run),transparent);background-size:200% 100%;animation:sweep 1.6s linear infinite}@keyframes sweep{0%{background-position:200% 0}to{background-position:-200% 0}}.card-title{font-weight:600;font-size:13px;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.card-summary{font-size:11.5px;color:var(--text-dim);line-height:1.5;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.card-meta{display:flex;gap:5px;flex-wrap:wrap;align-items:center}.badge{display:inline-flex;align-items:center;gap:4px;font-size:9.5px;font-weight:600;padding:2px 6px;border-radius:4px;border:1px solid var(--border);color:var(--text-mute);background:var(--surface-3);text-transform:uppercase;letter-spacing:.03em;white-space:nowrap}.badge.mono{text-transform:none;letter-spacing:0;font-size:10px}.badge.priority-high{color:var(--bad);border-color:#f2555a4d;background:var(--bad-soft)}.badge.priority-medium{color:var(--warn);border-color:#f0b4294d;background:var(--warn-soft)}.badge.priority-low{color:var(--text-mute)}.badge.agent{color:var(--brand-text);border-color:var(--brand-line);background:var(--brand-soft)}.card-actions{display:flex;gap:5px;flex-wrap:wrap}.log-tail{font-family:var(--mono);font-size:10.5px;color:var(--run);background:var(--bg);border:1px solid var(--border);padding:5px 7px;border-radius:5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.cancel-note{font-size:11px;color:var(--text-mute);display:flex;align-items:flex-start;gap:5px;line-height:1.45}.card-criteria{font-size:11px;color:var(--text-dim)}.card-criteria>summary{cursor:pointer;color:var(--text-mute);font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.03em;list-style:none;-webkit-user-select:none;user-select:none}.card-criteria>summary::-webkit-details-marker{display:none}.card-criteria>summary:before{content:"▸";display:inline-block;margin-right:5px;transition:transform .15s ease}.card-criteria[open]>summary:before{transform:rotate(90deg)}.card-criteria ul{margin:6px 0 2px;padding-left:16px;display:flex;flex-direction:column;gap:3px}.card-criteria li{line-height:1.4}.artifacts{padding:18px 22px;max-width:860px;display:flex;flex-direction:column;gap:24px}.artifacts-intro{font-size:12.5px;color:var(--text-dim);line-height:1.55;margin:0;max-width:660px}.artifact-group{display:flex;flex-direction:column;gap:7px}.artifact-group-head{display:flex;align-items:center;gap:8px;color:var(--text)}.artifact-group-head h2{font-size:13px;font-weight:600;margin:0}.artifact-group-head .count{color:var(--text-mute);font-size:11px;margin-left:2px}.artifact-group-sub{font-size:11.5px;color:var(--text-mute);line-height:1.5;margin:0 0 5px;max-width:660px}.artifact-list{list-style:none;margin:0;padding:0;border:1px solid var(--border);border-radius:8px;overflow:hidden}.artifact-item{display:flex;align-items:center;gap:10px;padding:9px 12px;background:var(--surface-2)}.artifact-item+.artifact-item{border-top:1px solid var(--border)}.artifact-name{font-size:12px;color:var(--brand-text);flex-shrink:0}.artifact-desc{font-size:11.5px;color:var(--text-dim);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.artifact-item .badge{margin-left:auto}.logs{padding:18px 22px;max-width:820px}.logs-intro{font-size:12.5px;color:var(--text-dim);line-height:1.55;margin:0 0 20px;max-width:660px}.log-view{display:flex;flex-direction:column}.log-date{font-size:11px;font-weight:700;color:var(--text-mute);letter-spacing:.04em;margin:20px 0 8px;padding-bottom:5px;border-bottom:1px solid var(--border)}.log-view>.log-date:first-child{margin-top:0}.log-head{font-size:12.5px;color:var(--text);line-height:1.5;margin-top:8px}.log-outcome{font-size:11.5px;color:var(--text-dim);line-height:1.5;padding-left:14px;margin-bottom:2px}.log-note{font-size:11.5px;color:var(--text-mute);line-height:1.5;margin:0 0 6px}.exchange{margin-top:2px}.exchange-toggle{display:inline-flex;align-items:center;gap:5px;font-size:10px;font-weight:600;color:var(--text-mute);background:none;border:1px solid var(--border);border-radius:5px;padding:3px 8px;cursor:pointer;text-transform:uppercase;letter-spacing:.03em}.exchange-toggle:hover{color:var(--text-dim);border-color:var(--border-strong)}.exchange-panel{margin-top:6px;max-height:220px;overflow-y:auto;background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:8px 9px;display:flex;flex-direction:column;gap:7px;font-size:11px;line-height:1.45}.exchange-msg{display:flex;flex-direction:column;gap:2px}.exchange-who{font-size:9px;font-weight:700;color:var(--brand-text);text-transform:uppercase;letter-spacing:.04em}.exchange-text{color:var(--text-dim);white-space:pre-wrap;overflow-wrap:anywhere}.exchange-tool{display:flex;gap:6px;align-items:baseline;font-family:var(--mono);font-size:10.5px;min-width:0}.exchange-arrow{color:var(--text-mute);flex-shrink:0}.exchange-tname{color:var(--run);font-weight:600;flex-shrink:0}.exchange-targ{color:var(--text-mute);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.exchange-result{display:flex;gap:6px;font-family:var(--mono);font-size:10.5px;color:var(--text-mute)}.exchange-err{color:var(--bad);font-size:10.5px;overflow-wrap:anywhere}.exchange-qa{margin-top:2px;padding-top:9px;border-top:1px solid var(--border);display:flex;flex-direction:column;gap:7px}.exchange-qa .qa-actions{border-top:none;padding-top:0}.pr-link{display:flex;align-items:center;gap:6px;padding:7px 9px;border-radius:var(--radius-sm);background:var(--brand-soft);border:1px solid var(--brand-line);color:var(--brand-text);font-family:var(--mono);font-size:11px;text-decoration:none;transition:background var(--t-fast),border-color var(--t-fast),color var(--t-fast),box-shadow var(--t-fast)}.pr-link:hover{background:#8b5cf638;border-color:var(--brand);color:var(--run);box-shadow:var(--glow)}.pr-link .label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.diff{font-family:var(--mono);font-size:10px;line-height:1.6;background:var(--bg);border:1px solid var(--border);padding:7px;border-radius:5px;max-height:150px;overflow:auto;white-space:pre-wrap;word-break:break-word;margin:0;color:var(--text-dim)}.test-result{display:flex;align-items:center;gap:7px;flex-wrap:wrap;font-size:11px}.test-result .cmd{color:var(--text-mute);font-size:10.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.test-badge{display:inline-flex;align-items:center;flex-shrink:0;font-size:9.5px;font-weight:700;padding:2px 7px;border-radius:4px;border:1px solid var(--border);color:var(--text-mute);background:var(--surface-3);text-transform:uppercase;letter-spacing:.03em}.test-badge.pass{color:var(--brand-text);border-color:var(--brand-line);background:var(--brand-soft)}.test-badge.fail{color:var(--bad);border-color:#f2555a4d;background:var(--bad-soft)}.preview-link{display:flex;align-items:center;gap:6px;padding:6px 9px;border-radius:var(--radius-sm);background:var(--surface-3);border:1px solid var(--border);color:var(--text-dim);font-family:var(--mono);font-size:11px;text-decoration:none;transition:background var(--t-fast),border-color var(--t-fast),color var(--t-fast)}.preview-link:hover{border-color:var(--border-strong);color:var(--text);background:var(--surface-2)}.preview-link .label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.qa-verdict{display:flex;flex-direction:column;gap:6px;padding:8px 9px;background:var(--surface-3);border:1px solid var(--border);border-radius:6px;font-size:11px}.qa-head{display:flex;align-items:flex-start;gap:7px}.qa-badge{flex-shrink:0;margin-top:1px;display:inline-flex;align-items:center;font-size:9.5px;font-weight:700;padding:2px 6px;border-radius:4px;border:1px solid var(--border);color:var(--text-mute);text-transform:uppercase;letter-spacing:.03em}.qa-badge.ready{color:var(--brand-text);border-color:var(--brand-line);background:var(--brand-soft)}.qa-badge.not-ready{color:var(--bad);border-color:#f2555a4d;background:var(--bad-soft)}.qa-summary{color:var(--text-dim);line-height:1.5;overflow-wrap:anywhere}.qa-note{color:var(--text-mute);font-size:10.5px;line-height:1.4}.qa-group{display:flex;flex-direction:column;gap:3px}.qa-label{font-size:9.5px;font-weight:600;color:var(--text-mute);text-transform:uppercase;letter-spacing:.04em}.qa-list{margin:0;padding-left:16px;display:flex;flex-direction:column;gap:3px;color:var(--text-dim)}.qa-list li{line-height:1.4;overflow-wrap:anywhere}.qa-actions{padding-top:8px;border-top:1px dashed var(--border)}.agents{flex:1;display:grid;grid-template-columns:280px 1fr;min-height:0;overflow:hidden}.agents-list{border-right:1px solid var(--border);overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:7px;min-height:0}.agents-list-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:3px}.agents-list-head span{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--text-dim)}.agent-row{text-align:left;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);padding:10px 11px;cursor:pointer;font:inherit;color:var(--text);animation:card-in var(--t-slow) var(--ease) backwards;animation-delay:calc(var(--i, 0) * 30ms);transition:border-color var(--t-fast),background var(--t-fast),transform var(--t-fast)}.agent-row:hover{border-color:var(--border-strong);background:var(--surface-2)}.agent-row:active{transform:scale(.99)}.agent-row.selected{border-color:var(--brand-line);background:var(--brand-soft)}.agent-row-top{display:flex;align-items:center;gap:8px;margin-bottom:4px}.agent-row-top .glyph{font-family:var(--mono);font-size:12px;color:var(--text-mute)}.agent-row.selected .glyph{color:var(--brand)}.agent-row-top .name{font-size:12.5px;font-weight:600}.agent-row .desc{font-size:11px;color:var(--text-mute);line-height:1.45;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.agent-row .foot{display:flex;gap:8px;margin-top:6px;font-family:var(--mono);font-size:9.5px;color:var(--text-mute)}.agent-detail{overflow-y:auto;padding:20px 24px 40px;min-height:0}.agent-detail-inner{max-width:680px}.agent-head{display:flex;align-items:center;gap:11px;margin-bottom:3px}.agent-head .glyph{font-family:var(--mono);font-size:17px;color:var(--brand)}.agent-head h2{margin:0;font-size:17px;font-weight:600}.agent-head .path{margin-left:auto;font-family:var(--mono);font-size:10.5px;color:var(--text-mute);display:flex;align-items:center;gap:5px}.agent-sub{font-size:12.5px;color:var(--text-mute);margin-bottom:18px}.field{margin-bottom:14px}.field label{display:block;font-size:10px;color:var(--text-mute);text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px}.field-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}.input,.textarea,.select{width:100%;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:var(--radius-sm);padding:8px 11px;font-family:inherit;font-size:13px;outline:none;transition:border-color var(--t-fast),box-shadow var(--t-fast)}.input:focus,.textarea:focus,.select:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--brand-soft)}.input::placeholder,.textarea::placeholder{color:var(--text-mute)}.textarea{resize:vertical;line-height:1.65;min-height:90px}.textarea.mono{font-family:var(--mono);font-size:12px}.select{-webkit-appearance:none;-moz-appearance:none;appearance:none;cursor:pointer;padding-right:28px}.select-wrap{position:relative}.select-wrap .chev{position:absolute;right:10px;top:50%;transform:translateY(-50%);pointer-events:none;color:var(--text-mute)}.tools{display:flex;flex-wrap:wrap;gap:6px}.tool{display:inline-flex;align-items:center;gap:5px;height:28px;padding:0 10px;border-radius:var(--radius-sm);background:var(--surface-2);border:1px solid var(--border);color:var(--text-mute);font-family:var(--mono);font-size:11.5px;cursor:pointer;transition:background var(--t-fast),border-color var(--t-fast),color var(--t-fast),transform var(--t-fast)}.tool:hover{border-color:var(--border-strong);color:var(--text-dim)}.tool:active{transform:scale(.96)}.tool.on{background:var(--brand-soft);border-color:var(--brand-line);color:var(--brand-text)}.tool.on.danger{background:var(--warn-soft);border-color:#f0b42957;color:var(--warn)}.save-row{display:flex;gap:8px;margin:18px 0}.save-row .input{flex:1}.hint{font-size:11px;color:var(--text-mute);margin-top:6px;line-height:1.5}.hint code,.settings-steps code{font-family:var(--mono);font-size:10.5px;color:var(--text-dim)}.error-text{font-size:11.5px;color:var(--bad);margin-top:7px}.ok-text{font-size:11.5px;color:var(--text-dim);margin-top:7px}.banner{display:flex;align-items:center;gap:7px;padding:8px 16px;font-size:11.5px;flex-shrink:0;border-bottom:1px solid transparent}.banner.warn{color:var(--warn);background:var(--warn-soft);border-bottom-color:#f0b42933}.banner.bad{color:var(--bad);background:var(--bad-soft);border-bottom-color:#f2555a33}.settings-status{display:flex;align-items:flex-start;gap:10px;padding:12px 13px;margin-bottom:20px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);transition:border-color var(--t),background var(--t)}.settings-status.live{border-color:var(--brand-line);background:var(--brand-soft)}.settings-status .status-dot{margin-top:4px}.settings-status .status-dot.idle{background:var(--border-strong)}.settings-status .state{font-size:12.5px;font-weight:600;color:var(--text)}.settings-status .detail{font-size:11.5px;color:var(--text-mute);margin-top:3px;line-height:1.5}.settings-status .meta{margin-left:auto;text-align:right;font-family:var(--mono);font-size:10px;color:var(--text-mute);line-height:1.6;white-space:nowrap}.settings-status .warn-note{color:var(--warn)}.settings-actions{display:flex;gap:8px;margin-top:14px}.settings-config-error{align-items:flex-start;white-space:pre-line;line-height:1.55}.settings-config-error .icon{margin-top:2px}.settings-failure{margin-top:10px;display:flex;flex-direction:column;gap:3px}.settings-failure .log-tail{white-space:pre-wrap;overflow-wrap:anywhere;color:var(--text-dim)}.settings-steps{margin:0;padding-left:17px;font-size:11.5px;color:var(--text-mute);line-height:1.85}.ext{color:var(--brand-text);text-decoration:none;border-bottom:1px solid var(--brand-line)}.ext:hover{color:var(--run);border-bottom-color:var(--brand)}.terminal{border-top:1px solid var(--border);background:var(--bg);display:flex;flex-direction:column;flex-shrink:0;min-height:0}.terminal-resize{height:5px;cursor:ns-resize;position:relative;background:transparent;flex-shrink:0;transition:background var(--t-fast)}.terminal-resize:after{content:"";position:absolute;top:-4px;right:0;bottom:-4px;left:0}.terminal-resize:hover,.terminal-resize.dragging{background:var(--brand-line)}.terminal-head{display:flex;align-items:center;gap:8px;height:34px;padding:0 12px;border-bottom:1px solid var(--border);background:var(--surface);flex-shrink:0}.terminal-title{display:flex;align-items:center;gap:6px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--text-dim)}.terminal-head .spacer{margin-left:auto}.running-pill{display:inline-flex;align-items:center;gap:5px;height:20px;padding:0 7px;border-radius:99px;background:var(--run-soft);border:1px solid var(--run-line);color:var(--run);font-family:var(--mono);font-size:10px;box-shadow:var(--glow-run)}.running-pill .pulse{width:5px;height:5px;border-radius:50%;background:var(--run);box-shadow:0 0 6px var(--run);animation:pulse 1.2s ease-in-out infinite}@keyframes pulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.35;transform:scale(.75)}}.terminal-body{flex:1;overflow-y:auto;padding:8px 12px 12px;font-family:var(--mono);font-size:11px;line-height:1.65;min-height:0}.term-line{display:flex;gap:9px;padding:1px 0;animation:line-in var(--t-fast) var(--ease) backwards;white-space:pre-wrap;word-break:break-word}@keyframes line-in{0%{opacity:0;transform:translate(-3px)}to{opacity:1;transform:none}}.term-ts{color:#4e4763;flex-shrink:0}.term-tag{flex-shrink:0;width:62px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:pointer}.term-msg{flex:1;min-width:0;color:var(--text-dim)}.term-line.tone-run .term-msg{color:#d3cfe2}.term-line.tone-good .term-msg{color:var(--brand-text)}.term-line.tone-bad .term-msg{color:var(--bad)}.term-line.tone-warn .term-msg{color:var(--warn)}.term-line.tone-dim .term-msg{color:var(--text-mute)}.terminal-empty{color:var(--text-mute);font-size:11px;padding:10px 0}.terminal-empty .kbd{font-family:var(--mono);background:var(--surface-2);border:1px solid var(--border);border-radius:4px;padding:1px 5px;font-size:10px}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;background:#0000009e;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);display:grid;place-items:center;z-index:100;padding:20px;animation:fade-in var(--t) var(--ease)}@keyframes fade-in{0%{opacity:0}to{opacity:1}}.modal{background:var(--surface);border:1px solid var(--border-strong);border-radius:12px;padding:18px;width:100%;max-width:440px;display:flex;flex-direction:column;gap:12px;box-shadow:0 20px 60px #0000008c;animation:modal-in var(--t-slow) var(--ease)}@keyframes modal-in{0%{opacity:0;transform:scale(.96) translateY(6px)}to{opacity:1;transform:none}}.modal h3{margin:0;font-size:15px;font-weight:600}.modal-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:2px}@media (max-width: 1180px){.board{grid-auto-flow:column;grid-auto-columns:250px;grid-template-columns:none}.agents{grid-template-columns:240px 1fr}}@media (max-width: 760px){.app{grid-template-columns:var(--rail)}.sidebar{width:var(--rail)}.board{grid-auto-flow:column;grid-auto-columns:84vw;grid-template-columns:none}.agents{grid-template-columns:1fr}.agents-list{display:none}.field-row{grid-template-columns:1fr}.control-label{display:none}}@media (prefers-reduced-motion: reduce){*,*:before,*:after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important;scroll-behavior:auto!important}.card.running:before{animation:none;background:var(--run)}.status-dot.live,.running-pill .pulse,.column-header .dot.live{animation:none}}@media (prefers-reduced-motion: reduce){.card.running{box-shadow:0 0 0 1px var(--run),var(--glow-run)}.column-header .dot.live{box-shadow:0 0 0 2px var(--run-soft),var(--glow-run)}}
@@ -9,8 +9,8 @@
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-UHUWnWxM.js"></script>
13
- <link rel="stylesheet" crossorigin href="/assets/index-ETSLKv6w.css">
12
+ <script type="module" crossorigin src="/assets/index-B-lZ4RcX.js"></script>
13
+ <link rel="stylesheet" crossorigin href="/assets/index-C21ZXjPS.css">
14
14
  </head>
15
15
  <body>
16
16
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splinterzzz/ouro",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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": [
@@ -5,6 +5,7 @@ import { isInitialized } from "../lib/paths.js";
5
5
  import { seedDefaultAgents } from "../lib/agents.js";
6
6
  import { store } from "../lib/store.js";
7
7
  import * as runs from "../lib/runs.js";
8
+ import { stopAllPreviews } from "../lib/preview.js";
8
9
 
9
10
  export async function dashboardCommand(opts) {
10
11
  if (!isInitialized()) {
@@ -41,6 +42,7 @@ export async function dashboardCommand(opts) {
41
42
  closing = true;
42
43
  console.log(chalk.gray("\nShutting down — stopping agent runs…"));
43
44
  runs.cancelAll();
45
+ stopAllPreviews();
44
46
  store.flush();
45
47
  server.close(() => process.exit(0));
46
48
  setTimeout(() => process.exit(0), 2000).unref(); // don't hang on a stuck socket
@@ -1,30 +1,178 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { execFileSync } from "node:child_process";
3
4
  import chalk from "chalk";
4
- import { ensureOuroDir, isInitialized, agentsDir, ouroDir } from "../lib/paths.js";
5
+ import { ensureOuroDir, isInitialized, agentsDir, ouroDir, repoRoot } from "../lib/paths.js";
5
6
  import { writeConfig } from "../lib/config.js";
6
7
  import { seedDefaultAgents } from "../lib/agents.js";
8
+ import { generateSpec } from "../lib/agentBackend.js";
9
+ import { listReferencedFiles } from "../lib/artifacts.js";
7
10
 
8
11
  // What inside .ouro/ is machine state vs. yours.
9
12
  //
10
- // agents/*.md and config.json are meant to be committed — that's the whole
11
- // point of agents-as-markdown. Everything here is either a secret (.env holds
12
- // a bot token), a scratch checkout, or per-machine runtime noise.
13
- const GITIGNORE = `# ouro runtime state — not yours to commit.
14
- # agents/*.md and config.json are deliberately NOT ignored: they're the
15
- # reviewable config this tool exists to give you.
13
+ // agents/, context/ and config.json are meant to be committed — that's the
14
+ // whole point of agents-as-markdown and a shared context folder. Everything
15
+ // else is a secret (.env holds a bot token), a scratch checkout, or per-machine
16
+ // runtime noise.
17
+ //
18
+ // The pattern is ignore-everything-then-unignore-safe, never the reverse: a new
19
+ // runtime file added in a later version is ignored by default (fails safe)
20
+ // rather than silently tracked until someone remembers to add it. The trailing
21
+ // .env guards keep a token out even if an un-ignore above is ever loosened.
22
+ const GITIGNORE = `# .ouro/.gitignore
23
+ # ignore everything by default
24
+ /*
25
+ # un-ignore only what's safe to commit
26
+ !agents/
27
+ !context/
28
+ !config.json
29
+ !.gitignore
30
+ # hard guard: secrets stay out even if a rule above is loosened later
16
31
  .env
17
- run/
18
- logs/
19
- worktrees/
20
- tickets.json
32
+ context/**/*.env
21
33
  `;
22
34
 
35
+ // Headers of a .ouro/.gitignore this tool has written — the current default-deny
36
+ // form and the pre-default-deny form it replaced. An install predating the safe
37
+ // pattern gets upgraded; a hand-authored file (neither header) is left alone.
38
+ const OURO_GITIGNORE_SIGNATURES = ["# .ouro/.gitignore", "# ouro runtime state"];
39
+
40
+ /**
41
+ * Writes/updates `.ouro/.gitignore`. Returns "created", "upgraded", "current",
42
+ * or "kept" (an existing file we didn't recognise as ours — never clobbered).
43
+ */
23
44
  function writeGitignore() {
24
45
  const file = path.join(ouroDir(), ".gitignore");
25
- if (fs.existsSync(file)) return false;
46
+ if (fs.existsSync(file)) {
47
+ const current = fs.readFileSync(file, "utf-8");
48
+ if (current === GITIGNORE) return "current";
49
+ const ouroManaged = OURO_GITIGNORE_SIGNATURES.some((sig) => current.includes(sig));
50
+ if (!ouroManaged) return "kept";
51
+ fs.writeFileSync(file, GITIGNORE);
52
+ return "upgraded";
53
+ }
26
54
  fs.writeFileSync(file, GITIGNORE);
27
- return true;
55
+ return "created";
56
+ }
57
+
58
+ /**
59
+ * The nested .ouro/.gitignore only works if no *outer* rule already excludes
60
+ * the directory — git won't re-include a path under a parent an ancestor
61
+ * .gitignore (root, or a global core.excludesFile) has excluded. So a user who
62
+ * added `.ouro` to their root .gitignore would get every agent/config silently
63
+ * untracked with no error. We can't safely edit their root file, but we can
64
+ * detect the shadow and tell them exactly which rule to remove.
65
+ *
66
+ * Best-effort: probes a path the nested file explicitly un-ignores. If git is
67
+ * absent or this isn't a repo, `git check-ignore` throws and we stay quiet.
68
+ */
69
+ function warnIfOuroShadowed() {
70
+ // Decide ignored-ness with -q, NOT -v: `check-ignore -v` exits 0 whenever a
71
+ // pattern *matched*, including our own `!config.json` negation, which would
72
+ // false-fire on a perfectly healthy repo. Plain -q reflects the final state —
73
+ // it exits non-zero (throws here) when the path is ultimately un-ignored.
74
+ try {
75
+ execFileSync("git", ["check-ignore", "-q", ".ouro/config.json"], { cwd: repoRoot(), stdio: "ignore" });
76
+ } catch {
77
+ return; // not ignored (the good case), or git / not-a-repo — stay quiet
78
+ }
79
+
80
+ // Confirmed ignored — the only way, given our nested `!config.json`, is an
81
+ // outer blanket rule. Fetch it with -v purely for the message.
82
+ let match = "";
83
+ try {
84
+ match = execFileSync("git", ["check-ignore", "-v", ".ouro/config.json"], {
85
+ cwd: repoRoot(),
86
+ encoding: "utf-8",
87
+ stdio: ["ignore", "pipe", "ignore"],
88
+ }).trim();
89
+ } catch {
90
+ /* keep going — we know it's ignored, we just couldn't name the rule */
91
+ }
92
+
93
+ console.log("");
94
+ console.log(chalk.yellow("⚠ Your repo's git config ignores .ouro/ before .ouro/.gitignore can act on it."));
95
+ if (match) console.log(chalk.gray(` Rule: ${match}`));
96
+ console.log(chalk.gray(" Result: agents/, context/ and config.json won't be tracked — the point of committing them is lost."));
97
+ console.log(chalk.gray(" Fix: remove the .ouro entry from that file. .ouro/.gitignore already keeps run/, logs/, .env and tickets.json out."));
98
+ }
99
+
100
+ function reportGitignore(result) {
101
+ if (result === "created") {
102
+ console.log(
103
+ chalk.green("✔ Wrote .ouro/.gitignore") +
104
+ chalk.gray(" — commits agents/, context/, config.json; ignores run/, logs/, worktrees/, .env, tickets.json")
105
+ );
106
+ } else if (result === "upgraded") {
107
+ console.log(chalk.green("✔ Upgraded .ouro/.gitignore to the ignore-everything-then-unignore-safe pattern"));
108
+ } else if (result === "kept") {
109
+ console.log(
110
+ chalk.yellow("• Left your custom .ouro/.gitignore untouched") +
111
+ chalk.gray(" — make sure it ignores .env and the run/, logs/, worktrees/ dirs")
112
+ );
113
+ }
114
+ // "current" — already up to date, nothing worth a line.
115
+ }
116
+
117
+ const SPEC_PROMPT = `Explore this repository read-only and write a CLAUDE.md that would help an engineer or an AI agent work in it effectively.
118
+
119
+ Cover, based only on what the code actually shows:
120
+ - What the project is and does, in a sentence or two.
121
+ - The layout: the key directories and what lives in each.
122
+ - How to install, build, test, and run it — use the real commands from package.json / scripts / config you actually find.
123
+ - Conventions worth knowing: language, frameworks, module style, how the pieces are wired together.
124
+ - The main entry points and the handful of files a newcomer should read first.
125
+
126
+ Base every claim on files you actually read. Do not invent commands or features. If something isn't discoverable, omit it rather than guess.
127
+
128
+ Output ONLY the CLAUDE.md markdown content — no preamble, no sign-off, and no code fence wrapping the whole document.`;
129
+
130
+ // Prepended to the generated file. Reverse-engineering describes what the code
131
+ // *currently does*, so it faithfully documents existing bugs as if intended —
132
+ // say so, in the file itself, where whoever reads it will see it.
133
+ const SPEC_CAVEAT = `> **Auto-generated by \`ouro init --spec\`.** Reverse-engineered from the code, so it
134
+ > documents what the project *currently does* — including any bugs, as if they were
135
+ > intended. A starting point to review and correct, not ground truth.
136
+
137
+ `;
138
+
139
+ /** Unwrap a single code fence if the model wrapped the whole document in one. */
140
+ function cleanSpec(text) {
141
+ const s = String(text ?? "").trim();
142
+ const fenced = s.match(/^```(?:markdown|md)?\n([\s\S]*)\n```$/);
143
+ return (fenced ? fenced[1] : s).trim();
144
+ }
145
+
146
+ /**
147
+ * `ouro init --spec`: if the repo has no CLAUDE.md / AGENTS.md, run a read-only
148
+ * agent pass that explores the codebase and writes one. Best-effort and never
149
+ * fatal — init has already succeeded by the time this runs, so a failure here
150
+ * (agent unreachable, empty output) just prints a note and leaves init intact.
151
+ */
152
+ async function maybeReverseEngineerSpec() {
153
+ const existing = listReferencedFiles();
154
+ if (existing.length) {
155
+ console.log(chalk.gray(`• --spec: ${existing.map((f) => f.path).join(", ")} already present — skipping.`));
156
+ return;
157
+ }
158
+
159
+ console.log(chalk.cyan("Reverse-engineering a spec from the codebase (read-only)…"));
160
+ try {
161
+ const body = cleanSpec(await generateSpec({ prompt: SPEC_PROMPT, cwd: repoRoot() }));
162
+ if (!body) {
163
+ console.log(chalk.yellow("• --spec: the pass returned nothing — skipped, init is unaffected."));
164
+ return;
165
+ }
166
+ fs.writeFileSync(path.join(repoRoot(), "CLAUDE.md"), SPEC_CAVEAT + body + "\n");
167
+ console.log(chalk.green("✔ Wrote CLAUDE.md") + chalk.gray(" — reverse-engineered from the code."));
168
+ console.log(
169
+ chalk.gray(" It describes what the code CURRENTLY does, so it documents existing bugs as if intended. Review it.")
170
+ );
171
+ } catch (err) {
172
+ console.log(
173
+ chalk.yellow(`• --spec: couldn't generate a spec (${String(err.message || err).split("\n")[0]}) — init is unaffected.`)
174
+ );
175
+ }
28
176
  }
29
177
 
30
178
  export async function initCommand(opts) {
@@ -34,12 +182,14 @@ export async function initCommand(opts) {
34
182
  // feature has a config but neither, and would otherwise stay broken.
35
183
  const seeded = seedDefaultAgents();
36
184
  if (seeded) console.log(chalk.green(`✔ Added ${seeded} default agents in ${agentsDir()}`));
37
- if (writeGitignore()) console.log(chalk.green("✔ Added .ouro/.gitignore"));
185
+ reportGitignore(writeGitignore());
186
+ warnIfOuroShadowed();
187
+ if (opts?.spec) await maybeReverseEngineerSpec();
38
188
  return;
39
189
  }
40
190
 
41
191
  ensureOuroDir();
42
- writeGitignore();
192
+ const gitignoreResult = writeGitignore();
43
193
 
44
194
  const backend = opts?.backend === "codex" ? "codex" : "claude-code";
45
195
 
@@ -57,6 +207,9 @@ export async function initCommand(opts) {
57
207
 
58
208
  console.log(chalk.green("✔ ouro initialized.") + ` Created .ouro/config.json (backend: ${backend})`);
59
209
  console.log(chalk.green(`✔ Seeded ${seeded} agents`) + ` in .ouro/agents/ — plain markdown, edit them in your editor.`);
210
+ reportGitignore(gitignoreResult);
211
+ warnIfOuroShadowed();
212
+ if (opts?.spec) await maybeReverseEngineerSpec();
60
213
  console.log("");
61
214
  console.log("Next steps:");
62
215
  console.log(" 1. Run " + chalk.cyan("ouro start") + " — dashboard + intake agent, in the background.");
package/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
2
3
  import { Command } from "commander";
3
4
  import chalk from "chalk";
4
5
  import { loadEnvFile } from "./lib/env.js";
@@ -10,6 +11,10 @@ import { stopCommand } from "./commands/stop.js";
10
11
  import { statusCommand } from "./commands/status.js";
11
12
  import { logsCommand } from "./commands/logs.js";
12
13
 
14
+ const pkg = JSON.parse(
15
+ readFileSync(new URL("../package.json", import.meta.url), "utf8")
16
+ );
17
+
13
18
  // Before any command reads a token: the detached daemon has no shell to
14
19
  // inherit exports from, so `.ouro/.env` is what survives a closed terminal or
15
20
  // a reboot. A real export still wins over the file. See lib/env.js.
@@ -24,12 +29,13 @@ program
24
29
  "and agents that run on your existing Claude Code / Codex subscription.\n" +
25
30
  "No API key required."
26
31
  )
27
- .version("0.1.0");
32
+ .version(pkg.version);
28
33
 
29
34
  program
30
35
  .command("init")
31
36
  .description("Configure the current repo for ouro (creates .ouro/ config + ticket store)")
32
37
  .option("--backend <backend>", "claude-code | codex", "claude-code")
38
+ .option("--spec", "if no CLAUDE.md / AGENTS.md exists, reverse-engineer one (read-only, best-effort)")
33
39
  .action(initCommand);
34
40
 
35
41
  program
@@ -32,7 +32,11 @@ function backend() {
32
32
  // Unified interface — every route calls through here instead of importing
33
33
  // codexExec/claudeCodeExec directly, so switching backends is a one-line
34
34
  // config change picked up on the next call.
35
- export const triage = (args) => backend().triage(args);
35
+ export const analyze = (args) => backend().analyze(args);
36
+ // Read-only exploration returning raw markdown. Backs `ouro init --spec`.
37
+ export const generateSpec = (args) => backend().generateSpec(args);
38
+ // Staging QA gate — validates the running result, returns a JSON verdict.
39
+ export const qaReview = (args) => backend().qaReview(args);
36
40
  export const runAgent = (args) => backend().runAgent(args);
37
41
  export const planTicket = (args) => backend().planTicket(args);
38
42
  export const executeTicket = (args) => backend().executeTicket(args);
package/src/lib/agents.js CHANGED
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { EventEmitter } from "node:events";
4
4
  import { agentsDir, ensureOuroDir } from "./paths.js";
5
5
  import { parseFrontmatter, stringifyFrontmatter } from "./frontmatter.js";
6
+ import { readConfig, writeConfig } from "./config.js";
6
7
 
7
8
  // Agents are plain markdown on disk — `.ouro/agents/<id>.md` — so they're
8
9
  // diffable, reviewable in a PR, and editable in your editor without ouro
@@ -173,8 +174,12 @@ export function deleteAgent(id) {
173
174
  }
174
175
  }
175
176
 
176
- // Shipped defaults. Written only when `.ouro/agents/` has no files at all, so
177
- // `ouro init` in an existing repo never resurrects an agent you deleted.
177
+ // Shipped defaults. The original three seed only into a fresh `.ouro/agents/`
178
+ // (an empty dir) so `ouro init` in an existing repo never resurrects one you
179
+ // deleted. Seeds marked `topUp` were added in a later version — they're also
180
+ // pushed by id into an already-populated install so an upgrade actually
181
+ // delivers them; a ledger of shipped ids (config.seededAgentIds) then keeps a
182
+ // topUp agent gone once you delete it.
178
183
  const SEEDS = [
179
184
  {
180
185
  id: "senior-engineer",
@@ -230,17 +235,77 @@ Review for, in priority order:
230
235
 
231
236
  For each finding give: the file and line, what breaks, and the concrete input or state that triggers it. Skip anything you cannot substantiate — a speculative finding is worse than no finding.`,
232
237
  },
238
+ {
239
+ id: "analyst",
240
+ // topUp: a default added after the original three. Seeded into existing
241
+ // installs by id (not just brand-new repos), while a later deletion sticks.
242
+ topUp: true,
243
+ data: {
244
+ name: "Analyst",
245
+ glyph: "◇",
246
+ description: "Read-only. Scopes a ticket into a summary and checkable acceptance criteria.",
247
+ model: DEFAULT_MODEL,
248
+ tools: ["Read", "Grep", "Glob"],
249
+ },
250
+ body: `You are an analyst. You scope a ticket before any code is written. You have read-only tools — explore the codebase to ground your assessment, but never edit.
251
+
252
+ Produce, in your final message:
253
+ 1. A one-paragraph summary of what the ticket actually asks for, restated so an engineer could pick it up cold.
254
+ 2. A priority — low, medium, or high — with a one-line justification.
255
+ 3. The files and modules most likely to change, from reading the code rather than guessing.
256
+ 4. Explicit acceptance criteria: a short, checkable list that defines "done". Each item must be something a test or a reviewer could mark pass or fail. These are the contract the implementation and the QA gate are both held to — vague criteria let a bad change slip through, so make them concrete and hard to game.
257
+
258
+ If the ticket is too vague to write checkable criteria, say exactly what is missing instead of inventing plausible-sounding ones.`,
259
+ },
260
+ {
261
+ id: "senior-qa-engineer",
262
+ topUp: true,
263
+ data: {
264
+ name: "Senior QA Engineer",
265
+ glyph: "◎",
266
+ description: "Read-only. Validates the running result against acceptance criteria — ready to ship, or loop back.",
267
+ model: DEFAULT_MODEL,
268
+ tools: ["Read", "Grep", "Glob"],
269
+ },
270
+ body: `You are a senior QA engineer validating a change that has already been implemented, in an isolated git worktree. You have read-only tools — you validate, you never modify or run the code. You are an independent check, separate from code review: the reviewer reads the diff, you validate the running result against the ticket's acceptance criteria.
271
+
272
+ Method:
273
+ 1. Take the acceptance criteria from the analysis as your definition of "ready". Validate against them, not against your own idea of done.
274
+ 2. Assess the test results ouro ran for you — what passed, what failed. Never call for weakening or deleting a test to make it pass.
275
+ 3. Read the diff. If the change touches UI (.jsx / .tsx / .css / .html and the like) and no screenshot is available, review the rendered/built HTML and the UI files with your Read tool instead — do not silently skip visual validation, substitute HTML analysis for it.
276
+ 4. Decide: ready to ship, or loop back to In Progress with specific, actionable reasons. "Not ready" with no concrete reason is not a verdict.
277
+
278
+ Judge the running result, not the intent. A change that reads correctly but fails its acceptance criteria is not ready.`,
279
+ },
233
280
  ];
234
281
 
235
282
  export function seedDefaultAgents() {
236
283
  ensureOuroDir();
237
- const existing = fs.existsSync(agentsDir()) ? fs.readdirSync(agentsDir()).filter((f) => f.endsWith(".md")) : [];
238
- if (existing.length > 0) return 0;
284
+ const files = fs.existsSync(agentsDir()) ? fs.readdirSync(agentsDir()).filter((f) => f.endsWith(".md")) : [];
285
+ const isEmpty = files.length === 0;
286
+
287
+ const shipped = new Set(readConfig().seededAgentIds ?? []);
288
+ const recorded = new Set(shipped);
289
+ let seeded = 0;
239
290
 
240
291
  for (const seed of SEEDS) {
292
+ if (files.includes(`${seed.id}.md`)) {
293
+ recorded.add(seed.id); // already on disk (e.g. a pre-ledger install) — record, don't rewrite
294
+ continue;
295
+ }
296
+ if (shipped.has(seed.id)) continue; // shipped before and now absent = deleted on purpose
297
+ // Legacy seeds only on a clean slate; topUp seeds also fill an existing repo.
298
+ if (!isEmpty && !seed.topUp) continue;
299
+
241
300
  fs.writeFileSync(agentPath(seed.id), stringifyFrontmatter(seed.data, seed.body));
301
+ recorded.add(seed.id);
302
+ seeded++;
242
303
  }
243
- return SEEDS.length;
304
+
305
+ // Persist the ledger only when it actually grew, so a steady-state init stays
306
+ // a no-op write. config.json is committed, so the record travels with the repo.
307
+ if (recorded.size !== shipped.size) writeConfig({ seededAgentIds: [...recorded] });
308
+ return seeded;
244
309
  }
245
310
 
246
311
  /** The agent a ticket runs as when it has no explicit assignment. */
@@ -0,0 +1,98 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { contextDir, repoRoot } from "./paths.js";
4
+
5
+ // The artifacts system — a shared, per-run context payload.
6
+ //
7
+ // One folder (.ouro/context/), one manifest. ouro guarantees *discoverability*:
8
+ // every run gets the folder path plus a manifest — the filenames, each with a
9
+ // one-line description — injected into its prompt. The agent controls
10
+ // *consumption*: it reads only what it judges relevant via its normal Read tool.
11
+ // We never dump file contents into the prompt. That's the whole point — it keeps
12
+ // context lean and lets self-describing filenames do the routing.
13
+ //
14
+ // Nothing is copied. Root convention files (CLAUDE.md / AGENTS.md) that the
15
+ // underlying CLIs already auto-read stay where they are; copying CLAUDE.md into
16
+ // the folder would just create two sources of truth that drift.
17
+
18
+ // Root convention files, referenced in place (never copied) and surfaced in the
19
+ // artifacts UI so "everything the agent can see" is one view.
20
+ const REFERENCED_FILENAMES = ["CLAUDE.md", "AGENTS.md", "CLAUDE.local.md"];
21
+
22
+ /**
23
+ * A one-line description for a context file: its frontmatter `description:` if
24
+ * present, else its first meaningful line (heading hashes stripped). Best-effort
25
+ * and truncated — this is a manifest label, not the file.
26
+ */
27
+ function describeFile(filePath) {
28
+ let text;
29
+ try {
30
+ text = fs.readFileSync(filePath, "utf-8");
31
+ } catch {
32
+ return "";
33
+ }
34
+
35
+ const fm = text.match(/^---\n([\s\S]*?)\n---/);
36
+ if (fm) {
37
+ const m = fm[1].match(/^description:\s*(.+)$/m);
38
+ if (m) return m[1].trim().replace(/^["']|["']$/g, "").slice(0, 140);
39
+ }
40
+
41
+ for (const raw of text.split("\n")) {
42
+ const line = raw.trim();
43
+ if (!line || line === "---") continue;
44
+ return line.replace(/^#+\s*/, "").slice(0, 140);
45
+ }
46
+ return "";
47
+ }
48
+
49
+ /** Files dropped in .ouro/context/, each with a one-line description. */
50
+ export function listContextFiles() {
51
+ let names = [];
52
+ try {
53
+ names = fs.readdirSync(contextDir()).filter((f) => !f.startsWith("."));
54
+ } catch {
55
+ return []; // no context dir yet — nothing to advertise
56
+ }
57
+
58
+ return names
59
+ .map((name) => {
60
+ const full = path.join(contextDir(), name);
61
+ let stat;
62
+ try {
63
+ stat = fs.statSync(full);
64
+ } catch {
65
+ return null;
66
+ }
67
+ if (!stat.isFile()) return null;
68
+ return { name, description: describeFile(full), size: stat.size };
69
+ })
70
+ .filter(Boolean)
71
+ .sort((a, b) => a.name.localeCompare(b.name));
72
+ }
73
+
74
+ /** Root convention files that exist — referenced in place, not copied. */
75
+ export function listReferencedFiles() {
76
+ return REFERENCED_FILENAMES.filter((name) => fs.existsSync(path.join(repoRoot(), name))).map((name) => ({
77
+ name,
78
+ path: name,
79
+ }));
80
+ }
81
+
82
+ /**
83
+ * The manifest string injected into a run's prompt. Manifest-only: folder path,
84
+ * filenames, one-line descriptions — never file contents. Returns "" when the
85
+ * folder is empty, so callers can concatenate unconditionally.
86
+ */
87
+ export function contextManifest() {
88
+ const files = listContextFiles();
89
+ if (files.length === 0) return "";
90
+
91
+ const lines = [
92
+ "Shared context files live in .ouro/context/. They are listed here, NOT included — read any that look relevant with your Read tool:",
93
+ ];
94
+ for (const f of files) {
95
+ lines.push(`- .ouro/context/${f.name}${f.description ? ` — ${f.description}` : ""}`);
96
+ }
97
+ return lines.join("\n");
98
+ }