@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.
- package/dashboard-dist/assets/index-B-lZ4RcX.js +64 -0
- package/dashboard-dist/assets/index-C21ZXjPS.css +1 -0
- package/dashboard-dist/index.html +2 -2
- package/package.json +1 -1
- package/src/commands/dashboard.js +2 -0
- package/src/commands/init.js +168 -15
- package/src/index.js +7 -1
- package/src/lib/agentBackend.js +5 -1
- package/src/lib/agents.js +70 -5
- package/src/lib/artifacts.js +98 -0
- package/src/lib/claudeCodeExec.js +53 -8
- package/src/lib/codexExec.js +41 -9
- package/src/lib/config.js +19 -0
- package/src/lib/ouroLog.js +91 -0
- package/src/lib/paths.js +8 -1
- package/src/lib/preview.js +120 -0
- package/src/lib/ship.js +2 -2
- package/src/lib/staging.js +97 -0
- package/src/lib/store.js +52 -4
- package/src/server/index.js +339 -19
- package/dashboard-dist/assets/index-ETSLKv6w.css +0 -1
- package/dashboard-dist/assets/index-UHUWnWxM.js +0 -63
|
@@ -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-
|
|
13
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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
|
@@ -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
|
package/src/commands/init.js
CHANGED
|
@@ -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
|
|
11
|
-
// point of agents-as-markdown
|
|
12
|
-
// a bot token), a scratch checkout, or per-machine
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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))
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
package/src/lib/agentBackend.js
CHANGED
|
@@ -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
|
|
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.
|
|
177
|
-
// `ouro init` in an existing repo never resurrects
|
|
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
|
|
238
|
-
|
|
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
|
-
|
|
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
|
+
}
|