@os-eco/overstory-cli 0.7.4 → 0.7.6

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 CHANGED
@@ -6,7 +6,7 @@ Multi-agent orchestration for AI coding agents.
6
6
  [![CI](https://github.com/jayminwest/overstory/actions/workflows/ci.yml/badge.svg)](https://github.com/jayminwest/overstory/actions/workflows/ci.yml)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
8
8
 
9
- Overstory turns a single coding session into a multi-agent team by spawning worker agents in git worktrees via tmux, coordinating them through a custom SQLite mail system, and merging their work back with tiered conflict resolution. A pluggable `AgentRuntime` interface lets you swap between runtimes — Claude Code, [Pi](https://github.com/nichochar/pi-coding-agent), or your own adapter.
9
+ Overstory turns a single coding session into a multi-agent team by spawning worker agents in git worktrees via tmux, coordinating them through a custom SQLite mail system, and merging their work back with tiered conflict resolution. A pluggable `AgentRuntime` interface lets you swap between runtimes — Claude Code, [Pi](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent), or your own adapter.
10
10
 
11
11
  > **Warning: Agent swarms are not a universal solution.** Do not deploy Overstory without understanding the risks of multi-agent orchestration — compounding error rates, cost amplification, debugging complexity, and merge conflicts are the normal case, not edge cases. Read [STEELMAN.md](STEELMAN.md) for a full risk analysis and the [Agentic Engineering Book](https://github.com/jayminwest/agentic-engineering-book) ([web version](https://jayminwest.com/agentic-engineering-book)) before using this tool in production.
12
12
 
@@ -15,7 +15,8 @@ Overstory turns a single coding session into a multi-agent team by spawning work
15
15
  Requires [Bun](https://bun.sh) v1.0+, git, and tmux. At least one supported agent runtime must be installed:
16
16
 
17
17
  - [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (`claude` CLI)
18
- - [Pi](https://github.com/nichochar/pi-coding-agent) (`pi` CLI)
18
+ - [Pi](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) (`pi` CLI)
19
+ - [GitHub Copilot](https://github.com/features/copilot) (`copilot` CLI)
19
20
 
20
21
  ```bash
21
22
  bun install -g @os-eco/overstory-cli
@@ -77,7 +78,7 @@ Every command supports `--json` where noted. Global flags: `-q`/`--quiet`, `--ti
77
78
 
78
79
  | Command | Description |
79
80
  |---------|-------------|
80
- | `ov init` | Initialize `.overstory/` in current project (`--yes`, `--name`) |
81
+ | `ov init` | Initialize `.overstory/` and bootstrap os-eco tools (`--yes`, `--name`, `--tools`, `--skip-mulch`, `--skip-seeds`, `--skip-canopy`, `--skip-onboard`, `--json`) |
81
82
  | `ov sling <task-id>` | Spawn a worker agent (`--capability`, `--name`, `--spec`, `--files`, `--parent`, `--depth`, `--skip-scout`, `--skip-review`, `--max-agents`, `--dispatch-max-agents`, `--skip-task-check`, `--no-scout-check`, `--runtime`, `--json`) |
82
83
  | `ov stop <agent-name>` | Terminate a running agent (`--clean-worktree`, `--json`) |
83
84
  | `ov prime` | Load context for orchestrator/agent (`--agent`, `--compact`) |
@@ -132,7 +133,7 @@ Every command supports `--json` where noted. Global flags: `-q`/`--quiet`, `--ti
132
133
  | `ov replay` | Interleaved chronological replay (`--run`, `--agent`, `--since`, `--until`, `--limit`, `--json`) |
133
134
  | `ov feed` | Unified real-time event stream (`--follow`, `--interval`, `--agent`, `--run`, `--json`) |
134
135
  | `ov logs` | Query NDJSON logs across agents (`--agent`, `--level`, `--since`, `--until`, `--follow`, `--json`) |
135
- | `ov costs` | Token/cost analysis and breakdown (`--live`, `--self`, `--agent`, `--run`, `--by-capability`, `--last`, `--json`) |
136
+ | `ov costs` | Token/cost analysis and breakdown (`--live`, `--self`, `--agent`, `--run`, `--bead`, `--by-capability`, `--last`, `--json`) |
136
137
  | `ov metrics` | Show session metrics (`--last`, `--json`) |
137
138
  | `ov run list` | List orchestration runs (`--last`, `--json`) |
138
139
  | `ov run show <id>` | Show run details |
@@ -153,7 +154,7 @@ Every command supports `--json` where noted. Global flags: `-q`/`--quiet`, `--ti
153
154
  | `ov monitor status` | Show monitor state |
154
155
  | `ov log <event>` | Log a hook event (`--agent`) |
155
156
  | `ov clean` | Clean up worktrees, sessions, artifacts (`--completed`, `--all`, `--run`) |
156
- | `ov doctor` | Run health checks on overstory setup (`--category`, `--fix`, `--json`) |
157
+ | `ov doctor` | Run health checks on overstory setup — 11 categories (`--category`, `--fix`, `--json`) |
157
158
  | `ov ecosystem` | Show os-eco tool versions and health (`--json`) |
158
159
  | `ov upgrade` | Upgrade overstory to latest npm version (`--check`, `--all`, `--json`) |
159
160
  | `ov agents discover` | Discover agents by capability/state/parent (`--capability`, `--state`, `--parent`, `--json`) |
@@ -171,6 +172,7 @@ Overstory is runtime-agnostic. The `AgentRuntime` interface (`src/runtimes/types
171
172
  |---------|-----|-----------------|--------|
172
173
  | Claude Code | `claude` | `settings.local.json` hooks | Stable |
173
174
  | Pi | `pi` | `.pi/extensions/` guard extension | Active development |
175
+ | Copilot | `copilot` | (none — `--allow-all-tools`) | Active development |
174
176
 
175
177
  ## How It Works
176
178
 
@@ -240,7 +242,7 @@ overstory/
240
242
  run.ts Orchestration run lifecycle
241
243
  trace.ts Agent/task timeline viewing
242
244
  clean.ts Worktree/session cleanup
243
- doctor.ts Health check runner (10 check modules)
245
+ doctor.ts Health check runner (11 check modules)
244
246
  inspect.ts Deep per-agent inspection
245
247
  spec.ts Task spec management
246
248
  errors.ts Aggregated error view
@@ -265,9 +267,9 @@ overstory/
265
267
  watchdog/ Tiered health monitoring (daemon, triage, health)
266
268
  logging/ Multi-format logger + sanitizer + reporter + color control + shared theme/format
267
269
  metrics/ SQLite metrics + pricing + transcript parsing
268
- doctor/ Health check modules (10 checks)
270
+ doctor/ Health check modules (11 checks)
269
271
  insights/ Session insight analyzer for auto-expertise
270
- runtimes/ AgentRuntime abstraction (registry + adapters: Claude, Pi)
272
+ runtimes/ AgentRuntime abstraction (registry + adapters: Claude, Pi, Copilot)
271
273
  tracker/ Pluggable task tracker (beads + seeds backends)
272
274
  mulch/ mulch client (programmatic API + CLI wrapper)
273
275
  e2e/ End-to-end lifecycle tests
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@os-eco/overstory-cli",
3
- "version": "0.7.4",
3
+ "version": "0.7.6",
4
4
  "description": "Multi-agent orchestration for AI coding agents — spawn workers in git worktrees via tmux, coordinate through SQLite mail, merge with tiered conflict resolution. Pluggable runtime adapters for Claude Code, Pi, and more.",
5
5
  "author": "Jaymin West",
6
6
  "license": "MIT",
@@ -10,6 +10,7 @@ import { loadConfig } from "../config.ts";
10
10
  import { ValidationError } from "../errors.ts";
11
11
  import { jsonOutput } from "../json.ts";
12
12
  import { accent, color } from "../logging/color.ts";
13
+ import { getRuntime } from "../runtimes/registry.ts";
13
14
  import { openSessionStore } from "../sessions/compat.ts";
14
15
  import { type AgentSession, SUPPORTED_CAPABILITIES } from "../types.ts";
15
16
 
@@ -41,12 +42,19 @@ const KNOWN_INSTRUCTION_PATHS = [
41
42
  * or can't be read.
42
43
  *
43
44
  * @param worktreePath - Absolute path to the agent's worktree
45
+ * @param runtimeInstructionPath - Optional runtime-specific instruction path to try first
44
46
  * @returns Array of file paths (relative to worktree root)
45
47
  */
46
- export async function extractFileScope(worktreePath: string): Promise<string[]> {
48
+ export async function extractFileScope(
49
+ worktreePath: string,
50
+ runtimeInstructionPath?: string,
51
+ ): Promise<string[]> {
47
52
  try {
48
53
  let content: string | null = null;
49
- for (const relPath of KNOWN_INSTRUCTION_PATHS) {
54
+ const pathsToTry = runtimeInstructionPath
55
+ ? [runtimeInstructionPath, ...KNOWN_INSTRUCTION_PATHS]
56
+ : KNOWN_INSTRUCTION_PATHS;
57
+ for (const relPath of pathsToTry) {
50
58
  const overlayPath = join(worktreePath, relPath);
51
59
  const overlayFile = Bun.file(overlayPath);
52
60
  if (await overlayFile.exists()) {
@@ -112,6 +120,16 @@ export async function discoverAgents(
112
120
  const overstoryDir = join(root, ".overstory");
113
121
  const { store } = openSessionStore(overstoryDir);
114
122
 
123
+ // Resolve runtime instruction path from config; fall back gracefully if config is absent.
124
+ let runtimeInstructionPath: string | undefined;
125
+ try {
126
+ const config = await loadConfig(root);
127
+ const runtime = getRuntime(undefined, config);
128
+ runtimeInstructionPath = runtime.instructionPath;
129
+ } catch {
130
+ // Config may not exist in all contexts; KNOWN_INSTRUCTION_PATHS will be used as fallback.
131
+ }
132
+
115
133
  try {
116
134
  const sessions: AgentSession[] = opts?.includeAll ? store.getAll() : store.getActive();
117
135
 
@@ -124,7 +142,7 @@ export async function discoverAgents(
124
142
  // Extract file scopes for each agent
125
143
  const agents: DiscoveredAgent[] = await Promise.all(
126
144
  filteredSessions.map(async (session) => {
127
- const fileScope = await extractFileScope(session.worktreePath);
145
+ const fileScope = await extractFileScope(session.worktreePath, runtimeInstructionPath);
128
146
  return {
129
147
  agentName: session.agentName,
130
148
  capability: session.capability,
@@ -55,12 +55,18 @@ export const COMMANDS: readonly CommandDef[] = [
55
55
  },
56
56
  {
57
57
  name: "init",
58
- desc: "Initialize .overstory/ in current project",
58
+ desc: "Initialize .overstory/ and bootstrap os-eco ecosystem tools",
59
59
  flags: [
60
60
  { name: "--force", desc: "Overwrite existing configuration" },
61
61
  { name: "--yes", desc: "Accept all defaults without prompting" },
62
62
  { name: "-y", desc: "Alias for --yes" },
63
63
  { name: "--name", desc: "Project name", takesValue: true },
64
+ { name: "--tools", desc: "Comma-separated list of tools to bootstrap", takesValue: true },
65
+ { name: "--skip-mulch", desc: "Skip mulch bootstrap" },
66
+ { name: "--skip-seeds", desc: "Skip seeds bootstrap" },
67
+ { name: "--skip-canopy", desc: "Skip canopy bootstrap" },
68
+ { name: "--skip-onboard", desc: "Skip CLAUDE.md onboarding step" },
69
+ { name: "--json", desc: "Output result as JSON" },
64
70
  { name: "--help", desc: "Show help" },
65
71
  ],
66
72
  },
@@ -540,7 +540,9 @@ describe("startCoordinator", () => {
540
540
  expect(calls.createSession).toHaveLength(1);
541
541
  const cmd = calls.createSession[0]?.command ?? "";
542
542
  expect(cmd).toContain("--append-system-prompt");
543
- expect(cmd).toContain("# Coordinator Agent");
543
+ // File path is passed via $(cat ...) instead of inlining content (overstory#45)
544
+ expect(cmd).toContain("$(cat '");
545
+ expect(cmd).toContain("agent-defs/coordinator.md");
544
546
  });
545
547
 
546
548
  test("reads model from manifest instead of hardcoding", async () => {
@@ -363,17 +363,20 @@ async function startCoordinator(
363
363
  // Inject the coordinator base definition via --append-system-prompt so the
364
364
  // coordinator knows its role, hierarchy rules, and delegation patterns
365
365
  // (overstory-gaio, overstory-0kwf).
366
+ // Pass the file path (not content) so the shell inside the tmux pane reads
367
+ // it via $(cat ...) — avoids tmux IPC "command too long" errors with large
368
+ // agent definitions (overstory#45).
366
369
  const agentDefPath = join(projectRoot, ".overstory", "agent-defs", "coordinator.md");
367
370
  const agentDefFile = Bun.file(agentDefPath);
368
- let appendSystemPrompt: string | undefined;
371
+ let appendSystemPromptFile: string | undefined;
369
372
  if (await agentDefFile.exists()) {
370
- appendSystemPrompt = await agentDefFile.text();
373
+ appendSystemPromptFile = agentDefPath;
371
374
  }
372
375
  const spawnCmd = runtime.buildSpawnCommand({
373
376
  model: resolvedModel.model,
374
377
  permissionMode: "bypass",
375
378
  cwd: projectRoot,
376
- appendSystemPrompt,
379
+ appendSystemPromptFile,
377
380
  env: {
378
381
  ...runtime.buildEnv(resolvedModel),
379
382
  OVERSTORY_AGENT_NAME: COORDINATOR_NAME,
@@ -1023,6 +1023,48 @@ describe("costsCommand", () => {
1023
1023
  });
1024
1024
  });
1025
1025
 
1026
+ // === --bead filter ===
1027
+
1028
+ describe("--bead filter", () => {
1029
+ test("--bead filters by task ID (JSON)", async () => {
1030
+ const dbPath = join(tempDir, ".overstory", "metrics.db");
1031
+ const store = createMetricsStore(dbPath);
1032
+ store.recordSession(makeMetrics({ agentName: "builder-1", taskId: "task-A" }));
1033
+ store.recordSession(makeMetrics({ agentName: "builder-2", taskId: "task-A" }));
1034
+ store.recordSession(
1035
+ makeMetrics({ agentName: "scout-1", taskId: "task-B", capability: "scout" }),
1036
+ );
1037
+ store.close();
1038
+
1039
+ await costsCommand(["--json", "--bead", "task-A"]);
1040
+ const out = output();
1041
+
1042
+ const parsed = JSON.parse(out.trim()) as { sessions: Record<string, unknown>[] };
1043
+ expect(parsed.sessions).toHaveLength(2);
1044
+ expect(parsed.sessions.every((s) => s.taskId === "task-A")).toBe(true);
1045
+ });
1046
+
1047
+ test("--bead returns empty for unknown task", async () => {
1048
+ const dbPath = join(tempDir, ".overstory", "metrics.db");
1049
+ const store = createMetricsStore(dbPath);
1050
+ store.recordSession(makeMetrics({ agentName: "builder-1", taskId: "task-A" }));
1051
+ store.close();
1052
+
1053
+ await costsCommand(["--json", "--bead", "nonexistent"]);
1054
+ const out = output();
1055
+
1056
+ const parsed = JSON.parse(out.trim()) as { sessions: unknown[] };
1057
+ expect(parsed.sessions).toEqual([]);
1058
+ });
1059
+
1060
+ test("--bead appears in help text", async () => {
1061
+ await costsCommand(["--help"]);
1062
+ const out = output();
1063
+
1064
+ expect(out).toContain("--bead");
1065
+ });
1066
+ });
1067
+
1026
1068
  // === --self flag ===
1027
1069
 
1028
1070
  describe("--self flag", () => {
@@ -1111,7 +1153,7 @@ describe("costsCommand", () => {
1111
1153
  await costsCommand(["--self"]);
1112
1154
  const out = output();
1113
1155
 
1114
- expect(out).toContain("No orchestrator transcript found");
1156
+ expect(out).toContain("No transcript found");
1115
1157
  });
1116
1158
 
1117
1159
  test("--self --json outputs error JSON when no transcript found", async () => {
@@ -1122,7 +1164,8 @@ describe("costsCommand", () => {
1122
1164
  const out = output();
1123
1165
 
1124
1166
  const parsed = JSON.parse(out.trim()) as Record<string, unknown>;
1125
- expect(parsed.error).toBe("No orchestrator transcript found");
1167
+ expect(typeof parsed.error).toBe("string");
1168
+ expect(parsed.error as string).toContain("No transcript found");
1126
1169
  });
1127
1170
 
1128
1171
  test("--self in help text", async () => {
@@ -16,6 +16,7 @@ import { color } from "../logging/color.ts";
16
16
  import { renderHeader, separator } from "../logging/theme.ts";
17
17
  import { createMetricsStore } from "../metrics/store.ts";
18
18
  import { estimateCost, parseTranscriptUsage } from "../metrics/transcript.ts";
19
+ import { getRuntime } from "../runtimes/registry.ts";
19
20
  import { openSessionStore } from "../sessions/compat.ts";
20
21
  import type { SessionMetrics } from "../types.ts";
21
22
 
@@ -43,24 +44,45 @@ function padLeft(str: string, width: number): string {
43
44
  }
44
45
 
45
46
  /**
46
- * Discover the orchestrator's Claude Code transcript JSONL file.
47
- *
48
- * Scans ~/.claude/projects/{project-key}/ for JSONL files and returns
49
- * the most recently modified one, corresponding to the current orchestrator session.
47
+ * Resolve the transcript directory for a given runtime and project root.
50
48
  *
49
+ * @param runtimeId - The runtime identifier (e.g. "claude")
51
50
  * @param projectRoot - Absolute path to the project root
52
- * @returns Absolute path to the most recent transcript, or null if none found
51
+ * @returns Absolute path to the transcript directory, or null if not supported
53
52
  */
54
- async function discoverOrchestratorTranscript(projectRoot: string): Promise<string | null> {
53
+ function getTranscriptDir(runtimeId: string, projectRoot: string): string | null {
55
54
  const homeDir = process.env.HOME ?? "";
56
55
  if (homeDir.length === 0) return null;
56
+ switch (runtimeId) {
57
+ case "claude": {
58
+ const projectKey = projectRoot.replace(/\//g, "-");
59
+ return join(homeDir, ".claude", "projects", projectKey);
60
+ }
61
+ default:
62
+ return null;
63
+ }
64
+ }
57
65
 
58
- const projectKey = projectRoot.replace(/\//g, "-");
59
- const projectDir = join(homeDir, ".claude", "projects", projectKey);
66
+ /**
67
+ * Discover the orchestrator's transcript JSONL file for the given runtime.
68
+ *
69
+ * Scans the runtime-specific transcript directory for JSONL files and returns
70
+ * the most recently modified one, corresponding to the current orchestrator session.
71
+ *
72
+ * @param runtimeId - The runtime identifier (e.g. "claude")
73
+ * @param projectRoot - Absolute path to the project root
74
+ * @returns Absolute path to the most recent transcript, or null if none found
75
+ */
76
+ async function discoverOrchestratorTranscript(
77
+ runtimeId: string,
78
+ projectRoot: string,
79
+ ): Promise<string | null> {
80
+ const transcriptDir = getTranscriptDir(runtimeId, projectRoot);
81
+ if (transcriptDir === null) return null;
60
82
 
61
83
  let entries: string[];
62
84
  try {
63
- entries = await readdir(projectDir);
85
+ entries = await readdir(transcriptDir);
64
86
  } catch {
65
87
  return null;
66
88
  }
@@ -72,7 +94,7 @@ async function discoverOrchestratorTranscript(projectRoot: string): Promise<stri
72
94
  let bestMtime = 0;
73
95
 
74
96
  for (const file of jsonlFiles) {
75
- const filePath = join(projectDir, file);
97
+ const filePath = join(transcriptDir, file);
76
98
  try {
77
99
  const fileStat = await stat(filePath);
78
100
  if (fileStat.mtimeMs > bestMtime) {
@@ -236,6 +258,7 @@ interface CostsOpts {
236
258
  byCapability?: boolean;
237
259
  agent?: string;
238
260
  run?: string;
261
+ bead?: string;
239
262
  last?: string;
240
263
  json?: boolean;
241
264
  }
@@ -247,6 +270,7 @@ async function executeCosts(opts: CostsOpts): Promise<void> {
247
270
  const byCapability = opts.byCapability ?? false;
248
271
  const agentName = opts.agent;
249
272
  const runId = opts.run;
273
+ const beadId = opts.bead;
250
274
  const lastStr = opts.last;
251
275
 
252
276
  if (lastStr !== undefined) {
@@ -267,13 +291,15 @@ async function executeCosts(opts: CostsOpts): Promise<void> {
267
291
 
268
292
  // Handle --self flag (early return for self-scan)
269
293
  if (self) {
270
- const transcriptPath = await discoverOrchestratorTranscript(config.project.root);
294
+ const runtime = getRuntime(undefined, config);
295
+ const transcriptPath = await discoverOrchestratorTranscript(runtime.id, config.project.root);
271
296
  if (!transcriptPath) {
272
297
  if (json) {
273
- jsonError("costs", "No orchestrator transcript found");
298
+ jsonError("costs", `No transcript found for runtime '${runtime.id}'`);
274
299
  } else {
275
300
  process.stdout.write(
276
- "No orchestrator transcript found.\nExpected at: ~/.claude/projects/{project-key}/*.jsonl\n",
301
+ `No transcript found for runtime '${runtime.id}'.\n` +
302
+ "Transcript discovery may not be supported for this runtime.\n",
277
303
  );
278
304
  }
279
305
  return;
@@ -521,6 +547,8 @@ async function executeCosts(opts: CostsOpts): Promise<void> {
521
547
  sessions = metricsStore.getSessionsByAgent(agentName);
522
548
  } else if (runId !== undefined) {
523
549
  sessions = metricsStore.getSessionsByRun(runId);
550
+ } else if (beadId !== undefined) {
551
+ sessions = metricsStore.getSessionsByTask(beadId);
524
552
  } else {
525
553
  sessions = metricsStore.getRecentSessions(last);
526
554
  }
@@ -559,6 +587,7 @@ export function createCostsCommand(): Command {
559
587
  .option("--self", "Show cost for the current orchestrator session")
560
588
  .option("--agent <name>", "Filter by agent name")
561
589
  .option("--run <id>", "Filter by run ID")
590
+ .option("--bead <id>", "Show cost breakdown for a specific task/bead")
562
591
  .option("--by-capability", "Group results by capability with subtotals")
563
592
  .option("--last <n>", "Number of recent sessions (default: 20)")
564
593
  .option("--json", "Output as JSON")
@@ -15,6 +15,7 @@ import { checkDependencies } from "../doctor/dependencies.ts";
15
15
  import { checkEcosystem } from "../doctor/ecosystem.ts";
16
16
  import { checkLogs } from "../doctor/logs.ts";
17
17
  import { checkMergeQueue } from "../doctor/merge-queue.ts";
18
+ import { checkProviders } from "../doctor/providers.ts";
18
19
  import { checkStructure } from "../doctor/structure.ts";
19
20
  import type { DoctorCategory, DoctorCheck, DoctorCheckFn } from "../doctor/types.ts";
20
21
  import { checkVersion } from "../doctor/version.ts";
@@ -35,6 +36,7 @@ const ALL_CHECKS: Array<{ category: DoctorCategory; fn: DoctorCheckFn }> = [
35
36
  { category: "logs", fn: checkLogs },
36
37
  { category: "version", fn: checkVersion },
37
38
  { category: "ecosystem", fn: checkEcosystem },
39
+ { category: "providers", fn: checkProviders },
38
40
  ];
39
41
 
40
42
  /**
@@ -166,7 +168,7 @@ export function createDoctorCommand(options?: DoctorCommandOptions): Command {
166
168
  .option("--fix", "Attempt to auto-fix issues")
167
169
  .addHelpText(
168
170
  "after",
169
- "\nCategories: dependencies, structure, config, databases, consistency, agents, merge, logs, version, ecosystem",
171
+ "\nCategories: dependencies, structure, config, databases, consistency, agents, merge, logs, version, ecosystem, providers",
170
172
  )
171
173
  .action(
172
174
  async (opts: { json?: boolean; verbose?: boolean; category?: string; fix?: boolean }) => {