better-symphony 1.0.0 → 1.0.1
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 +8 -13
- package/package.json +1 -1
- package/src/cli.ts +104 -0
package/README.md
CHANGED
|
@@ -5,18 +5,16 @@ A headless coding agent orchestrator that polls issue trackers (Linear, GitHub I
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
cd better-symphony
|
|
11
|
-
bun install
|
|
8
|
+
# Run directly with bunx (no install needed)
|
|
9
|
+
bunx better-symphony
|
|
12
10
|
|
|
13
|
-
#
|
|
14
|
-
|
|
11
|
+
# Or install globally
|
|
12
|
+
bun install -g better-symphony
|
|
15
13
|
```
|
|
16
14
|
|
|
17
15
|
## Quick Start
|
|
18
16
|
|
|
19
|
-
> **Important:** Symphony is run from **your project's directory
|
|
17
|
+
> **Important:** Symphony is run from **your project's directory**. Your project should have a `workflows/` folder containing your workflow `.md` files. Symphony auto-detects `workflows/*.md` in the current working directory.
|
|
20
18
|
|
|
21
19
|
```bash
|
|
22
20
|
cd ~/your-project # Your project with a workflows/ directory
|
|
@@ -25,14 +23,11 @@ cd ~/your-project # Your project with a workflows/ directory
|
|
|
25
23
|
export LINEAR_API_KEY=lin_api_xxxxx
|
|
26
24
|
|
|
27
25
|
# Run all workflows in workflows/
|
|
28
|
-
symphony
|
|
26
|
+
bunx better-symphony
|
|
29
27
|
|
|
30
28
|
# Or run specific workflow(s)
|
|
31
|
-
symphony -w workflows/dev.md
|
|
32
|
-
symphony -w workflows/prd.md workflows/dev.md workflows/ralph.md
|
|
33
|
-
|
|
34
|
-
# If you didn't set up the alias, use the full path:
|
|
35
|
-
bun run ~/path/to/better-symphony/src/cli.ts -w workflows/dev.md
|
|
29
|
+
bunx better-symphony -w workflows/dev.md
|
|
30
|
+
bunx better-symphony -w workflows/prd.md workflows/dev.md workflows/ralph.md
|
|
36
31
|
```
|
|
37
32
|
|
|
38
33
|
### CLI Flags
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -21,6 +21,7 @@ interface CLIOptions {
|
|
|
21
21
|
logFile?: string;
|
|
22
22
|
debug: boolean;
|
|
23
23
|
dryRun: boolean;
|
|
24
|
+
routes: boolean;
|
|
24
25
|
headless: boolean;
|
|
25
26
|
web: boolean;
|
|
26
27
|
webPort: number;
|
|
@@ -37,6 +38,7 @@ function parseArgs(): CLIOptions {
|
|
|
37
38
|
filters: [],
|
|
38
39
|
debug: false,
|
|
39
40
|
dryRun: false,
|
|
41
|
+
routes: false,
|
|
40
42
|
headless: false,
|
|
41
43
|
web: false,
|
|
42
44
|
webPort: 3000,
|
|
@@ -62,6 +64,8 @@ function parseArgs(): CLIOptions {
|
|
|
62
64
|
options.debug = true;
|
|
63
65
|
} else if (arg === "--dry-run") {
|
|
64
66
|
options.dryRun = true;
|
|
67
|
+
} else if (arg === "--routes") {
|
|
68
|
+
options.routes = true;
|
|
65
69
|
} else if (arg === "--headless") {
|
|
66
70
|
options.headless = true;
|
|
67
71
|
} else if (arg === "--web") {
|
|
@@ -130,6 +134,7 @@ Options:
|
|
|
130
134
|
-f, --filter <strings...> Filter auto-discovered workflows by filename substring
|
|
131
135
|
-l, --log <path> Log file path (appends JSON lines)
|
|
132
136
|
--dry-run Render prompts for matching issues and print them (no agent launched)
|
|
137
|
+
--routes Print workflow routing rules and exit
|
|
133
138
|
--headless Run without TUI (plain log output)
|
|
134
139
|
--web Start web dashboard (implies --headless)
|
|
135
140
|
--web-port <port> Web dashboard port (default: 3000)
|
|
@@ -148,12 +153,105 @@ Examples:
|
|
|
148
153
|
symphony --web # Run with web dashboard
|
|
149
154
|
symphony --web --web-port 8080 # Web dashboard on port 8080
|
|
150
155
|
symphony --dry-run # Preview rendered prompts
|
|
156
|
+
symphony --routes # Print routing rules for all workflows
|
|
157
|
+
symphony --routes -f dev # Print routing rules for dev workflow only
|
|
151
158
|
|
|
152
159
|
Environment Variables:
|
|
153
160
|
LINEAR_API_KEY Linear API key (required)
|
|
154
161
|
`);
|
|
155
162
|
}
|
|
156
163
|
|
|
164
|
+
// ── Routes Mode ─────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
function printRoutes(options: CLIOptions): void {
|
|
167
|
+
const { loadWorkflow, buildServiceConfig } = require("./config/loader.js");
|
|
168
|
+
|
|
169
|
+
console.log("\nWorkflow Routes");
|
|
170
|
+
console.log("===============\n");
|
|
171
|
+
|
|
172
|
+
// Collect route info for collision detection
|
|
173
|
+
const routeInfos: Array<{
|
|
174
|
+
name: string;
|
|
175
|
+
trackerKind: string;
|
|
176
|
+
scope: string;
|
|
177
|
+
requiredLabels: string[];
|
|
178
|
+
excludedLabels: string[];
|
|
179
|
+
}> = [];
|
|
180
|
+
|
|
181
|
+
for (const workflowPath of options.workflowPaths) {
|
|
182
|
+
const name = basename(workflowPath).replace(/\.md$/, "");
|
|
183
|
+
try {
|
|
184
|
+
const workflow = loadWorkflow(workflowPath);
|
|
185
|
+
const config = buildServiceConfig(workflow);
|
|
186
|
+
const t = config.tracker;
|
|
187
|
+
const scope = t.kind === "linear" ? t.project_slug : t.repo;
|
|
188
|
+
|
|
189
|
+
routeInfos.push({
|
|
190
|
+
name,
|
|
191
|
+
trackerKind: t.kind,
|
|
192
|
+
scope,
|
|
193
|
+
requiredLabels: t.required_labels,
|
|
194
|
+
excludedLabels: t.excluded_labels,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
console.log(`${name} (${t.kind})`);
|
|
198
|
+
if (t.kind === "linear") {
|
|
199
|
+
console.log(` Project: ${t.project_slug || "(none)"}`);
|
|
200
|
+
} else {
|
|
201
|
+
console.log(` Repo: ${t.repo || "(none)"}`);
|
|
202
|
+
}
|
|
203
|
+
console.log(` Mode: ${config.agent.mode}`);
|
|
204
|
+
console.log(` Active states: ${t.active_states.join(", ")}`);
|
|
205
|
+
console.log(` Required labels: ${t.required_labels.length > 0 ? t.required_labels.join(", ") : "(none)"}`);
|
|
206
|
+
console.log(` Excluded labels: ${t.excluded_labels.length > 0 ? t.excluded_labels.join(", ") : "(none)"}`);
|
|
207
|
+
console.log(` Max concurrency: ${config.agent.max_concurrent_agents}`);
|
|
208
|
+
console.log();
|
|
209
|
+
} catch (err) {
|
|
210
|
+
console.log(`${name}`);
|
|
211
|
+
console.log(` ⚠ Error loading: ${(err as Error).message}\n`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Collision detection
|
|
216
|
+
const warnings: string[] = [];
|
|
217
|
+
|
|
218
|
+
// Check for label overlaps (scoped by tracker kind + project/repo)
|
|
219
|
+
const labelMap = new Map<string, string[]>();
|
|
220
|
+
for (const route of routeInfos) {
|
|
221
|
+
for (const label of route.requiredLabels) {
|
|
222
|
+
const key = `${route.trackerKind}|${route.scope}|${label}`;
|
|
223
|
+
if (!labelMap.has(key)) labelMap.set(key, []);
|
|
224
|
+
labelMap.get(key)!.push(route.name);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
for (const [key, names] of labelMap) {
|
|
228
|
+
if (names.length > 1) {
|
|
229
|
+
const label = key.split("|")[2];
|
|
230
|
+
warnings.push(
|
|
231
|
+
`⚠ Label "${label}" is required by multiple workflows: ${names.join(", ")}\n Both workflows will pick up the same issues — this is likely unintentional.`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check for workflows with no label filters
|
|
237
|
+
for (const route of routeInfos) {
|
|
238
|
+
if (route.requiredLabels.length === 0 && route.excludedLabels.length === 0) {
|
|
239
|
+
warnings.push(
|
|
240
|
+
`⚠ Workflow "${route.name}" has no label filters — it will match all issues in active states.`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (warnings.length > 0) {
|
|
246
|
+
console.log("Warnings");
|
|
247
|
+
console.log("========\n");
|
|
248
|
+
for (const w of warnings) {
|
|
249
|
+
console.log(w);
|
|
250
|
+
console.log();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
157
255
|
// ── Main ────────────────────────────────────────────────────────
|
|
158
256
|
|
|
159
257
|
async function main(): Promise<void> {
|
|
@@ -167,6 +265,12 @@ async function main(): Promise<void> {
|
|
|
167
265
|
}
|
|
168
266
|
}
|
|
169
267
|
|
|
268
|
+
// Routes mode: print routing summary and exit
|
|
269
|
+
if (options.routes) {
|
|
270
|
+
printRoutes(options);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
170
274
|
// Dry run mode always runs headless (only supports single workflow)
|
|
171
275
|
if (options.dryRun) {
|
|
172
276
|
await runDryRun(options);
|