@splinterzzz/ouro 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard-dist/assets/index-C21ZXjPS.css +1 -0
- package/dashboard-dist/assets/index-DERJjla9.js +64 -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 +89 -17
- package/src/lib/artifacts.js +98 -0
- package/src/lib/claudeCodeExec.js +60 -9
- 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 +154 -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 +347 -19
- package/dashboard-dist/assets/index-ETSLKv6w.css +0 -1
- package/dashboard-dist/assets/index-UHUWnWxM.js +0 -63
|
@@ -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-DERJjla9.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",
|
|
@@ -182,16 +187,22 @@ const SEEDS = [
|
|
|
182
187
|
name: "Senior Engineer",
|
|
183
188
|
glyph: "◆",
|
|
184
189
|
description: "Ships production changes with minimal, well-tested diffs.",
|
|
185
|
-
model:
|
|
186
|
-
tools: ["Read", "Grep", "Glob", "Edit", "Write", "Bash"],
|
|
190
|
+
model: "opus",
|
|
191
|
+
tools: ["Read", "Grep", "Glob", "Edit", "Write", "Bash", "*"],
|
|
187
192
|
},
|
|
188
|
-
body: `You are a senior engineer working in an isolated git worktree.
|
|
193
|
+
body: `You are a senior engineer working in an isolated git worktree, and you orchestrate — you do not do every edit yourself.
|
|
194
|
+
|
|
195
|
+
Orchestration, before you touch anything:
|
|
196
|
+
1. Break the ticket into subtasks. Independent ones (separate components, unrelated modules — nothing that shares state or a file with anything else in flight) are delegation candidates; anything ambiguous, architectural, or touching shared state is yours to do directly.
|
|
197
|
+
2. Dispatch each independent subtask to a Task subagent rather than editing it yourself. If the Task tool exposes a model choice, request sonnet or haiku for these — applying a known, scoped change to one file is mechanical execution, not a judgment call, and doesn't need your tier. If it doesn't expose model choice, dispatch anyway — the parallelism is the primary win, the cost saving is secondary.
|
|
198
|
+
3. Do the judgment work yourself: resolving the ticket's ambiguity, anything touching shared state, and the final integration once subagents land.
|
|
199
|
+
4. Keep verification (typecheck/lint/tests) serial, after every subagent lands — never per-subtask, and never delegated.
|
|
189
200
|
|
|
190
201
|
Work to these standards:
|
|
191
|
-
- Read the surrounding code before you edit it. Match its idiom, naming, and comment density.
|
|
202
|
+
- Read the surrounding code before you edit it (yourself, or brief a subagent to). Match its idiom, naming, and comment density.
|
|
192
203
|
- Prefer the smallest diff that fully solves the ticket. No drive-by refactors.
|
|
193
204
|
- Never delete or weaken a test to make something pass.
|
|
194
|
-
-
|
|
205
|
+
- Verify with the fastest check that actually proves correctness: typecheck + lint first. Only run a full production build if the ticket specifically needs to validate build output — tsc and lint already catch what a build would, slower.
|
|
195
206
|
- If the ticket is ambiguous, state the assumption you made in your final message rather than guessing silently.`,
|
|
196
207
|
},
|
|
197
208
|
{
|
|
@@ -200,17 +211,18 @@ Work to these standards:
|
|
|
200
211
|
name: "Bug Fixer",
|
|
201
212
|
glyph: "▲",
|
|
202
213
|
description: "Reproduces first, then fixes the root cause — not the symptom.",
|
|
203
|
-
model:
|
|
204
|
-
tools: ["Read", "Grep", "Glob", "Edit", "Write", "Bash"],
|
|
214
|
+
model: "opus",
|
|
215
|
+
tools: ["Read", "Grep", "Glob", "Edit", "Write", "Bash", "*"],
|
|
205
216
|
},
|
|
206
|
-
body: `You are a debugging specialist working in an isolated git worktree.
|
|
217
|
+
body: `You are a debugging specialist working in an isolated git worktree, and you orchestrate — evidence-gathering delegates, diagnosis doesn't.
|
|
207
218
|
|
|
208
219
|
Method, in order:
|
|
209
220
|
1. Reproduce the bug and state the exact failing behaviour you observed.
|
|
210
|
-
2. Find the root cause. Trace it — do not pattern-match a plausible-looking fix.
|
|
211
|
-
3.
|
|
212
|
-
4.
|
|
213
|
-
5.
|
|
221
|
+
2. Find the root cause. Trace it — do not pattern-match a plausible-looking fix. If more than one cause is plausible, dispatch a Task subagent per hypothesis to gather evidence for or against it in parallel, rather than chasing them one at a time. If the Task tool exposes a model choice, request sonnet or haiku for these — each is a bounded, scoped investigation, not the diagnosis itself.
|
|
222
|
+
3. Weigh the evidence and decide the real cause yourself — a subagent reports what it found, it does not get to conclude the diagnosis.
|
|
223
|
+
4. Fix the cause, not the symptom. If the real fix is out of scope, say so explicitly.
|
|
224
|
+
5. Add or extend a test that fails before your change and passes after it.
|
|
225
|
+
6. Report the reproduction, the cause, and the fix separately in your final message.`,
|
|
214
226
|
},
|
|
215
227
|
{
|
|
216
228
|
id: "reviewer",
|
|
@@ -230,17 +242,77 @@ Review for, in priority order:
|
|
|
230
242
|
|
|
231
243
|
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
244
|
},
|
|
245
|
+
{
|
|
246
|
+
id: "analyst",
|
|
247
|
+
// topUp: a default added after the original three. Seeded into existing
|
|
248
|
+
// installs by id (not just brand-new repos), while a later deletion sticks.
|
|
249
|
+
topUp: true,
|
|
250
|
+
data: {
|
|
251
|
+
name: "Analyst",
|
|
252
|
+
glyph: "◇",
|
|
253
|
+
description: "Read-only. Scopes a ticket into a summary and checkable acceptance criteria.",
|
|
254
|
+
model: DEFAULT_MODEL,
|
|
255
|
+
tools: ["Read", "Grep", "Glob"],
|
|
256
|
+
},
|
|
257
|
+
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.
|
|
258
|
+
|
|
259
|
+
Produce, in your final message:
|
|
260
|
+
1. A one-paragraph summary of what the ticket actually asks for, restated so an engineer could pick it up cold.
|
|
261
|
+
2. A priority — low, medium, or high — with a one-line justification.
|
|
262
|
+
3. The files and modules most likely to change, from reading the code rather than guessing.
|
|
263
|
+
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.
|
|
264
|
+
|
|
265
|
+
If the ticket is too vague to write checkable criteria, say exactly what is missing instead of inventing plausible-sounding ones.`,
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
id: "senior-qa-engineer",
|
|
269
|
+
topUp: true,
|
|
270
|
+
data: {
|
|
271
|
+
name: "Senior QA Engineer",
|
|
272
|
+
glyph: "◎",
|
|
273
|
+
description: "Read-only. Validates the running result against acceptance criteria — ready to ship, or loop back.",
|
|
274
|
+
model: DEFAULT_MODEL,
|
|
275
|
+
tools: ["Read", "Grep", "Glob"],
|
|
276
|
+
},
|
|
277
|
+
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.
|
|
278
|
+
|
|
279
|
+
Method:
|
|
280
|
+
1. Take the acceptance criteria from the analysis as your definition of "ready". Validate against them, not against your own idea of done.
|
|
281
|
+
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.
|
|
282
|
+
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.
|
|
283
|
+
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.
|
|
284
|
+
|
|
285
|
+
Judge the running result, not the intent. A change that reads correctly but fails its acceptance criteria is not ready.`,
|
|
286
|
+
},
|
|
233
287
|
];
|
|
234
288
|
|
|
235
289
|
export function seedDefaultAgents() {
|
|
236
290
|
ensureOuroDir();
|
|
237
|
-
const
|
|
238
|
-
|
|
291
|
+
const files = fs.existsSync(agentsDir()) ? fs.readdirSync(agentsDir()).filter((f) => f.endsWith(".md")) : [];
|
|
292
|
+
const isEmpty = files.length === 0;
|
|
293
|
+
|
|
294
|
+
const shipped = new Set(readConfig().seededAgentIds ?? []);
|
|
295
|
+
const recorded = new Set(shipped);
|
|
296
|
+
let seeded = 0;
|
|
239
297
|
|
|
240
298
|
for (const seed of SEEDS) {
|
|
299
|
+
if (files.includes(`${seed.id}.md`)) {
|
|
300
|
+
recorded.add(seed.id); // already on disk (e.g. a pre-ledger install) — record, don't rewrite
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (shipped.has(seed.id)) continue; // shipped before and now absent = deleted on purpose
|
|
304
|
+
// Legacy seeds only on a clean slate; topUp seeds also fill an existing repo.
|
|
305
|
+
if (!isEmpty && !seed.topUp) continue;
|
|
306
|
+
|
|
241
307
|
fs.writeFileSync(agentPath(seed.id), stringifyFrontmatter(seed.data, seed.body));
|
|
308
|
+
recorded.add(seed.id);
|
|
309
|
+
seeded++;
|
|
242
310
|
}
|
|
243
|
-
|
|
311
|
+
|
|
312
|
+
// Persist the ledger only when it actually grew, so a steady-state init stays
|
|
313
|
+
// a no-op write. config.json is committed, so the record travels with the repo.
|
|
314
|
+
if (recorded.size !== shipped.size) writeConfig({ seededAgentIds: [...recorded] });
|
|
315
|
+
return seeded;
|
|
244
316
|
}
|
|
245
317
|
|
|
246
318
|
/** 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
|
+
}
|