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.
Files changed (3) hide show
  1. package/README.md +8 -13
  2. package/package.json +1 -1
  3. 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
- # Clone and install Symphony
9
- git clone https://github.com/AugmentCo/better-symphony.git
10
- cd better-symphony
11
- bun install
8
+ # Run directly with bunx (no install needed)
9
+ bunx better-symphony
12
10
 
13
- # Optional: create a global alias so you can run `symphony` from anywhere
14
- alias symphony="bun run $(pwd)/src/cli.ts"
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**, not from inside the `better-symphony` repo. Your project should have a `workflows/` folder containing your workflow `.md` files. Symphony auto-detects `workflows/*.md` in the current working 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "better-symphony",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Better Symphony - Headless coding agent orchestrator with configurable workflows",
5
5
  "type": "module",
6
6
  "license": "MIT",
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);