opencode-swarm-plugin 0.36.0 → 0.37.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.
Files changed (54) hide show
  1. package/.hive/issues.jsonl +16 -4
  2. package/.hive/memories.jsonl +274 -1
  3. package/.turbo/turbo-build.log +4 -4
  4. package/.turbo/turbo-test.log +318 -318
  5. package/CHANGELOG.md +113 -0
  6. package/bin/swarm.test.ts +106 -0
  7. package/bin/swarm.ts +413 -179
  8. package/dist/compaction-hook.d.ts +54 -4
  9. package/dist/compaction-hook.d.ts.map +1 -1
  10. package/dist/eval-capture.d.ts +122 -17
  11. package/dist/eval-capture.d.ts.map +1 -1
  12. package/dist/index.d.ts +1 -7
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1278 -619
  15. package/dist/planning-guardrails.d.ts +121 -0
  16. package/dist/planning-guardrails.d.ts.map +1 -1
  17. package/dist/plugin.d.ts +9 -9
  18. package/dist/plugin.d.ts.map +1 -1
  19. package/dist/plugin.js +1283 -329
  20. package/dist/schemas/task.d.ts +0 -1
  21. package/dist/schemas/task.d.ts.map +1 -1
  22. package/dist/swarm-decompose.d.ts +0 -8
  23. package/dist/swarm-decompose.d.ts.map +1 -1
  24. package/dist/swarm-orchestrate.d.ts.map +1 -1
  25. package/dist/swarm-prompts.d.ts +0 -4
  26. package/dist/swarm-prompts.d.ts.map +1 -1
  27. package/dist/swarm-review.d.ts.map +1 -1
  28. package/dist/swarm.d.ts +0 -6
  29. package/dist/swarm.d.ts.map +1 -1
  30. package/evals/README.md +38 -0
  31. package/evals/coordinator-session.eval.ts +154 -0
  32. package/evals/fixtures/coordinator-sessions.ts +328 -0
  33. package/evals/lib/data-loader.ts +69 -0
  34. package/evals/scorers/coordinator-discipline.evalite-test.ts +536 -0
  35. package/evals/scorers/coordinator-discipline.ts +315 -0
  36. package/evals/scorers/index.ts +12 -0
  37. package/examples/plugin-wrapper-template.ts +303 -4
  38. package/package.json +2 -2
  39. package/src/compaction-hook.test.ts +8 -1
  40. package/src/compaction-hook.ts +31 -21
  41. package/src/eval-capture.test.ts +390 -0
  42. package/src/eval-capture.ts +163 -4
  43. package/src/hive.integration.test.ts +148 -0
  44. package/src/hive.ts +89 -0
  45. package/src/index.ts +68 -1
  46. package/src/planning-guardrails.test.ts +387 -2
  47. package/src/planning-guardrails.ts +289 -0
  48. package/src/plugin.ts +10 -10
  49. package/src/swarm-decompose.test.ts +195 -0
  50. package/src/swarm-decompose.ts +72 -1
  51. package/src/swarm-orchestrate.ts +44 -0
  52. package/src/swarm-prompts.ts +20 -0
  53. package/src/swarm-review.integration.test.ts +24 -29
  54. package/src/swarm-review.ts +41 -0
package/bin/swarm.ts CHANGED
@@ -983,7 +983,7 @@ function getPluginWrapper(): string {
983
983
  `[swarm] Could not read plugin template from ${templatePath}, using minimal wrapper`,
984
984
  );
985
985
  return `// Minimal fallback - install opencode-swarm-plugin globally for full functionality
986
- import { SwarmPlugin } from "opencode-swarm-plugin"
986
+ import SwarmPlugin from "opencode-swarm-plugin"
987
987
  export default SwarmPlugin
988
988
  `;
989
989
  }
@@ -1709,7 +1709,7 @@ async function doctor() {
1709
1709
  if (updateInfo) showUpdateNotification(updateInfo);
1710
1710
  }
1711
1711
 
