baro-ai 0.2.0 → 0.3.0
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/README.md +119 -0
- package/bin/baro +0 -0
- package/dist/openai-planner.js +411 -0
- package/package.json +4 -8
- package/scripts/postinstall.js +11 -15
- package/dist/chunk-HWC47EK2.js +0 -61
- package/dist/cli.js +0 -862
- package/dist/core/executor.js +0 -170
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# baro
|
|
2
|
+
|
|
3
|
+
Autonomous parallel coding engine. Give it a goal, it breaks it into stories, builds a dependency DAG, and executes them in parallel using AI agents.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npm install -g baro-ai
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## How it works
|
|
10
|
+
|
|
11
|
+
1. **You describe a goal** - "Add authentication with JWT and role-based access control"
|
|
12
|
+
2. **AI plans the work** - Claude (or OpenAI) explores your codebase and creates a dependency graph of user stories
|
|
13
|
+
3. **You review the plan** - Scrollable plan review with accept/refine/quit
|
|
14
|
+
4. **Stories execute in parallel** - Independent stories run simultaneously, each with its own Claude agent
|
|
15
|
+
5. **Live TUI dashboard** - Watch progress, logs, DAG visualization, and stats in real-time
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
### Interactive mode
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
baro
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Opens the welcome screen where you type your goal and choose a planner.
|
|
26
|
+
|
|
27
|
+
### Direct mode
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
baro "Add a REST API for user management"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Skips the welcome screen and starts planning immediately.
|
|
34
|
+
|
|
35
|
+
### Options
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
baro [goal] [options]
|
|
39
|
+
|
|
40
|
+
Arguments:
|
|
41
|
+
goal Project goal (opens welcome screen if omitted)
|
|
42
|
+
|
|
43
|
+
Options:
|
|
44
|
+
--planner <planner> Planner to use: claude or openai (default: claude)
|
|
45
|
+
--cwd <path> Working directory (default: current directory)
|
|
46
|
+
-h, --help Print help
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Examples
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Interactive - opens welcome screen
|
|
53
|
+
baro
|
|
54
|
+
|
|
55
|
+
# Plan and execute with Claude (default)
|
|
56
|
+
baro "Refactor the database layer to use connection pooling"
|
|
57
|
+
|
|
58
|
+
# Use OpenAI for planning
|
|
59
|
+
baro --planner openai "Add WebSocket support"
|
|
60
|
+
|
|
61
|
+
# Run in a specific directory
|
|
62
|
+
baro --cwd ~/projects/myapp "Add unit tests for all API endpoints"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## TUI Screens
|
|
66
|
+
|
|
67
|
+
### Welcome
|
|
68
|
+
|
|
69
|
+
ASCII art logo, goal text input, and planner toggle (Claude/OpenAI).
|
|
70
|
+
|
|
71
|
+
### Planning
|
|
72
|
+
|
|
73
|
+
Animated spinner showing planning progress with elapsed timer. The selected AI explores your codebase and generates a structured plan.
|
|
74
|
+
|
|
75
|
+
### Review
|
|
76
|
+
|
|
77
|
+
Scrollable list of all planned stories with descriptions and dependencies. Navigate with arrow keys, accept with Enter, or quit with q.
|
|
78
|
+
|
|
79
|
+
### Execution Dashboard
|
|
80
|
+
|
|
81
|
+
Three tabs while stories execute:
|
|
82
|
+
|
|
83
|
+
- **Dashboard** - Story list with status icons + live logs from the active agent
|
|
84
|
+
- **DAG** - Dependency graph visualization showing levels and connections
|
|
85
|
+
- **Stats** - Summary table with times, file counts, and completion stats
|
|
86
|
+
|
|
87
|
+
Keybinds: `1/2/3` switch tabs, `Tab/Shift+Tab` switch log panels, `q` quit.
|
|
88
|
+
|
|
89
|
+
## Requirements
|
|
90
|
+
|
|
91
|
+
- [Claude CLI](https://docs.anthropic.com/en/docs/claude-cli) installed and authenticated (for Claude planner/executor)
|
|
92
|
+
- Node.js 18+ (only needed if using `--planner openai`)
|
|
93
|
+
- macOS (arm64/x64) or Linux (x64/arm64)
|
|
94
|
+
|
|
95
|
+
## Architecture
|
|
96
|
+
|
|
97
|
+
Baro is a Rust binary distributed via npm:
|
|
98
|
+
|
|
99
|
+
- **TUI** - ratatui-based terminal UI with 4 screens
|
|
100
|
+
- **Planner** - Spawns Claude CLI or OpenAI (via Node.js bridge) to generate a PRD
|
|
101
|
+
- **DAG Engine** - Kahn's algorithm for topological sort with level grouping
|
|
102
|
+
- **Executor** - Parallel story execution via tokio, one Claude agent per story
|
|
103
|
+
- **npm package** - `postinstall` downloads the prebuilt binary for your platform
|
|
104
|
+
|
|
105
|
+
## Development
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# Build the Rust binary
|
|
109
|
+
cargo build -p baro-tui --release
|
|
110
|
+
|
|
111
|
+
# Run locally
|
|
112
|
+
./target/release/baro "your goal"
|
|
113
|
+
|
|
114
|
+
# The binary is in crates/baro-tui/
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT
|
package/bin/baro
ADDED
|
Binary file
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/core/planner.ts
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
6
|
+
|
|
7
|
+
// src/core/stream.ts
|
|
8
|
+
var API_URL = "https://api.openai.com/v1/responses";
|
|
9
|
+
var MAX_TOOL_ROUNDS = 15;
|
|
10
|
+
async function streamCompletion(opts) {
|
|
11
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
12
|
+
if (!apiKey) throw new Error("OPENAI_API_KEY not set");
|
|
13
|
+
let input = [
|
|
14
|
+
...opts.messages.map((m) => ({ role: m.role, content: m.content })),
|
|
15
|
+
{ role: "user", content: opts.task }
|
|
16
|
+
];
|
|
17
|
+
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
|
|
18
|
+
const body = { model: opts.model, input, stream: true };
|
|
19
|
+
if (opts.reasoning) body.reasoning = opts.reasoning;
|
|
20
|
+
if (opts.jsonSchema) {
|
|
21
|
+
body.text = {
|
|
22
|
+
format: { type: "json_schema", name: "prd_output", schema: opts.jsonSchema, strict: true }
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (opts.tools?.length) {
|
|
26
|
+
body.tools = opts.tools.map((t) => ({
|
|
27
|
+
type: "function",
|
|
28
|
+
name: t.name,
|
|
29
|
+
description: t.description,
|
|
30
|
+
parameters: t.parameters
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
const response = await fetch(API_URL, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
36
|
+
body: JSON.stringify(body)
|
|
37
|
+
});
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
const errText = await response.text();
|
|
40
|
+
throw new Error(`OpenAI API error ${response.status}: ${errText}`);
|
|
41
|
+
}
|
|
42
|
+
if (!response.body) throw new Error("No response body");
|
|
43
|
+
const { textOutput, toolCalls, responseId } = await parseSSE(response.body, opts);
|
|
44
|
+
if (toolCalls.length === 0) {
|
|
45
|
+
return textOutput;
|
|
46
|
+
}
|
|
47
|
+
const toolResults = [];
|
|
48
|
+
for (const tc of toolCalls) {
|
|
49
|
+
const toolDef = opts.tools?.find((t) => t.name === tc.name);
|
|
50
|
+
if (!toolDef) continue;
|
|
51
|
+
opts.onToolCall?.(tc.name, tc.args);
|
|
52
|
+
let result;
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(tc.args);
|
|
55
|
+
result = await toolDef.invoke(parsed);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
result = `Error: ${err.message}`;
|
|
58
|
+
}
|
|
59
|
+
toolResults.push({
|
|
60
|
+
type: "function_call_output",
|
|
61
|
+
call_id: tc.callId,
|
|
62
|
+
output: typeof result === "string" ? result : JSON.stringify(result)
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
input = toolResults;
|
|
66
|
+
}
|
|
67
|
+
throw new Error("Too many tool calling rounds");
|
|
68
|
+
}
|
|
69
|
+
async function parseSSE(body, opts) {
|
|
70
|
+
let textOutput = "";
|
|
71
|
+
const toolCalls = [];
|
|
72
|
+
const toolCallArgs = /* @__PURE__ */ new Map();
|
|
73
|
+
let responseId = "";
|
|
74
|
+
const reader = body.getReader();
|
|
75
|
+
const decoder = new TextDecoder();
|
|
76
|
+
let buffer = "";
|
|
77
|
+
while (true) {
|
|
78
|
+
const { done, value } = await reader.read();
|
|
79
|
+
if (done) break;
|
|
80
|
+
buffer += decoder.decode(value, { stream: true });
|
|
81
|
+
const lines = buffer.split("\n");
|
|
82
|
+
buffer = lines.pop() ?? "";
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
if (!line.startsWith("data: ")) continue;
|
|
85
|
+
const data = line.slice(6).trim();
|
|
86
|
+
if (data === "[DONE]") continue;
|
|
87
|
+
try {
|
|
88
|
+
const ev = JSON.parse(data);
|
|
89
|
+
if (ev.type === "response.created") {
|
|
90
|
+
responseId = ev.response?.id ?? "";
|
|
91
|
+
} else if (ev.type === "response.output_text.delta") {
|
|
92
|
+
textOutput += ev.delta ?? "";
|
|
93
|
+
opts.onToken(ev.delta ?? "");
|
|
94
|
+
} else if (ev.type === "response.reasoning.delta") {
|
|
95
|
+
opts.onThinking?.(ev.delta ?? "");
|
|
96
|
+
} else if (ev.type === "response.output_item.added") {
|
|
97
|
+
if (ev.item?.type === "function_call") {
|
|
98
|
+
toolCallArgs.set(ev.output_index, {
|
|
99
|
+
callId: ev.item.call_id ?? "",
|
|
100
|
+
name: ev.item.name ?? "",
|
|
101
|
+
args: ""
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
} else if (ev.type === "response.function_call_arguments.delta") {
|
|
105
|
+
const tc = toolCallArgs.get(ev.output_index);
|
|
106
|
+
if (tc) tc.args += ev.delta ?? "";
|
|
107
|
+
} else if (ev.type === "response.output_item.done") {
|
|
108
|
+
if (ev.item?.type === "function_call") {
|
|
109
|
+
const tc = toolCallArgs.get(ev.output_index);
|
|
110
|
+
if (tc) {
|
|
111
|
+
toolCalls.push({ callId: tc.callId, name: tc.name, args: tc.args });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { textOutput, toolCalls, responseId };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/core/tools.ts
|
|
123
|
+
import * as fs from "fs";
|
|
124
|
+
import * as path from "path";
|
|
125
|
+
import { execSync } from "child_process";
|
|
126
|
+
var IGNORE = /* @__PURE__ */ new Set([
|
|
127
|
+
"node_modules",
|
|
128
|
+
".git",
|
|
129
|
+
"dist",
|
|
130
|
+
"build",
|
|
131
|
+
".next",
|
|
132
|
+
".nuxt",
|
|
133
|
+
"coverage",
|
|
134
|
+
".cache",
|
|
135
|
+
"__pycache__",
|
|
136
|
+
"target",
|
|
137
|
+
".output"
|
|
138
|
+
]);
|
|
139
|
+
var MAX_FILE_SIZE = 15e3;
|
|
140
|
+
function createCodebaseTools(cwd2) {
|
|
141
|
+
return [
|
|
142
|
+
{
|
|
143
|
+
name: "list_files",
|
|
144
|
+
description: "List files and directories. Use path='' for project root. Returns file names with types. Ignores node_modules, .git, etc.",
|
|
145
|
+
parameters: {
|
|
146
|
+
type: "object",
|
|
147
|
+
properties: {
|
|
148
|
+
path: { type: "string", description: "Relative path from project root. Empty for root." },
|
|
149
|
+
recursive: { type: "boolean", description: "List all files recursively (max 200). Default false." }
|
|
150
|
+
},
|
|
151
|
+
required: ["path"],
|
|
152
|
+
additionalProperties: false
|
|
153
|
+
},
|
|
154
|
+
async invoke(args2) {
|
|
155
|
+
const target = safePath(cwd2, args2.path || ".");
|
|
156
|
+
if (!target || !fs.existsSync(target)) return `Directory not found: ${args2.path}`;
|
|
157
|
+
if (!fs.statSync(target).isDirectory()) return `Not a directory: ${args2.path}`;
|
|
158
|
+
const results = [];
|
|
159
|
+
function walk(dir, prefix, depth) {
|
|
160
|
+
if (results.length >= 200 || depth > 4) return;
|
|
161
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
162
|
+
if (IGNORE.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
163
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
164
|
+
if (entry.isDirectory()) {
|
|
165
|
+
results.push(rel + "/");
|
|
166
|
+
if (args2.recursive) walk(path.join(dir, entry.name), rel, depth + 1);
|
|
167
|
+
} else {
|
|
168
|
+
results.push(rel);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
walk(target, "", 0);
|
|
173
|
+
return results.join("\n") || "(empty directory)";
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: "read_file",
|
|
178
|
+
description: "Read file contents. Large files truncated to ~15000 chars. Use to understand code structure, configs, etc.",
|
|
179
|
+
parameters: {
|
|
180
|
+
type: "object",
|
|
181
|
+
properties: {
|
|
182
|
+
path: { type: "string", description: "Relative path to file (e.g. 'src/index.ts')" }
|
|
183
|
+
},
|
|
184
|
+
required: ["path"],
|
|
185
|
+
additionalProperties: false
|
|
186
|
+
},
|
|
187
|
+
async invoke(args2) {
|
|
188
|
+
const target = safePath(cwd2, args2.path);
|
|
189
|
+
if (!target || !fs.existsSync(target)) return `File not found: ${args2.path}`;
|
|
190
|
+
if (fs.statSync(target).isDirectory()) return `${args2.path} is a directory. Use list_files.`;
|
|
191
|
+
if (fs.statSync(target).size > 5e5) return `File too large (${(fs.statSync(target).size / 1024).toFixed(0)}KB)`;
|
|
192
|
+
let content = fs.readFileSync(target, "utf-8");
|
|
193
|
+
if (content.length > MAX_FILE_SIZE) {
|
|
194
|
+
content = content.slice(0, MAX_FILE_SIZE) + "\n... (truncated)";
|
|
195
|
+
}
|
|
196
|
+
return content;
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: "grep",
|
|
201
|
+
description: "Search for a text pattern across project files. Returns matching lines with file paths. Ignores node_modules, .git, etc.",
|
|
202
|
+
parameters: {
|
|
203
|
+
type: "object",
|
|
204
|
+
properties: {
|
|
205
|
+
pattern: { type: "string", description: "Text to search for (case-insensitive)" },
|
|
206
|
+
path: { type: "string", description: "Directory to search in. Default: entire project." },
|
|
207
|
+
file_pattern: { type: "string", description: "File glob (e.g. '*.ts'). Default: all." }
|
|
208
|
+
},
|
|
209
|
+
required: ["pattern"],
|
|
210
|
+
additionalProperties: false
|
|
211
|
+
},
|
|
212
|
+
async invoke(args2) {
|
|
213
|
+
const searchDir = safePath(cwd2, args2.path || ".");
|
|
214
|
+
if (!searchDir || !fs.existsSync(searchDir)) return `Directory not found: ${args2.path}`;
|
|
215
|
+
try {
|
|
216
|
+
const excludes = Array.from(IGNORE).map((d) => `--exclude-dir=${d}`).join(" ");
|
|
217
|
+
const include = args2.file_pattern ? `--include='${args2.file_pattern}'` : "";
|
|
218
|
+
const cmd = `grep -rn -i ${excludes} ${include} --max-count=50 -- ${JSON.stringify(args2.pattern)} ${JSON.stringify(searchDir)} 2>/dev/null || true`;
|
|
219
|
+
const output = execSync(cmd, { encoding: "utf-8", maxBuffer: 1024 * 1024 });
|
|
220
|
+
const lines = output.split("\n").filter(Boolean).map(
|
|
221
|
+
(line) => line.startsWith(cwd2) ? line.slice(cwd2.length + 1) : line
|
|
222
|
+
);
|
|
223
|
+
return lines.slice(0, 50).join("\n") || "No matches found.";
|
|
224
|
+
} catch {
|
|
225
|
+
return "No matches found.";
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: "file_tree",
|
|
231
|
+
description: "Get a condensed tree view of the project structure up to 3 levels deep. Good starting point to understand the codebase.",
|
|
232
|
+
parameters: {
|
|
233
|
+
type: "object",
|
|
234
|
+
properties: {},
|
|
235
|
+
additionalProperties: false
|
|
236
|
+
},
|
|
237
|
+
async invoke() {
|
|
238
|
+
const lines = [path.basename(cwd2) + "/"];
|
|
239
|
+
function walk(dir, prefix, depth) {
|
|
240
|
+
if (lines.length >= 150 || depth > 3) return;
|
|
241
|
+
let entries;
|
|
242
|
+
try {
|
|
243
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
244
|
+
} catch {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
entries.sort((a, b) => {
|
|
248
|
+
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
249
|
+
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
250
|
+
return a.name.localeCompare(b.name);
|
|
251
|
+
});
|
|
252
|
+
for (let i = 0; i < entries.length; i++) {
|
|
253
|
+
if (IGNORE.has(entries[i].name) || entries[i].name.startsWith(".")) continue;
|
|
254
|
+
const isLast = i === entries.length - 1;
|
|
255
|
+
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
256
|
+
const childPrefix = isLast ? " " : "\u2502 ";
|
|
257
|
+
if (entries[i].isDirectory()) {
|
|
258
|
+
lines.push(`${prefix}${connector}${entries[i].name}/`);
|
|
259
|
+
walk(path.join(dir, entries[i].name), prefix + childPrefix, depth + 1);
|
|
260
|
+
} else {
|
|
261
|
+
lines.push(`${prefix}${connector}${entries[i].name}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
walk(cwd2, "", 0);
|
|
266
|
+
return lines.join("\n");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
];
|
|
270
|
+
}
|
|
271
|
+
function safePath(cwd2, filePath) {
|
|
272
|
+
const resolved = path.resolve(cwd2, filePath);
|
|
273
|
+
if (!resolved.startsWith(path.resolve(cwd2))) return null;
|
|
274
|
+
return resolved;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/core/planner.ts
|
|
278
|
+
var StorySchema = z.object({
|
|
279
|
+
id: z.string().describe("Short ID like S1, S2, S3"),
|
|
280
|
+
priority: z.number().describe("Priority level: lower = earlier"),
|
|
281
|
+
title: z.string().describe("Short title for the story"),
|
|
282
|
+
description: z.string().describe("What needs to be implemented"),
|
|
283
|
+
dependsOn: z.array(z.string()).describe("IDs of stories this depends on"),
|
|
284
|
+
retries: z.number().describe("Retry attempts if story fails (usually 2)"),
|
|
285
|
+
acceptance: z.array(z.string()).describe("Testable acceptance criteria"),
|
|
286
|
+
tests: z.array(z.string()).describe("Test commands (e.g. ['npm test'])")
|
|
287
|
+
});
|
|
288
|
+
var PrdSchema = z.object({
|
|
289
|
+
project: z.string().describe("Short project name"),
|
|
290
|
+
branchName: z.string().describe("Git branch name (kebab-case)"),
|
|
291
|
+
description: z.string().describe("One-line project description"),
|
|
292
|
+
userStories: z.array(StorySchema).describe("User stories forming a DAG")
|
|
293
|
+
});
|
|
294
|
+
var SYSTEM_PROMPT = `You are an expert software architect. Break down project goals into concrete user stories that form a dependency DAG.
|
|
295
|
+
|
|
296
|
+
IMPORTANT: Before generating a plan, USE YOUR TOOLS to explore the existing codebase:
|
|
297
|
+
1. Call file_tree to see the project structure
|
|
298
|
+
2. Call read_file on key files (package.json, README, entry points, configs)
|
|
299
|
+
3. Call grep to find relevant patterns
|
|
300
|
+
4. THEN generate a plan that fits the existing code
|
|
301
|
+
|
|
302
|
+
Rules:
|
|
303
|
+
- Each story: single focused unit of work for one AI agent
|
|
304
|
+
- Use dependsOn for dependencies; same-priority stories with no deps run IN PARALLEL
|
|
305
|
+
- Keep stories small (15-60 min of work)
|
|
306
|
+
- Include testable acceptance criteria and test commands
|
|
307
|
+
- No circular dependencies
|
|
308
|
+
- Start with foundational stories, build up
|
|
309
|
+
- retries: 2-3 for most stories
|
|
310
|
+
- IDs: S1, S2, S3...
|
|
311
|
+
- branchName: kebab-case
|
|
312
|
+
- Build on existing code, don't recreate what exists`;
|
|
313
|
+
function fixSchemaForOpenAI(schema) {
|
|
314
|
+
if (!schema || typeof schema !== "object") return schema;
|
|
315
|
+
if (schema.type === "object" && schema.properties) {
|
|
316
|
+
schema.additionalProperties = false;
|
|
317
|
+
}
|
|
318
|
+
if (schema.properties) {
|
|
319
|
+
for (const key of Object.keys(schema.properties)) {
|
|
320
|
+
fixSchemaForOpenAI(schema.properties[key]);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (schema.items) fixSchemaForOpenAI(schema.items);
|
|
324
|
+
delete schema.$schema;
|
|
325
|
+
return schema;
|
|
326
|
+
}
|
|
327
|
+
var Planner = class {
|
|
328
|
+
messages;
|
|
329
|
+
model;
|
|
330
|
+
onToken;
|
|
331
|
+
onToolCall;
|
|
332
|
+
tools;
|
|
333
|
+
constructor(options = {}) {
|
|
334
|
+
this.model = options.model ?? "gpt-5.4";
|
|
335
|
+
this.onToken = options.onToken;
|
|
336
|
+
this.onToolCall = options.onToolCall;
|
|
337
|
+
this.tools = options.cwd ? createCodebaseTools(options.cwd) : [];
|
|
338
|
+
this.messages = [{ role: "system", content: SYSTEM_PROMPT }];
|
|
339
|
+
}
|
|
340
|
+
async send(userMessage) {
|
|
341
|
+
const raw = zodToJsonSchema(PrdSchema, "prd");
|
|
342
|
+
const schema = fixSchemaForOpenAI(
|
|
343
|
+
raw.definitions?.prd ?? raw
|
|
344
|
+
);
|
|
345
|
+
const fullText = await streamCompletion({
|
|
346
|
+
model: this.model,
|
|
347
|
+
messages: this.messages,
|
|
348
|
+
task: userMessage,
|
|
349
|
+
jsonSchema: schema,
|
|
350
|
+
reasoning: { effort: "high" },
|
|
351
|
+
tools: this.tools.length > 0 ? this.tools : void 0,
|
|
352
|
+
onToken: this.onToken ?? (() => {
|
|
353
|
+
}),
|
|
354
|
+
onToolCall: this.onToolCall
|
|
355
|
+
});
|
|
356
|
+
let prd;
|
|
357
|
+
try {
|
|
358
|
+
prd = JSON.parse(fullText);
|
|
359
|
+
} catch {
|
|
360
|
+
throw new Error("Failed to parse plan output as JSON");
|
|
361
|
+
}
|
|
362
|
+
this.messages.push(
|
|
363
|
+
{ role: "user", content: userMessage },
|
|
364
|
+
{ role: "assistant", content: fullText }
|
|
365
|
+
);
|
|
366
|
+
return {
|
|
367
|
+
...prd,
|
|
368
|
+
userStories: prd.userStories.map((s) => ({
|
|
369
|
+
...s,
|
|
370
|
+
passes: false,
|
|
371
|
+
completedAt: null,
|
|
372
|
+
durationSecs: null
|
|
373
|
+
}))
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// src/core/openai-planner.ts
|
|
379
|
+
var args = process.argv.slice(2);
|
|
380
|
+
var goal = "";
|
|
381
|
+
var cwd = process.cwd();
|
|
382
|
+
for (let i = 0; i < args.length; i++) {
|
|
383
|
+
if (args[i] === "--cwd" && args[i + 1]) {
|
|
384
|
+
cwd = args[++i];
|
|
385
|
+
} else if (!goal) {
|
|
386
|
+
goal = args[i];
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (!goal) {
|
|
390
|
+
console.error("Usage: openai-planner <goal> [--cwd <path>]");
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
393
|
+
async function main() {
|
|
394
|
+
const planner = new Planner({
|
|
395
|
+
cwd,
|
|
396
|
+
onToken: () => {
|
|
397
|
+
},
|
|
398
|
+
onToolCall: (name, args2) => {
|
|
399
|
+
process.stderr.write(`[openai] tool: ${name}
|
|
400
|
+
`);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
try {
|
|
404
|
+
const prd = await planner.send(goal);
|
|
405
|
+
process.stdout.write(JSON.stringify(prd, null, 2) + "\n");
|
|
406
|
+
} catch (err) {
|
|
407
|
+
console.error(`OpenAI planner error: ${err.message}`);
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,29 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baro-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Autonomous parallel coding - plan and execute with AI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"baro": "./
|
|
7
|
+
"baro": "./bin/baro"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"dist/",
|
|
11
10
|
"bin/",
|
|
11
|
+
"dist/",
|
|
12
12
|
"scripts/postinstall.js"
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
|
-
"build": "tsup src/
|
|
15
|
+
"build": "tsup src/core/openai-planner.ts --format esm",
|
|
16
16
|
"postinstall": "node scripts/postinstall.js"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"ink": "^5.2.0",
|
|
20
|
-
"ink-spinner": "^5.0.0",
|
|
21
|
-
"react": "^18.3.1",
|
|
22
19
|
"zod": "^3.24.0",
|
|
23
20
|
"zod-to-json-schema": "^3.25.1"
|
|
24
21
|
},
|
|
25
22
|
"devDependencies": {
|
|
26
|
-
"@types/react": "^18.3.0",
|
|
27
23
|
"@types/node": "^22.0.0",
|
|
28
24
|
"tsup": "^8.0.2",
|
|
29
25
|
"typescript": "^5.3.3"
|
package/scripts/postinstall.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Postinstall script - downloads the baro
|
|
3
|
+
* Postinstall script - downloads the baro binary for the current platform.
|
|
4
4
|
* Binary is fetched from GitHub Releases.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { execSync } from "child_process"
|
|
8
7
|
import * as fs from "fs"
|
|
9
8
|
import * as path from "path"
|
|
10
9
|
import { fileURLToPath } from "url"
|
|
@@ -13,12 +12,12 @@ import * as https from "https"
|
|
|
13
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
14
13
|
const PACKAGE_ROOT = path.resolve(__dirname, "..")
|
|
15
14
|
const BIN_DIR = path.join(PACKAGE_ROOT, "bin")
|
|
16
|
-
const BINARY_NAME = "baro
|
|
15
|
+
const BINARY_NAME = "baro"
|
|
17
16
|
const REPO = "Lotus015/baro"
|
|
18
17
|
|
|
19
18
|
function getPlatformKey() {
|
|
20
|
-
const platform = process.platform
|
|
21
|
-
const arch = process.arch
|
|
19
|
+
const platform = process.platform
|
|
20
|
+
const arch = process.arch
|
|
22
21
|
|
|
23
22
|
const map = {
|
|
24
23
|
"darwin-arm64": "darwin-arm64",
|
|
@@ -29,9 +28,9 @@ function getPlatformKey() {
|
|
|
29
28
|
|
|
30
29
|
const key = `${platform}-${arch}`
|
|
31
30
|
if (!map[key]) {
|
|
32
|
-
console.warn(
|
|
33
|
-
console.warn(` You can build it manually: cargo build --release
|
|
34
|
-
process.exit(0)
|
|
31
|
+
console.warn(`Warning: no prebuilt baro binary for ${key}.`)
|
|
32
|
+
console.warn(` You can build it manually: cargo build --release -p baro-tui`)
|
|
33
|
+
process.exit(0)
|
|
35
34
|
}
|
|
36
35
|
return map[key]
|
|
37
36
|
}
|
|
@@ -64,7 +63,6 @@ async function download(url, dest) {
|
|
|
64
63
|
}
|
|
65
64
|
|
|
66
65
|
async function main() {
|
|
67
|
-
// Skip if binary already exists (e.g. local dev)
|
|
68
66
|
const binaryPath = path.join(BIN_DIR, BINARY_NAME)
|
|
69
67
|
if (fs.existsSync(binaryPath)) {
|
|
70
68
|
return
|
|
@@ -75,19 +73,17 @@ async function main() {
|
|
|
75
73
|
|
|
76
74
|
const url = `https://github.com/${REPO}/releases/download/v${version}/${BINARY_NAME}-${platformKey}`
|
|
77
75
|
|
|
78
|
-
console.log(`Downloading baro
|
|
76
|
+
console.log(`Downloading baro for ${platformKey}...`)
|
|
79
77
|
|
|
80
78
|
fs.mkdirSync(BIN_DIR, { recursive: true })
|
|
81
79
|
|
|
82
80
|
try {
|
|
83
81
|
await download(url, binaryPath)
|
|
84
82
|
fs.chmodSync(binaryPath, 0o755)
|
|
85
|
-
console.log(
|
|
83
|
+
console.log(`baro installed successfully`)
|
|
86
84
|
} catch (err) {
|
|
87
|
-
console.warn(
|
|
88
|
-
console.warn(`
|
|
89
|
-
console.warn(` Build manually: cargo build --release`)
|
|
90
|
-
// Don't fail the install
|
|
85
|
+
console.warn(`Warning: Could not download baro: ${err.message}`)
|
|
86
|
+
console.warn(` Build manually: cargo build --release -p baro-tui`)
|
|
91
87
|
}
|
|
92
88
|
}
|
|
93
89
|
|
package/dist/chunk-HWC47EK2.js
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
// src/core/cli-task.ts
|
|
2
|
-
import { spawn } from "child_process";
|
|
3
|
-
var CliTask = class {
|
|
4
|
-
id;
|
|
5
|
-
opts;
|
|
6
|
-
constructor(opts) {
|
|
7
|
-
this.id = opts.id;
|
|
8
|
-
this.opts = opts;
|
|
9
|
-
}
|
|
10
|
-
execute() {
|
|
11
|
-
return new Promise((resolve, reject) => {
|
|
12
|
-
const start = Date.now();
|
|
13
|
-
let stdout = "";
|
|
14
|
-
let stderr = "";
|
|
15
|
-
const proc = spawn(this.opts.command, this.opts.args, {
|
|
16
|
-
cwd: this.opts.cwd,
|
|
17
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
18
|
-
env: { ...process.env }
|
|
19
|
-
});
|
|
20
|
-
proc.stdout.on("data", (chunk) => {
|
|
21
|
-
const text = chunk.toString();
|
|
22
|
-
stdout += text;
|
|
23
|
-
if (this.opts.onStdout) {
|
|
24
|
-
for (const line of text.split("\n").filter(Boolean)) {
|
|
25
|
-
this.opts.onStdout(line);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
proc.stderr.on("data", (chunk) => {
|
|
30
|
-
const text = chunk.toString();
|
|
31
|
-
stderr += text;
|
|
32
|
-
if (this.opts.onStderr) {
|
|
33
|
-
for (const line of text.split("\n").filter(Boolean)) {
|
|
34
|
-
this.opts.onStderr(line);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
proc.on("error", (err) => {
|
|
39
|
-
reject(new Error(`Failed to spawn ${this.opts.command}: ${err.message}`));
|
|
40
|
-
});
|
|
41
|
-
proc.on("close", (code) => {
|
|
42
|
-
const result = {
|
|
43
|
-
stdout,
|
|
44
|
-
stderr,
|
|
45
|
-
exitCode: code ?? 1,
|
|
46
|
-
durationMs: Date.now() - start
|
|
47
|
-
};
|
|
48
|
-
if (code === 0) resolve(result);
|
|
49
|
-
else {
|
|
50
|
-
const err = new Error(`${this.opts.command} exited with code ${code}`);
|
|
51
|
-
err.result = result;
|
|
52
|
-
reject(err);
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
export {
|
|
60
|
-
CliTask
|
|
61
|
-
};
|