1712
- async function setup() {
1712
+ async function setup(forceReinstall = false, nonInteractive = false) {
1713
1713
  console.clear();
1714
1714
  console.log(yellow(BANNER));
1715
1715
  console.log(getDecoratedBee());
@@ -1783,7 +1783,7 @@ async function setup() {
1783
1783
  legacyWorkerPath,
1784
1784
  ].filter((f) => existsSync(f));
1785
1785
 
1786
- if (existingFiles.length > 0) {
1786
+ if (existingFiles.length > 0 && !forceReinstall) {
1787
1787
  p.log.success("Swarm is already configured!");
1788
1788
  p.log.message(dim(" Found " + existingFiles.length + "/5 config files"));
1789
1789
 
@@ -1903,7 +1903,8 @@ async function setup() {
1903
1903
  p.log.step("Missing " + requiredMissing.length + " required dependencies");
1904
1904
 
1905
1905
  for (const { dep } of requiredMissing) {
1906
- const shouldInstall = await p.confirm({
1906
+ // In non-interactive mode, auto-install required deps
1907
+ const shouldInstall = nonInteractive ? true : await p.confirm({
1907
1908
  message: "Install " + dep.name + "? (" + dep.description + ")",
1908
1909
  initialValue: true,
1909
1910
  });
@@ -1931,8 +1932,8 @@ async function setup() {
1931
1932
  }
1932
1933
  }
1933
1934
 
1934
- // Only prompt for optional deps if there are missing ones
1935
- if (optionalMissing.length > 0) {
1935
+ // Only prompt for optional deps if there are missing ones (skip in non-interactive mode)
1936
+ if (optionalMissing.length > 0 && !nonInteractive) {
1936
1937
  const installable = optionalMissing.filter(
1937
1938
  (r) => r.dep.installType !== "manual",
1938
1939
  );
@@ -2099,135 +2100,157 @@ async function setup() {
2099
2100
  p.log.message(dim(' No OpenCode config found (skipping MCP check)'));
2100
2101
  }
2101
2102
 
2102
- // Model selection
2103
- p.log.step("Configuring swarm agents...");
2104
- p.log.message(dim(" Coordinator handles orchestration, worker executes tasks"));
2103
+ // Model defaults: opus for coordinator, sonnet for worker, haiku for lite
2104
+ const DEFAULT_COORDINATOR = "anthropic/claude-opus-4-5";
2105
+ const DEFAULT_WORKER = "anthropic/claude-sonnet-4-5";
2106
+ const DEFAULT_LITE = "anthropic/claude-haiku-4-5";
2107
+
2108
+ // Model selection (skip if non-interactive)
2109
+ let coordinatorModel: string;
2110
+ let workerModel: string;
2111
+ let liteModel: string;
2112
+
2113
+ if (nonInteractive) {
2114
+ coordinatorModel = DEFAULT_COORDINATOR;
2115
+ workerModel = DEFAULT_WORKER;
2116
+ liteModel = DEFAULT_LITE;
2117
+ p.log.step("Using default models:");
2118
+ p.log.message(dim(` Coordinator: ${coordinatorModel}`));
2119
+ p.log.message(dim(` Worker: ${workerModel}`));
2120
+ p.log.message(dim(` Lite: ${liteModel}`));
2121
+ } else {
2122
+ p.log.step("Configuring swarm agents...");
2123
+ p.log.message(dim(" Coordinator handles orchestration, worker executes tasks"));
2105
2124
 
2106
- const coordinatorModel = await p.select({
2107
- message: "Select coordinator model (for orchestration/planning):",
2108
- options: [
2109
- {
2110
- value: "anthropic/claude-sonnet-4-5",
2111
- label: "Claude Sonnet 4.5",
2112
- hint: "Best balance of speed and capability (recommended)",
2113
- },
2114
- {
2115
- value: "anthropic/claude-haiku-4-5",
2116
- label: "Claude Haiku 4.5",
2117
- hint: "Fast and cost-effective",
2118
- },
2119
- {
2120
- value: "anthropic/claude-opus-4-5",
2121
- label: "Claude Opus 4.5",
2122
- hint: "Most capable, slower",
2123
- },
2124
- {
2125
- value: "openai/gpt-4o",
2126
- label: "GPT-4o",
2127
- hint: "Fast, good for most tasks",
2128
- },
2129
- {
2130
- value: "openai/gpt-4-turbo",
2131
- label: "GPT-4 Turbo",
2132
- hint: "Powerful, more expensive",
2133
- },
2134
- {
2135
- value: "google/gemini-2.0-flash",
2136
- label: "Gemini 2.0 Flash",
2137
- hint: "Fast and capable",
2138
- },
2139
- {
2140
- value: "google/gemini-1.5-pro",
2141
- label: "Gemini 1.5 Pro",
2142
- hint: "More capable",
2143
- },
2144
- ],
2145
- initialValue: "anthropic/claude-sonnet-4-5",
2146
- });
2125
+ const selectedCoordinator = await p.select({
2126
+ message: "Select coordinator model (for orchestration/planning):",
2127
+ options: [
2128
+ {
2129
+ value: "anthropic/claude-opus-4-5",
2130
+ label: "Claude Opus 4.5",
2131
+ hint: "Most capable, best for complex orchestration (recommended)",
2132
+ },
2133
+ {
2134
+ value: "anthropic/claude-sonnet-4-5",
2135
+ label: "Claude Sonnet 4.5",
2136
+ hint: "Good balance of speed and capability",
2137
+ },
2138
+ {
2139
+ value: "anthropic/claude-haiku-4-5",
2140
+ label: "Claude Haiku 4.5",
2141
+ hint: "Fast and cost-effective",
2142
+ },
2143
+ {
2144
+ value: "openai/gpt-4o",
2145
+ label: "GPT-4o",
2146
+ hint: "Fast, good for most tasks",
2147
+ },
2148
+ {
2149
+ value: "openai/gpt-4-turbo",
2150
+ label: "GPT-4 Turbo",
2151
+ hint: "Powerful, more expensive",
2152
+ },
2153
+ {
2154
+ value: "google/gemini-2.0-flash",
2155
+ label: "Gemini 2.0 Flash",
2156
+ hint: "Fast and capable",
2157
+ },
2158
+ {
2159
+ value: "google/gemini-1.5-pro",
2160
+ label: "Gemini 1.5 Pro",
2161
+ hint: "More capable",
2162
+ },
2163
+ ],
2164
+ initialValue: DEFAULT_COORDINATOR,
2165
+ });
2147
2166
 
2148
- if (p.isCancel(coordinatorModel)) {
2149
- p.cancel("Setup cancelled");
2150
- process.exit(0);
2151
- }
2167
+ if (p.isCancel(selectedCoordinator)) {
2168
+ p.cancel("Setup cancelled");
2169
+ process.exit(0);
2170
+ }
2171
+ coordinatorModel = selectedCoordinator;
2152
2172
 
2153
- const workerModel = await p.select({
2154
- message: "Select worker model (for task execution):",
2155
- options: [
2156
- {
2157
- value: "anthropic/claude-haiku-4-5",
2158
- label: "Claude Haiku 4.5",
2159
- hint: "Fast and cost-effective (recommended)",
2160
- },
2161
- {
2162
- value: "anthropic/claude-sonnet-4-5",
2163
- label: "Claude Sonnet 4.5",
2164
- hint: "Best balance of speed and capability",
2165
- },
2166
- {
2167
- value: "anthropic/claude-opus-4-5",
2168
- label: "Claude Opus 4.5",
2169
- hint: "Most capable, slower",
2170
- },
2171
- {
2172
- value: "openai/gpt-4o",
2173
- label: "GPT-4o",
2174
- hint: "Fast, good for most tasks",
2175
- },
2176
- {
2177
- value: "openai/gpt-4-turbo",
2178
- label: "GPT-4 Turbo",
2179
- hint: "Powerful, more expensive",
2180
- },
2181
- {
2182
- value: "google/gemini-2.0-flash",
2183
- label: "Gemini 2.0 Flash",
2184
- hint: "Fast and capable",
2185
- },
2186
- {
2187
- value: "google/gemini-1.5-pro",
2188
- label: "Gemini 1.5 Pro",
2189
- hint: "More capable",
2190
- },
2191
- ],
2192
- initialValue: "anthropic/claude-haiku-4-5",
2193
- });
2173
+ const selectedWorker = await p.select({
2174
+ message: "Select worker model (for task execution):",
2175
+ options: [
2176
+ {
2177
+ value: "anthropic/claude-sonnet-4-5",
2178
+ label: "Claude Sonnet 4.5",
2179
+ hint: "Best balance of speed and capability (recommended)",
2180
+ },
2181
+ {
2182
+ value: "anthropic/claude-haiku-4-5",
2183
+ label: "Claude Haiku 4.5",
2184
+ hint: "Fast and cost-effective",
2185
+ },
2186
+ {
2187
+ value: "anthropic/claude-opus-4-5",
2188
+ label: "Claude Opus 4.5",
2189
+ hint: "Most capable, slower",
2190
+ },
2191
+ {
2192
+ value: "openai/gpt-4o",
2193
+ label: "GPT-4o",
2194
+ hint: "Fast, good for most tasks",
2195
+ },
2196
+ {
2197
+ value: "openai/gpt-4-turbo",
2198
+ label: "GPT-4 Turbo",
2199
+ hint: "Powerful, more expensive",
2200
+ },
2201
+ {
2202
+ value: "google/gemini-2.0-flash",
2203
+ label: "Gemini 2.0 Flash",
2204
+ hint: "Fast and capable",
2205
+ },
2206
+ {
2207
+ value: "google/gemini-1.5-pro",
2208
+ label: "Gemini 1.5 Pro",
2209
+ hint: "More capable",
2210
+ },
2211
+ ],
2212
+ initialValue: DEFAULT_WORKER,
2213
+ });
2194
2214
 
2195
- if (p.isCancel(workerModel)) {
2196
- p.cancel("Setup cancelled");
2197
- process.exit(0);
2198
- }
2215
+ if (p.isCancel(selectedWorker)) {
2216
+ p.cancel("Setup cancelled");
2217
+ process.exit(0);
2218
+ }
2219
+ workerModel = selectedWorker;
2199
2220
 
2200
- // Lite model selection for simple tasks (docs, tests)
2201
- const liteModel = await p.select({
2202
- message: "Select lite model (for docs, tests, simple edits):",
2203
- options: [
2204
- {
2205
- value: "anthropic/claude-haiku-4-5",
2206
- label: "Claude Haiku 4.5",
2207
- hint: "Fast and cost-effective (recommended)",
2208
- },
2209
- {
2210
- value: "anthropic/claude-sonnet-4-5",
2211
- label: "Claude Sonnet 4.5",
2212
- hint: "More capable, slower",
2213
- },
2214
- {
2215
- value: "openai/gpt-4o-mini",
2216
- label: "GPT-4o Mini",
2217
- hint: "Fast and cheap",
2218
- },
2219
- {
2220
- value: "google/gemini-2.0-flash",
2221
- label: "Gemini 2.0 Flash",
2222
- hint: "Fast and capable",
2223
- },
2224
- ],
2225
- initialValue: "anthropic/claude-haiku-4-5",
2226
- });
2221
+ // Lite model selection for simple tasks (docs, tests)
2222
+ const selectedLite = await p.select({
2223
+ message: "Select lite model (for docs, tests, simple edits):",
2224
+ options: [
2225
+ {
2226
+ value: "anthropic/claude-haiku-4-5",
2227
+ label: "Claude Haiku 4.5",
2228
+ hint: "Fast and cost-effective (recommended)",
2229
+ },
2230
+ {
2231
+ value: "anthropic/claude-sonnet-4-5",
2232
+ label: "Claude Sonnet 4.5",
2233
+ hint: "More capable, slower",
2234
+ },
2235
+ {
2236
+ value: "openai/gpt-4o-mini",
2237
+ label: "GPT-4o Mini",
2238
+ hint: "Fast and cheap",
2239
+ },
2240
+ {
2241
+ value: "google/gemini-2.0-flash",
2242
+ label: "Gemini 2.0 Flash",
2243
+ hint: "Fast and capable",
2244
+ },
2245
+ ],
2246
+ initialValue: DEFAULT_LITE,
2247
+ });
2227
2248
 
2228
- if (p.isCancel(liteModel)) {
2229
- p.cancel("Setup cancelled");
2230
- process.exit(0);
2249
+ if (p.isCancel(selectedLite)) {
2250
+ p.cancel("Setup cancelled");
2251
+ process.exit(0);
2252
+ }
2253
+ liteModel = selectedLite;
2231
2254
  }
2232
2255
 
2233
2256
  p.log.success("Selected models:");
@@ -2249,7 +2272,8 @@ async function setup() {
2249
2272
 
2250
2273
  // Write plugin and command files
2251
2274
  p.log.step("Writing configuration files...");
2252
- stats[writeFileWithStatus(pluginPath, getPluginWrapper(), "Plugin")]++;
2275
+ const pluginContent = getPluginWrapper().replace(/__SWARM_LITE_MODEL__/g, liteModel);
2276
+ stats[writeFileWithStatus(pluginPath, pluginContent, "Plugin")]++;
2253
2277
  stats[writeFileWithStatus(commandPath, SWARM_COMMAND, "Command")]++;
2254
2278
 
2255
2279
  // Write nested agent files (swarm/planner.md, swarm/worker.md, swarm/researcher.md)
@@ -2288,21 +2312,8 @@ async function setup() {
2288
2312
  );
2289
2313
 
2290
2314
  if (missingBundled.length > 0 || managedBundled.length > 0) {
2291
- const shouldSync = await p.confirm({
2292
- message:
2293
- "Sync bundled skills into your global skills directory? " +
2294
- (missingBundled.length > 0
2295
- ? `(${missingBundled.length} missing)`
2296
- : "(update managed skills)"),
2297
- initialValue: isReinstall || missingBundled.length > 0,
2298
- });
2299
-
2300
- if (p.isCancel(shouldSync)) {
2301
- p.cancel("Setup cancelled");
2302
- process.exit(0);
2303
- }
2304
-
2305
- if (shouldSync) {
2315
+ // Always sync bundled skills - no prompt needed
2316
+ {
2306
2317
  const syncSpinner = p.spinner();
2307
2318
  syncSpinner.start("Syncing bundled skills...");
2308
2319
  try {
@@ -2339,15 +2350,10 @@ async function setup() {
2339
2350
  }
2340
2351
  }
2341
2352
 
2342
- // Offer to update AGENTS.md with skill awareness
2353
+ // Always update AGENTS.md with skill awareness - no prompt needed
2343
2354
  const agentsPath = join(configDir, "AGENTS.md");
2344
2355
  if (existsSync(agentsPath)) {
2345
- const updateAgents = await p.confirm({
2346
- message: "Update AGENTS.md with skill awareness?",
2347
- initialValue: true,
2348
- });
2349
-
2350
- if (!p.isCancel(updateAgents) && updateAgents) {
2356
+ {
2351
2357
  const s = p.spinner();
2352
2358
  s.start("Updating AGENTS.md...");
2353
2359
 
@@ -2708,12 +2714,15 @@ async function help() {
2708
2714
  console.log(magenta(" " + getRandomMessage()));
2709
2715
  console.log(`
2710
2716
  ${cyan("Commands:")}
2711
- swarm setup Interactive installer - checks and installs dependencies
2712
- swarm doctor Health check - shows status of all dependencies
2717
+ swarm setup Interactive installer - checks and installs dependencies
2718
+ --reinstall, -r Skip prompt, go straight to reinstall
2719
+ --yes, -y Non-interactive with defaults (opus/sonnet/haiku)
2720
+ swarm doctor Health check - shows status of all dependencies
2713
2721
  swarm init Initialize beads in current project
2714
2722
  swarm config Show paths to generated config files
2715
2723
  swarm agents Update AGENTS.md with skill awareness
2716
2724
  swarm migrate Migrate PGlite database to libSQL
2725
+ swarm cells List or get cells from database (replaces 'swarm tool hive_query')
2717
2726
  swarm log View swarm logs with filtering
2718
2727
  swarm update Update to latest version
2719
2728
  swarm version Show version and banner
@@ -2725,6 +2734,14 @@ ${cyan("Tool Execution:")}
2725
2734
  swarm tool <name> Execute tool with no args
2726
2735
  swarm tool <name> --json '<args>' Execute tool with JSON args
2727
2736
 
2737
+ ${cyan("Cell Management:")}
2738
+ swarm cells List cells from database (default: 20 most recent)
2739
+ swarm cells <id> Get single cell by ID or partial hash
2740
+ swarm cells --status <status> Filter by status (open, in_progress, closed, blocked)
2741
+ swarm cells --type <type> Filter by type (task, bug, feature, epic, chore)
2742
+ swarm cells --ready Show next ready (unblocked) cell
2743
+ swarm cells --json Raw JSON output (array, no wrapper)
2744
+
2728
2745
  ${cyan("Log Viewing:")}
2729
2746
  swarm log Tail recent logs (last 50 lines)
2730
2747
  swarm log <module> Filter by module (e.g., compaction)
@@ -3101,17 +3118,43 @@ interface LogLine {
3101
3118
  time: string;
3102
3119
  module: string;
3103
3120
  msg: string;
3121
+ data?: Record<string, unknown>; // Extra structured data
3104
3122
  }
3105
3123
 
3106
- function parseLogLine(line: string): LogLine | null {
3124
+ function parseLogLine(line: string, sourceFile?: string): LogLine | null {
3107
3125
  try {
3108
3126
  const parsed = JSON.parse(line);
3109
- if (typeof parsed.level === "number" && parsed.time && parsed.msg) {
3127
+ if (parsed.time && parsed.msg) {
3128
+ // Handle both pino format (level: number) and plugin wrapper format (level: string)
3129
+ let level: number;
3130
+ if (typeof parsed.level === "number") {
3131
+ level = parsed.level;
3132
+ } else if (typeof parsed.level === "string") {
3133
+ level = levelNameToNumber(parsed.level);
3134
+ } else {
3135
+ level = 30; // default to info
3136
+ }
3137
+
3138
+ // Derive module from: explicit field, or source filename (e.g., "compaction.log" -> "compaction")
3139
+ let module = parsed.module;
3140
+ if (!module && sourceFile) {
3141
+ // Extract module from filename: "compaction.log" -> "compaction", "swarm.1log" -> "swarm"
3142
+ const match = sourceFile.match(/([^/]+?)(?:\.\d+)?\.?log$/);
3143
+ if (match) {
3144
+ module = match[1];
3145
+ }
3146
+ }
3147
+
3148
+ // Extract extra data (everything except core fields)
3149
+ const { level: _l, time: _t, module: _m, msg: _msg, ...extraData } = parsed;
3150
+ const hasExtraData = Object.keys(extraData).length > 0;
3151
+
3110
3152
  return {
3111
- level: parsed.level,
3153
+ level,
3112
3154
  time: parsed.time,
3113
- module: parsed.module || "unknown",
3155
+ module: module || "unknown",
3114
3156
  msg: parsed.msg,
3157
+ data: hasExtraData ? extraData : undefined,
3115
3158
  };
3116
3159
  }
3117
3160
  } catch {
@@ -3164,36 +3207,218 @@ function parseDuration(duration: string): number | null {
3164
3207
  return value * multipliers[unit];
3165
3208
  }
3166
3209
 
3167
- function formatLogLine(log: LogLine, useColor = true): string {
3210
+ function formatLogLine(log: LogLine, useColor = true, verbose = false): string {
3168
3211
  const timestamp = new Date(log.time).toLocaleTimeString();
3169
3212
  const levelName = levelToName(log.level);
3170
3213
  const module = log.module.padEnd(12);
3171
3214
  const levelStr = useColor ? levelToColor(log.level)(levelName) : levelName;
3172
3215
 
3173
- return `${timestamp} ${levelStr} ${module} ${log.msg}`;
3216
+ let output = `${timestamp} ${levelStr} ${module} ${log.msg}`;
3217
+
3218
+ // In verbose mode, pretty print the structured data
3219
+ if (verbose && log.data) {
3220
+ output += `\n${dim(JSON.stringify(log.data, null, 2))}`;
3221
+ }
3222
+
3223
+ return output;
3224
+ }
3225
+
3226
+ interface LogEntry {
3227
+ line: string;
3228
+ file: string;
3174
3229
  }
3175
3230
 
3176
- function readLogFiles(dir: string): string[] {
3231
+ function readLogFiles(dir: string): LogEntry[] {
3177
3232
  if (!existsSync(dir)) return [];
3178
3233
 
3179
3234
  const allFiles = readdirSync(dir);
3235
+ // Match both pino-roll format (*.1log, *.2log) AND plain *.log files
3180
3236
  const logFiles = allFiles
3181
- .filter((f: string) => /\.\d+log$/.test(f))
3237
+ .filter((f: string) => /\.\d+log$/.test(f) || /\.log$/.test(f))
3182
3238
  .sort()
3183
3239
  .map((f: string) => join(dir, f));
3184
3240
 
3185
- const lines: string[] = [];
3241
+ const entries: LogEntry[] = [];
3186
3242
  for (const file of logFiles) {
3187
3243
  try {
3188
3244
  const content = readFileSync(file, "utf-8");
3189
3245
  const fileLines = content.split("\n").filter((line: string) => line.trim());
3190
- lines.push(...fileLines);
3246
+ for (const line of fileLines) {
3247
+ entries.push({ line, file });
3248
+ }
3191
3249
  } catch {
3192
3250
  // Skip unreadable files
3193
3251
  }
3194
3252
  }
3195
3253
 
3196
- return lines;
3254
+ return entries;
3255
+ }
3256
+
3257
+ /**
3258
+ * Format cells as table output
3259
+ */
3260
+ function formatCellsTable(cells: Array<{
3261
+ id: string;
3262
+ title: string;
3263
+ status: string;
3264
+ priority: number;
3265
+ }>): string {
3266
+ if (cells.length === 0) {
3267
+ return "No cells found";
3268
+ }
3269
+
3270
+ const rows = cells.map(c => ({
3271
+ id: c.id,
3272
+ title: c.title.length > 50 ? c.title.slice(0, 47) + "..." : c.title,
3273
+ status: c.status,
3274
+ priority: String(c.priority),
3275
+ }));
3276
+
3277
+ // Calculate column widths
3278
+ const widths = {
3279
+ id: Math.max(2, ...rows.map(r => r.id.length)),
3280
+ title: Math.max(5, ...rows.map(r => r.title.length)),
3281
+ status: Math.max(6, ...rows.map(r => r.status.length)),
3282
+ priority: Math.max(8, ...rows.map(r => r.priority.length)),
3283
+ };
3284
+
3285
+ // Build header
3286
+ const header = [
3287
+ "ID".padEnd(widths.id),
3288
+ "TITLE".padEnd(widths.title),
3289
+ "STATUS".padEnd(widths.status),
3290
+ "PRIORITY".padEnd(widths.priority),
3291
+ ].join(" ");
3292
+
3293
+ const separator = "-".repeat(header.length);
3294
+
3295
+ // Build rows
3296
+ const bodyRows = rows.map(r =>
3297
+ [
3298
+ r.id.padEnd(widths.id),
3299
+ r.title.padEnd(widths.title),
3300
+ r.status.padEnd(widths.status),
3301
+ r.priority.padEnd(widths.priority),
3302
+ ].join(" ")
3303
+ );
3304
+
3305
+ return [header, separator, ...bodyRows].join("\n");
3306
+ }
3307
+
3308
+ /**
3309
+ * List or get cells from database
3310
+ */
3311
+ async function cells() {
3312
+ const args = process.argv.slice(3);
3313
+
3314
+ // Parse arguments
3315
+ let cellId: string | null = null;
3316
+ let statusFilter: string | null = null;
3317
+ let typeFilter: string | null = null;
3318
+ let readyOnly = false;
3319
+ let jsonOutput = false;
3320
+
3321
+ for (let i = 0; i < args.length; i++) {
3322
+ const arg = args[i];
3323
+
3324
+ if (arg === "--status" && i + 1 < args.length) {
3325
+ statusFilter = args[++i];
3326
+ if (!["open", "in_progress", "closed", "blocked"].includes(statusFilter)) {
3327
+ p.log.error(`Invalid status: ${statusFilter}`);
3328
+ p.log.message(dim(" Valid statuses: open, in_progress, closed, blocked"));
3329
+ process.exit(1);
3330
+ }
3331
+ } else if (arg === "--type" && i + 1 < args.length) {
3332
+ typeFilter = args[++i];
3333
+ if (!["task", "bug", "feature", "epic", "chore"].includes(typeFilter)) {
3334
+ p.log.error(`Invalid type: ${typeFilter}`);
3335
+ p.log.message(dim(" Valid types: task, bug, feature, epic, chore"));
3336
+ process.exit(1);
3337
+ }
3338
+ } else if (arg === "--ready") {
3339
+ readyOnly = true;
3340
+ } else if (arg === "--json") {
3341
+ jsonOutput = true;
3342
+ } else if (!arg.startsWith("--") && !arg.startsWith("-")) {
3343
+ // Positional arg = cell ID (full or partial)
3344
+ cellId = arg;
3345
+ }
3346
+ }
3347
+
3348
+ // Get adapter using swarm-mail
3349
+ const projectPath = process.cwd();
3350
+ const { getSwarmMailLibSQL, createHiveAdapter, resolvePartialId } = await import("swarm-mail");
3351
+
3352
+ try {
3353
+ const swarmMail = await getSwarmMailLibSQL(projectPath);
3354
+ const db = await swarmMail.getDatabase();
3355
+ const adapter = createHiveAdapter(db, projectPath);
3356
+
3357
+ // Run migrations to ensure schema exists
3358
+ await adapter.runMigrations();
3359
+
3360
+ // If cell ID provided, get single cell
3361
+ if (cellId) {
3362
+ // Resolve partial ID to full ID
3363
+ const fullId = await resolvePartialId(adapter, projectPath, cellId) || cellId;
3364
+ const cell = await adapter.getCell(projectPath, fullId);
3365
+
3366
+ if (!cell) {
3367
+ p.log.error(`Cell not found: ${cellId}`);
3368
+ process.exit(1);
3369
+ }
3370
+
3371
+ if (jsonOutput) {
3372
+ console.log(JSON.stringify([cell], null, 2));
3373
+ } else {
3374
+ const table = formatCellsTable([{
3375
+ id: cell.id,
3376
+ title: cell.title,
3377
+ status: cell.status,
3378
+ priority: cell.priority,
3379
+ }]);
3380
+ console.log(table);
3381
+ }
3382
+ return;
3383
+ }
3384
+
3385
+ // Otherwise query cells
3386
+ let cells: Array<{ id: string; title: string; status: string; priority: number }>;
3387
+
3388
+ if (readyOnly) {
3389
+ const readyCell = await adapter.getNextReadyCell(projectPath);
3390
+ cells = readyCell ? [{
3391
+ id: readyCell.id,
3392
+ title: readyCell.title,
3393
+ status: readyCell.status,
3394
+ priority: readyCell.priority,
3395
+ }] : [];
3396
+ } else {
3397
+ const queriedCells = await adapter.queryCells(projectPath, {
3398
+ status: statusFilter as any || undefined,
3399
+ type: typeFilter as any || undefined,
3400
+ limit: 20,
3401
+ });
3402
+
3403
+ cells = queriedCells.map(c => ({
3404
+ id: c.id,
3405
+ title: c.title,
3406
+ status: c.status,
3407
+ priority: c.priority,
3408
+ }));
3409
+ }
3410
+
3411
+ if (jsonOutput) {
3412
+ console.log(JSON.stringify(cells, null, 2));
3413
+ } else {
3414
+ const table = formatCellsTable(cells);
3415
+ console.log(table);
3416
+ }
3417
+ } catch (error) {
3418
+ const message = error instanceof Error ? error.message : String(error);
3419
+ p.log.error(`Failed to query cells: ${message}`);
3420
+ process.exit(1);
3421
+ }
3197
3422
  }
3198
3423
 
3199
3424
  async function logs() {
@@ -3207,6 +3432,7 @@ async function logs() {
3207
3432
  let limit = 50;
3208
3433
  let watchMode = false;
3209
3434
  let pollInterval = 1000; // 1 second default
3435
+ let verbose = false;
3210
3436
 
3211
3437
  for (let i = 0; i < args.length; i++) {
3212
3438
  const arg = args[i];
@@ -3231,6 +3457,8 @@ async function logs() {
3231
3457
  }
3232
3458
  } else if (arg === "--watch" || arg === "-w") {
3233
3459
  watchMode = true;
3460
+ } else if (arg === "--verbose" || arg === "-v") {
3461
+ verbose = true;
3234
3462
  } else if (arg === "--interval" && i + 1 < args.length) {
3235
3463
  pollInterval = parseInt(args[++i], 10);
3236
3464
  if (isNaN(pollInterval) || pollInterval < 100) {
@@ -3290,7 +3518,7 @@ async function logs() {
3290
3518
  // Initialize positions from current file sizes
3291
3519
  const initializePositions = () => {
3292
3520
  if (!existsSync(logsDir)) return;
3293
- const files = readdirSync(logsDir).filter((f: string) => /\.\d+log$/.test(f));
3521
+ const files = readdirSync(logsDir).filter((f: string) => /\.\d+log$/.test(f) || /\.log$/.test(f));
3294
3522
  for (const file of files) {
3295
3523
  const filePath = join(logsDir, file);
3296
3524
  try {
@@ -3327,14 +3555,14 @@ async function logs() {
3327
3555
  };
3328
3556
 
3329
3557
  // Print initial logs (last N lines)
3330
- const rawLines = readLogFiles(logsDir);
3331
- let logs: LogLine[] = rawLines
3332
- .map(parseLogLine)
3558
+ const rawEntries = readLogFiles(logsDir);
3559
+ let logs: LogLine[] = rawEntries
3560
+ .map(entry => parseLogLine(entry.line, entry.file))
3333
3561
  .filter((log): log is LogLine => log !== null);
3334
3562
  logs = filterLogs(logs).slice(-limit);
3335
3563
 
3336
3564
  for (const log of logs) {
3337
- console.log(formatLogLine(log));
3565
+ console.log(formatLogLine(log, true, verbose));
3338
3566
  }
3339
3567
 
3340
3568
  // Initialize positions after printing initial logs
@@ -3344,18 +3572,18 @@ async function logs() {
3344
3572
  const pollForNewLogs = () => {
3345
3573
  if (!existsSync(logsDir)) return;
3346
3574
 
3347
- const files = readdirSync(logsDir).filter((f: string) => /\.\d+log$/.test(f));
3575
+ const files = readdirSync(logsDir).filter((f: string) => /\.\d+log$/.test(f) || /\.log$/.test(f));
3348
3576
 
3349
3577
  for (const file of files) {
3350
3578
  const filePath = join(logsDir, file);
3351
3579
  const newLines = readNewLines(filePath);
3352
3580
 
3353
3581
  for (const line of newLines) {
3354
- const parsed = parseLogLine(line);
3582
+ const parsed = parseLogLine(line, filePath);
3355
3583
  if (parsed) {
3356
3584
  const filtered = filterLogs([parsed]);
3357
3585
  if (filtered.length > 0) {
3358
- console.log(formatLogLine(filtered[0]));
3586
+ console.log(formatLogLine(filtered[0], true, verbose));
3359
3587
  }
3360
3588
  }
3361
3589
  }
@@ -3381,11 +3609,11 @@ async function logs() {
3381
3609
  }
3382
3610
 
3383
3611
  // Non-watch mode - one-shot output
3384
- const rawLines = readLogFiles(logsDir);
3612
+ const rawEntries = readLogFiles(logsDir);
3385
3613
 
3386
3614
  // Parse and filter
3387
- let logs: LogLine[] = rawLines
3388
- .map(parseLogLine)
3615
+ let logs: LogLine[] = rawEntries
3616
+ .map(entry => parseLogLine(entry.line, entry.file))
3389
3617
  .filter((log): log is LogLine => log !== null);
3390
3618
 
3391
3619
  logs = filterLogs(logs);
@@ -3410,7 +3638,7 @@ async function logs() {
3410
3638
  console.log();
3411
3639
 
3412
3640
  for (const log of logs) {
3413
- console.log(formatLogLine(log));
3641
+ console.log(formatLogLine(log, true, verbose));
3414
3642
  }
3415
3643
  console.log();
3416
3644
  }
@@ -3522,9 +3750,12 @@ async function db() {
3522
3750
  const command = process.argv[2];
3523
3751
 
3524
3752
  switch (command) {
3525
- case "setup":
3526
- await setup();
3753
+ case "setup": {
3754
+ const reinstallFlag = process.argv.includes("--reinstall") || process.argv.includes("-r");
3755
+ const yesFlag = process.argv.includes("--yes") || process.argv.includes("-y");
3756
+ await setup(reinstallFlag || yesFlag, yesFlag);
3527
3757
  break;
3758
+ }
3528
3759
  case "doctor":
3529
3760
  await doctor();
3530
3761
  break;
@@ -3559,6 +3790,9 @@ switch (command) {
3559
3790
  case "db":
3560
3791
  await db();
3561
3792
  break;
3793
+ case "cells":
3794
+ await cells();
3795
+ break;
3562
3796
  case "log":
3563
3797
  case "logs":
3564
3798
  await logs();