@os-eco/overstory-cli 0.7.0 → 0.7.3

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 (91) hide show
  1. package/README.md +7 -6
  2. package/agents/builder.md +1 -1
  3. package/agents/coordinator.md +12 -11
  4. package/agents/lead.md +6 -6
  5. package/agents/monitor.md +4 -4
  6. package/agents/reviewer.md +1 -1
  7. package/agents/scout.md +5 -5
  8. package/agents/supervisor.md +36 -32
  9. package/package.json +1 -1
  10. package/src/agents/guard-rules.ts +97 -0
  11. package/src/agents/hooks-deployer.test.ts +6 -5
  12. package/src/agents/hooks-deployer.ts +7 -90
  13. package/src/agents/identity.test.ts +3 -2
  14. package/src/agents/manifest.test.ts +4 -3
  15. package/src/agents/overlay.test.ts +10 -9
  16. package/src/agents/overlay.ts +5 -5
  17. package/src/commands/agents.test.ts +10 -4
  18. package/src/commands/clean.test.ts +3 -0
  19. package/src/commands/completions.test.ts +8 -5
  20. package/src/commands/completions.ts +38 -2
  21. package/src/commands/coordinator.test.ts +1 -0
  22. package/src/commands/coordinator.ts +15 -11
  23. package/src/commands/costs.test.ts +9 -3
  24. package/src/commands/dashboard.test.ts +265 -6
  25. package/src/commands/dashboard.ts +367 -64
  26. package/src/commands/doctor.test.ts +3 -2
  27. package/src/commands/errors.test.ts +3 -2
  28. package/src/commands/feed.test.ts +3 -2
  29. package/src/commands/feed.ts +2 -29
  30. package/src/commands/init.test.ts +1 -2
  31. package/src/commands/init.ts +1 -8
  32. package/src/commands/inspect.test.ts +17 -2
  33. package/src/commands/log.test.ts +262 -8
  34. package/src/commands/log.ts +232 -110
  35. package/src/commands/logs.test.ts +3 -2
  36. package/src/commands/mail.test.ts +8 -2
  37. package/src/commands/metrics.test.ts +4 -3
  38. package/src/commands/monitor.ts +15 -11
  39. package/src/commands/nudge.test.ts +4 -2
  40. package/src/commands/prime.test.ts +4 -2
  41. package/src/commands/prime.ts +6 -2
  42. package/src/commands/replay.test.ts +3 -2
  43. package/src/commands/run.test.ts +3 -1
  44. package/src/commands/sling.test.ts +142 -1
  45. package/src/commands/sling.ts +145 -24
  46. package/src/commands/status.test.ts +9 -8
  47. package/src/commands/stop.test.ts +1 -0
  48. package/src/commands/supervisor.ts +19 -12
  49. package/src/commands/trace.test.ts +4 -2
  50. package/src/commands/watch.test.ts +3 -2
  51. package/src/commands/worktree.test.ts +9 -0
  52. package/src/config.test.ts +3 -3
  53. package/src/config.ts +29 -0
  54. package/src/doctor/agents.test.ts +3 -2
  55. package/src/doctor/consistency.test.ts +14 -0
  56. package/src/doctor/logs.test.ts +3 -2
  57. package/src/doctor/structure.test.ts +3 -2
  58. package/src/e2e/init-sling-lifecycle.test.ts +3 -5
  59. package/src/index.ts +3 -1
  60. package/src/logging/color.ts +1 -1
  61. package/src/logging/format.test.ts +110 -0
  62. package/src/logging/format.ts +42 -1
  63. package/src/logging/logger.test.ts +3 -2
  64. package/src/mail/broadcast.test.ts +1 -0
  65. package/src/mail/client.test.ts +3 -2
  66. package/src/mail/store.test.ts +3 -2
  67. package/src/merge/queue.test.ts +3 -2
  68. package/src/merge/resolver.test.ts +39 -0
  69. package/src/merge/resolver.ts +24 -5
  70. package/src/mulch/client.test.ts +63 -2
  71. package/src/mulch/client.ts +62 -1
  72. package/src/runtimes/claude.test.ts +5 -4
  73. package/src/runtimes/pi-guards.test.ts +457 -0
  74. package/src/runtimes/pi-guards.ts +349 -0
  75. package/src/runtimes/pi.test.ts +620 -0
  76. package/src/runtimes/pi.ts +244 -0
  77. package/src/runtimes/registry.test.ts +33 -0
  78. package/src/runtimes/registry.ts +15 -2
  79. package/src/runtimes/types.ts +63 -0
  80. package/src/schema-consistency.test.ts +5 -2
  81. package/src/sessions/compat.test.ts +3 -2
  82. package/src/sessions/compat.ts +1 -0
  83. package/src/sessions/store.test.ts +34 -2
  84. package/src/sessions/store.ts +37 -4
  85. package/src/test-helpers.ts +20 -1
  86. package/src/types.ts +17 -0
  87. package/src/watchdog/daemon.test.ts +11 -7
  88. package/src/watchdog/daemon.ts +1 -1
  89. package/src/watchdog/health.test.ts +1 -0
  90. package/src/watchdog/triage.test.ts +3 -2
  91. package/src/watchdog/triage.ts +14 -4
@@ -3,6 +3,13 @@ import { dirname, join } from "node:path";
3
3
  import { DEFAULT_QUALITY_GATES } from "../config.ts";
4
4
  import { AgentError } from "../errors.ts";
5
5
  import type { QualityGate } from "../types.ts";
6
+ import {
7
+ DANGEROUS_BASH_PATTERNS,
8
+ INTERACTIVE_TOOLS,
9
+ NATIVE_TEAM_TOOLS,
10
+ SAFE_BASH_PREFIXES,
11
+ WRITE_TOOLS,
12
+ } from "./guard-rules.ts";
6
13
 
7
14
  /**
8
15
  * Capabilities that must never modify project files.
@@ -31,96 +38,6 @@ const COORDINATION_CAPABILITIES = new Set(["coordinator", "supervisor", "monitor
31
38
  */
32
39
  const COORDINATION_SAFE_PREFIXES = ["git add", "git commit"];
33
40
 
34
- /**
35
- * Claude Code native team/task tools that bypass overstory orchestration.
36
- * All overstory agents must use `overstory sling` for delegation, not these.
37
- */
38
- const NATIVE_TEAM_TOOLS = [
39
- "Task",
40
- "TeamCreate",
41
- "TeamDelete",
42
- "SendMessage",
43
- "TaskCreate",
44
- "TaskUpdate",
45
- "TaskList",
46
- "TaskGet",
47
- "TaskOutput",
48
- "TaskStop",
49
- ];
50
-
51
- /**
52
- * Tools that require human interaction and block indefinitely in non-interactive
53
- * tmux sessions. Agents run non-interactively and must never call these tools.
54
- * Use overstory mail (--type question) to escalate to the orchestrator instead.
55
- */
56
- const INTERACTIVE_TOOLS = ["AskUserQuestion", "EnterPlanMode", "EnterWorktree"];
57
-
58
- /** Tools that non-implementation agents must not use. */
59
- const WRITE_TOOLS = ["Write", "Edit", "NotebookEdit"];
60
-
61
- /**
62
- * Bash commands that modify files and must be blocked for non-implementation agents.
63
- * Each pattern is a regex fragment used inside a grep -qE check.
64
- */
65
- const DANGEROUS_BASH_PATTERNS = [
66
- "sed\\s+-i",
67
- "sed\\s+--in-place",
68
- "echo\\s+.*>",
69
- "printf\\s+.*>",
70
- "cat\\s+.*>",
71
- "tee\\s",
72
- "\\bvim\\b",
73
- "\\bnano\\b",
74
- "\\bvi\\b",
75
- "\\bmv\\s",
76
- "\\bcp\\s",
77
- "\\brm\\s",
78
- "\\bmkdir\\s",
79
- "\\btouch\\s",
80
- "\\bchmod\\s",
81
- "\\bchown\\s",
82
- ">>",
83
- "\\bgit\\s+add\\b",
84
- "\\bgit\\s+commit\\b",
85
- "\\bgit\\s+merge\\b",
86
- "\\bgit\\s+push\\b",
87
- "\\bgit\\s+reset\\b",
88
- "\\bgit\\s+checkout\\b",
89
- "\\bgit\\s+rebase\\b",
90
- "\\bgit\\s+stash\\b",
91
- "\\bnpm\\s+install\\b",
92
- "\\bbun\\s+install\\b",
93
- "\\bbun\\s+add\\b",
94
- // Runtime eval flags — bypass shell pattern guards by executing JS/Python directly
95
- "\\bbun\\s+-e\\b",
96
- "\\bbun\\s+--eval\\b",
97
- "\\bnode\\s+-e\\b",
98
- "\\bnode\\s+--eval\\b",
99
- "\\bdeno\\s+eval\\b",
100
- "\\bpython3?\\s+-c\\b",
101
- "\\bperl\\s+-e\\b",
102
- "\\bruby\\s+-e\\b",
103
- ];
104
-
105
- /**
106
- * Bash commands that are always safe for non-implementation agents.
107
- * If a command starts with any of these prefixes, it bypasses the dangerous command check.
108
- * This whitelist is checked BEFORE the blocklist.
109
- */
110
- const SAFE_BASH_PREFIXES = [
111
- "ov ",
112
- "overstory ",
113
- "bd ",
114
- "sd ",
115
- "git status",
116
- "git log",
117
- "git diff",
118
- "git show",
119
- "git blame",
120
- "git branch",
121
- "mulch ",
122
- ];
123
-
124
41
  /**
125
42
  * Extract command prefixes from quality gate configurations.
126
43
  *
@@ -1,8 +1,9 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { mkdir, mkdtemp, rm } from "node:fs/promises";
2
+ import { mkdir, mkdtemp } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { AgentError } from "../errors.ts";
6
+ import { cleanupTempDir } from "../test-helpers.ts";
6
7
  import type { AgentIdentity } from "../types.ts";
7
8
  import { createIdentity, loadIdentity, updateIdentity } from "./identity.ts";
8
9
 
@@ -14,7 +15,7 @@ describe("identity", () => {
14
15
  });
15
16
 
16
17
  afterEach(async () => {
17
- await rm(tempDir, { recursive: true, force: true });
18
+ await cleanupTempDir(tempDir);
18
19
  });
19
20
 
20
21
  describe("createIdentity", () => {
@@ -1,8 +1,9 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { mkdir, mkdtemp, rm } from "node:fs/promises";
2
+ import { mkdir, mkdtemp } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { AgentError } from "../errors.ts";
6
+ import { cleanupTempDir } from "../test-helpers.ts";
6
7
  import type { AgentManifest, OverstoryConfig } from "../types.ts";
7
8
  import { createManifestLoader, resolveModel, resolveProviderEnv } from "./manifest.ts";
8
9
 
@@ -41,7 +42,7 @@ describe("createManifestLoader", () => {
41
42
  });
42
43
 
43
44
  afterEach(async () => {
44
- await rm(tempDir, { recursive: true, force: true });
45
+ await cleanupTempDir(tempDir);
45
46
  });
46
47
 
47
48
  /** Write the manifest JSON and create matching .md files. */
@@ -806,7 +807,7 @@ describe("manifest validation accepts arbitrary model strings", () => {
806
807
  });
807
808
 
808
809
  afterEach(async () => {
809
- await rm(tempDir, { recursive: true, force: true });
810
+ await cleanupTempDir(tempDir);
810
811
  });
811
812
 
812
813
  test("accepts provider-prefixed model string", async () => {
@@ -1,8 +1,9 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { mkdir, mkdtemp, rm } from "node:fs/promises";
2
+ import { mkdir, mkdtemp } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { AgentError } from "../errors.ts";
6
+ import { cleanupTempDir } from "../test-helpers.ts";
6
7
  import type { OverlayConfig, QualityGate } from "../types.ts";
7
8
  import {
8
9
  formatQualityGatesBash,
@@ -265,7 +266,7 @@ describe("generateOverlay", () => {
265
266
  expect(output).not.toContain("bun run lint");
266
267
  });
267
268
 
268
- test("scout completion section includes bd close and mail send", async () => {
269
+ test("scout completion section includes sd close and mail send", async () => {
269
270
  const config = makeConfig({
270
271
  capability: "scout",
271
272
  agentName: "recon-1",
@@ -274,7 +275,7 @@ describe("generateOverlay", () => {
274
275
  });
275
276
  const output = await generateOverlay(config);
276
277
 
277
- expect(output).toContain("bd close overstory-task1");
278
+ expect(output).toContain("sd close overstory-task1");
278
279
  expect(output).toContain("ov mail send --to lead-alpha");
279
280
  });
280
281
 
@@ -426,14 +427,14 @@ describe("generateOverlay", () => {
426
427
  expect(output).not.toContain("Quality Gates");
427
428
  });
428
429
 
429
- test("default trackerCli renders as bd in quality gates", async () => {
430
+ test("default trackerCli renders as sd in quality gates", async () => {
430
431
  const config = makeConfig({ capability: "builder", taskId: "overstory-task1" });
431
432
  const output = await generateOverlay(config);
432
433
 
433
- expect(output).toContain("bd close overstory-task1");
434
+ expect(output).toContain("sd close overstory-task1");
434
435
  });
435
436
 
436
- test("custom trackerCli replaces bd in quality gates", async () => {
437
+ test("custom trackerCli replaces sd in quality gates", async () => {
437
438
  const config = makeConfig({
438
439
  capability: "builder",
439
440
  trackerCli: "sd",
@@ -489,11 +490,11 @@ describe("generateOverlay", () => {
489
490
  expect(output).not.toContain("{{TRACKER_NAME}}");
490
491
  });
491
492
 
492
- test("defaults backward-compatible: no trackerCli/trackerName produces bd/beads", async () => {
493
+ test("defaults: no trackerCli/trackerName produces sd/seeds", async () => {
493
494
  const config = makeConfig({ capability: "builder", taskId: "overstory-back" });
494
495
  const output = await generateOverlay(config);
495
496
 
496
- expect(output).toContain("bd close overstory-back");
497
+ expect(output).toContain("sd close overstory-back");
497
498
  });
498
499
 
499
500
  test("dispatch overrides: skipReview injects SKIP REVIEW directive for leads", async () => {
@@ -584,7 +585,7 @@ describe("writeOverlay", () => {
584
585
  });
585
586
 
586
587
  afterEach(async () => {
587
- await rm(tempDir, { recursive: true, force: true });
588
+ await cleanupTempDir(tempDir);
588
589
  });
589
590
 
590
591
  test("creates .claude/CLAUDE.md in worktree directory", async () => {
@@ -182,7 +182,7 @@ function formatQualityGates(config: OverlayConfig): string {
182
182
  "Before reporting completion:",
183
183
  "",
184
184
  `1. **Record mulch learnings:** \`ml record <domain> --type <convention|pattern|reference> --description "..."\` — capture reusable knowledge from your work`,
185
- `2. **Close issue:** \`${config.trackerCli ?? "bd"} close ${config.taskId} --reason "summary of findings"\``,
185
+ `2. **Close issue:** \`${config.trackerCli ?? "sd"} close ${config.taskId} --reason "summary of findings"\``,
186
186
  `3. **Send results:** \`ov mail send --to ${config.parentAgent ?? "coordinator"} --subject "done" --body "Summary" --type result --agent ${config.agentName}\``,
187
187
  "",
188
188
  "You are a read-only agent. Do NOT commit, modify files, or run quality gates.",
@@ -207,7 +207,7 @@ function formatQualityGates(config: OverlayConfig): string {
207
207
  `${gateLines.length + 1}. **Commit:** all changes committed to your branch (${config.branchName})`,
208
208
  `${gateLines.length + 2}. **Record mulch learnings:** \`ml record <domain> --type <convention|pattern|failure|decision> --description "..." --outcome-status success --outcome-agent ${config.agentName}\` — capture insights from your work`,
209
209
  `${gateLines.length + 3}. **Signal completion:** send \`worker_done\` mail to ${config.parentAgent ?? "coordinator"}: \`ov mail send --to ${config.parentAgent ?? "coordinator"} --subject "Worker done: ${config.taskId}" --body "Quality gates passed." --type worker_done --agent ${config.agentName}\``,
210
- `${gateLines.length + 4}. **Close issue:** \`${config.trackerCli ?? "bd"} close ${config.taskId} --reason "summary of changes"\``,
210
+ `${gateLines.length + 4}. **Close issue:** \`${config.trackerCli ?? "sd"} close ${config.taskId} --reason "summary of changes"\``,
211
211
  "",
212
212
  "Do NOT push to the canonical branch. Your work will be merged by the",
213
213
  "coordinator via `ov merge`.",
@@ -225,7 +225,7 @@ function formatConstraints(config: OverlayConfig): string {
225
225
  "",
226
226
  "- You are **read-only**: do NOT modify, create, or delete any files",
227
227
  "- Do NOT commit, push, or make any git state changes",
228
- `- Report completion via \`${config.trackerCli ?? "bd"} close\` AND \`ov mail send --type result\``,
228
+ `- Report completion via \`${config.trackerCli ?? "sd"} close\` AND \`ov mail send --type result\``,
229
229
  "- If you encounter a blocking issue, send mail with `--priority urgent --type error`",
230
230
  ].join("\n");
231
231
  }
@@ -238,7 +238,7 @@ function formatConstraints(config: OverlayConfig): string {
238
238
  "- Only modify files in your File Scope",
239
239
  `- Commit only to your branch: ${config.branchName}`,
240
240
  "- Never push to the canonical branch",
241
- `- Report completion via \`${config.trackerCli ?? "bd"} close\` AND \`ov mail send --type result\``,
241
+ `- Report completion via \`${config.trackerCli ?? "sd"} close\` AND \`ov mail send --type result\``,
242
242
  "- If you encounter a blocking issue, send mail with `--priority urgent --type error`",
243
243
  ].join("\n");
244
244
  }
@@ -318,7 +318,7 @@ export async function generateOverlay(config: OverlayConfig): Promise<string> {
318
318
  "{{QUALITY_GATE_STEPS}}": formatQualityGatesSteps(config.qualityGates),
319
319
  "{{QUALITY_GATE_BASH}}": formatQualityGatesBash(config.qualityGates),
320
320
  "{{QUALITY_GATE_CAPABILITIES}}": formatQualityGatesCapabilities(config.qualityGates),
321
- "{{TRACKER_CLI}}": config.trackerCli ?? "bd",
321
+ "{{TRACKER_CLI}}": config.trackerCli ?? "sd",
322
322
  "{{TRACKER_NAME}}": config.trackerName ?? "seeds",
323
323
  };
324
324
 
@@ -3,10 +3,11 @@
3
3
  */
4
4
 
5
5
  import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
6
- import { mkdtemp, rm } from "node:fs/promises";
6
+ import { mkdtemp } from "node:fs/promises";
7
7
  import { tmpdir } from "node:os";
8
8
  import { join } from "node:path";
9
9
  import { createSessionStore } from "../sessions/store.ts";
10
+ import { cleanupTempDir } from "../test-helpers.ts";
10
11
  import type { AgentSession } from "../types.ts";
11
12
  import { agentsCommand, discoverAgents, extractFileScope } from "./agents.ts";
12
13
 
@@ -65,7 +66,7 @@ Some expertise here.
65
66
  });
66
67
 
67
68
  afterEach(async () => {
68
- await rm(tempDir, { recursive: true, force: true });
69
+ await cleanupTempDir(tempDir);
69
70
  });
70
71
  });
71
72
 
@@ -108,6 +109,7 @@ describe("discoverAgents", () => {
108
109
  lastActivity: "2024-01-01T00:01:00Z",
109
110
  escalationLevel: 0,
110
111
  stalledSince: null,
112
+ transcriptPath: null,
111
113
  };
112
114
 
113
115
  store.upsert(session);
@@ -140,6 +142,7 @@ describe("discoverAgents", () => {
140
142
  lastActivity: "2024-01-01T00:01:00Z",
141
143
  escalationLevel: 0,
142
144
  stalledSince: null,
145
+ transcriptPath: null,
143
146
  };
144
147
 
145
148
  const scout: AgentSession = {
@@ -159,6 +162,7 @@ describe("discoverAgents", () => {
159
162
  lastActivity: "2024-01-01T00:01:00Z",
160
163
  escalationLevel: 0,
161
164
  stalledSince: null,
165
+ transcriptPath: null,
162
166
  };
163
167
 
164
168
  store.upsert(builder);
@@ -191,6 +195,7 @@ describe("discoverAgents", () => {
191
195
  lastActivity: "2024-01-01T00:01:00Z",
192
196
  escalationLevel: 0,
193
197
  stalledSince: null,
198
+ transcriptPath: null,
194
199
  };
195
200
 
196
201
  const completed: AgentSession = {
@@ -210,6 +215,7 @@ describe("discoverAgents", () => {
210
215
  lastActivity: "2024-01-01T00:02:00Z",
211
216
  escalationLevel: 0,
212
217
  stalledSince: null,
218
+ transcriptPath: null,
213
219
  };
214
220
 
215
221
  store.upsert(working);
@@ -230,7 +236,7 @@ describe("discoverAgents", () => {
230
236
  });
231
237
 
232
238
  afterEach(async () => {
233
- await rm(tempDir, { recursive: true, force: true });
239
+ await cleanupTempDir(tempDir);
234
240
  });
235
241
  });
236
242
 
@@ -317,6 +323,6 @@ logging:
317
323
  afterEach(async () => {
318
324
  process.stdout.write = originalStdoutWrite;
319
325
  process.chdir(originalCwd);
320
- await rm(tempDir, { recursive: true, force: true });
326
+ await cleanupTempDir(tempDir);
321
327
  });
322
328
  });
@@ -159,6 +159,7 @@ describe("--all", () => {
159
159
  lastActivity: new Date().toISOString(),
160
160
  escalationLevel: 0,
161
161
  stalledSince: null,
162
+ transcriptPath: null,
162
163
  });
163
164
  store.close();
164
165
 
@@ -303,6 +304,7 @@ describe("individual flags", () => {
303
304
  lastActivity: new Date().toISOString(),
304
305
  escalationLevel: 0,
305
306
  stalledSince: null,
307
+ transcriptPath: null,
306
308
  });
307
309
  store.close();
308
310
 
@@ -424,6 +426,7 @@ describe("synthetic session-end events", () => {
424
426
  lastActivity: new Date().toISOString(),
425
427
  escalationLevel: 0,
426
428
  stalledSince: null,
429
+ transcriptPath: null,
427
430
  ...overrides,
428
431
  };
429
432
  }
@@ -12,8 +12,8 @@ import {
12
12
  } from "./completions.ts";
13
13
 
14
14
  describe("COMMANDS array", () => {
15
- it("should have exactly 30 commands", () => {
16
- expect(COMMANDS).toHaveLength(30);
15
+ it("should have exactly 33 commands", () => {
16
+ expect(COMMANDS).toHaveLength(33);
17
17
  });
18
18
 
19
19
  it("should include all expected command names", () => {
@@ -48,6 +48,9 @@ describe("COMMANDS array", () => {
48
48
  expect(names).toContain("feed");
49
49
  expect(names).toContain("logs");
50
50
  expect(names).toContain("stop");
51
+ expect(names).toContain("ecosystem");
52
+ expect(names).toContain("upgrade");
53
+ expect(names).toContain("completions");
51
54
  });
52
55
  });
53
56
 
@@ -59,7 +62,7 @@ describe("generateBash", () => {
59
62
  expect(script).toContain("_init_completion");
60
63
  });
61
64
 
62
- it("should include all 27 command names", () => {
65
+ it("should include all 33 command names", () => {
63
66
  const script = generateBash();
64
67
  for (const cmd of COMMANDS) {
65
68
  expect(script).toContain(cmd.name);
@@ -93,7 +96,7 @@ describe("generateZsh", () => {
93
96
  expect(script).toContain("_arguments");
94
97
  });
95
98
 
96
- it("should include all 27 command names", () => {
99
+ it("should include all 33 command names", () => {
97
100
  const script = generateZsh();
98
101
  for (const cmd of COMMANDS) {
99
102
  expect(script).toContain(cmd.name);
@@ -123,7 +126,7 @@ describe("generateFish", () => {
123
126
  expect(script).toContain("__fish_use_subcommand");
124
127
  });
125
128
 
126
- it("should include all 27 command names", () => {
129
+ it("should include all 33 command names", () => {
127
130
  const script = generateFish();
128
131
  for (const cmd of COMMANDS) {
129
132
  expect(script).toContain(cmd.name);
@@ -58,6 +58,9 @@ export const COMMANDS: readonly CommandDef[] = [
58
58
  desc: "Initialize .overstory/ in current project",
59
59
  flags: [
60
60
  { name: "--force", desc: "Overwrite existing configuration" },
61
+ { name: "--yes", desc: "Accept all defaults without prompting" },
62
+ { name: "-y", desc: "Alias for --yes" },
63
+ { name: "--name", desc: "Project name", takesValue: true },
61
64
  { name: "--help", desc: "Show help" },
62
65
  ],
63
66
  },
@@ -76,7 +79,13 @@ export const COMMANDS: readonly CommandDef[] = [
76
79
  { name: "--files", desc: "Exclusive file scope (comma-separated)", takesValue: true },
77
80
  { name: "--parent", desc: "Parent agent name", takesValue: true },
78
81
  { name: "--depth", desc: "Current hierarchy depth", takesValue: true },
82
+ { name: "--skip-scout", desc: "Skip scout phase for lead agents" },
83
+ { name: "--skip-review", desc: "Skip review phase for lead agents" },
84
+ { name: "--skip-task-check", desc: "Skip task existence validation" },
79
85
  { name: "--force-hierarchy", desc: "Bypass hierarchy validation" },
86
+ { name: "--max-agents", desc: "Max children per lead", takesValue: true },
87
+ { name: "--dispatch-max-agents", desc: "Per-lead max agents ceiling", takesValue: true },
88
+ { name: "--runtime", desc: "Runtime adapter", takesValue: true },
80
89
  { name: "--json", desc: "JSON output" },
81
90
  { name: "--help", desc: "Show help" },
82
91
  ],
@@ -107,6 +116,7 @@ export const COMMANDS: readonly CommandDef[] = [
107
116
  { name: "--json", desc: "JSON output" },
108
117
  { name: "--verbose", desc: "Extra per-agent detail" },
109
118
  { name: "--agent", desc: "Filter by agent", takesValue: true },
119
+ { name: "--all", desc: "Show sessions from all runs" },
110
120
  { name: "--watch", desc: "Watch mode" },
111
121
  { name: "--interval", desc: "Poll interval in ms", takesValue: true },
112
122
  { name: "--help", desc: "Show help" },
@@ -138,6 +148,7 @@ export const COMMANDS: readonly CommandDef[] = [
138
148
  flags: [
139
149
  { name: "--branch", desc: "Specific branch to merge", takesValue: true },
140
150
  { name: "--all", desc: "All completed branches" },
151
+ { name: "--into", desc: "Target branch to merge into", takesValue: true },
141
152
  { name: "--dry-run", desc: "Check for conflicts only" },
142
153
  { name: "--json", desc: "JSON output" },
143
154
  { name: "--help", desc: "Show help" },
@@ -190,8 +201,10 @@ export const COMMANDS: readonly CommandDef[] = [
190
201
  "merge",
191
202
  "logs",
192
203
  "version",
204
+ "ecosystem",
193
205
  ],
194
206
  },
207
+ { name: "--fix", desc: "Attempt to auto-fix issues" },
195
208
  { name: "--help", desc: "Show help" },
196
209
  ],
197
210
  },
@@ -356,7 +369,7 @@ export const COMMANDS: readonly CommandDef[] = [
356
369
  },
357
370
  {
358
371
  name: "supervisor",
359
- desc: "Per-project supervisor agent",
372
+ desc: "[DEPRECATED] Per-project supervisor agent",
360
373
  flags: [
361
374
  { name: "--json", desc: "JSON output" },
362
375
  { name: "--help", desc: "Show help" },
@@ -606,6 +619,29 @@ export const COMMANDS: readonly CommandDef[] = [
606
619
  },
607
620
  ],
608
621
  },
622
+ {
623
+ name: "ecosystem",
624
+ desc: "Show a summary dashboard of all installed os-eco tools",
625
+ flags: [
626
+ { name: "--json", desc: "JSON output" },
627
+ { name: "--help", desc: "Show help" },
628
+ ],
629
+ },
630
+ {
631
+ name: "upgrade",
632
+ desc: "Upgrade overstory to the latest version",
633
+ flags: [
634
+ { name: "--check", desc: "Compare current vs latest without installing" },
635
+ { name: "--all", desc: "Upgrade all os-eco tools" },
636
+ { name: "--json", desc: "JSON output" },
637
+ { name: "--help", desc: "Show help" },
638
+ ],
639
+ },
640
+ {
641
+ name: "completions",
642
+ desc: "Generate shell completions",
643
+ flags: [{ name: "--help", desc: "Show help" }],
644
+ },
609
645
  ] as const;
610
646
 
611
647
  export function generateBash(): string {
@@ -618,7 +654,7 @@ export function generateBash(): string {
618
654
  " local cur prev words cword",
619
655
  " _init_completion || return",
620
656
  "",
621
- " local commands='init sling prime stop status dashboard inspect merge nudge clean doctor log logs watch trace errors feed replay costs metrics spec coordinator supervisor hooks monitor mail group worktree run'",
657
+ " local commands='agents init sling prime stop status dashboard inspect merge nudge clean doctor log logs watch trace errors feed replay costs metrics spec coordinator supervisor hooks monitor mail group worktree run ecosystem upgrade completions'",
622
658
  "",
623
659
  " # Top-level completion",
624
660
  " if [[ $cword -eq 1 ]]; then",
@@ -297,6 +297,7 @@ function makeCoordinatorSession(overrides: Partial<AgentSession> = {}): AgentSes
297
297
  lastActivity: new Date().toISOString(),
298
298
  escalationLevel: 0,
299
299
  stalledSince: null,
300
+ transcriptPath: null,
300
301
  ...overrides,
301
302
  };
302
303
  }
@@ -15,7 +15,6 @@
15
15
  import { mkdir, unlink } from "node:fs/promises";
16
16
  import { join } from "node:path";
17
17
  import { Command } from "commander";
18
- import { deployHooks } from "../agents/hooks-deployer.ts";
19
18
  import { createIdentity, loadIdentity } from "../agents/identity.ts";
20
19
  import { createManifestLoader, resolveModel } from "../agents/manifest.ts";
21
20
  import { loadConfig } from "../config.ts";
@@ -320,13 +319,26 @@ async function startCoordinator(
320
319
  store.updateState(COORDINATOR_NAME, "completed");
321
320
  }
322
321
 
322
+ // Resolve model and runtime early (needed for deployConfig and spawn)
323
+ const manifestLoader = createManifestLoader(
324
+ join(projectRoot, config.agents.manifestPath),
325
+ join(projectRoot, config.agents.baseDir),
326
+ );
327
+ const manifest = await manifestLoader.load();
328
+ const resolvedModel = resolveModel(config, manifest, "coordinator", "opus");
329
+ const runtime = getRuntime(undefined, config);
330
+
323
331
  // Deploy hooks to the project root so the coordinator gets event logging,
324
332
  // mail check --inject, and activity tracking via the standard hook pipeline.
325
333
  // The ENV_GUARD prefix on all hooks (both template and generated guards)
326
334
  // ensures they only activate when OVERSTORY_AGENT_NAME is set (i.e. for
327
335
  // the coordinator's tmux session), so the user's own Claude Code session
328
336
  // at the project root is unaffected.
329
- await deployHooks(projectRoot, COORDINATOR_NAME, "coordinator");
337
+ await runtime.deployConfig(projectRoot, undefined, {
338
+ agentName: COORDINATOR_NAME,
339
+ capability: "coordinator",
340
+ worktreePath: projectRoot,
341
+ });
330
342
 
331
343
  // Create coordinator identity if first run
332
344
  const identityBaseDir = join(projectRoot, ".overstory", "agents");
@@ -343,15 +355,6 @@ async function startCoordinator(
343
355
  });
344
356
  }
345
357
 
346
- // Resolve model from config > manifest > fallback
347
- const manifestLoader = createManifestLoader(
348
- join(projectRoot, config.agents.manifestPath),
349
- join(projectRoot, config.agents.baseDir),
350
- );
351
- const manifest = await manifestLoader.load();
352
- const resolvedModel = resolveModel(config, manifest, "coordinator", "opus");
353
- const runtime = getRuntime(undefined, config);
354
-
355
358
  // Preflight: verify tmux is installed before attempting to spawn.
356
359
  // Without this check, a missing tmux leads to cryptic errors later.
357
360
  await tmux.ensureTmuxAvailable();
@@ -402,6 +405,7 @@ async function startCoordinator(
402
405
  lastActivity: new Date().toISOString(),
403
406
  escalationLevel: 0,
404
407
  stalledSince: null,
408
+ transcriptPath: null,
405
409
  };
406
410
 
407
411
  store.upsert(session);
@@ -9,12 +9,13 @@
9
9
  */
10
10
 
11
11
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
12
- import { mkdir, mkdtemp, rm } from "node:fs/promises";
12
+ import { mkdir, mkdtemp } from "node:fs/promises";
13
13
  import { tmpdir } from "node:os";
14
14
  import { join } from "node:path";
15
15
  import { ValidationError } from "../errors.ts";
16
16
  import { createMetricsStore } from "../metrics/store.ts";
17
17
  import { createSessionStore } from "../sessions/store.ts";
18
+ import { cleanupTempDir } from "../test-helpers.ts";
18
19
  import type { SessionMetrics } from "../types.ts";
19
20
  import { costsCommand } from "./costs.ts";
20
21
 
@@ -72,7 +73,7 @@ describe("costsCommand", () => {
72
73
  afterEach(async () => {
73
74
  process.stdout.write = originalWrite;
74
75
  process.chdir(originalCwd);
75
- await rm(tempDir, { recursive: true, force: true });
76
+ await cleanupTempDir(tempDir);
76
77
  });
77
78
 
78
79
  function output(): string {
@@ -780,6 +781,7 @@ describe("costsCommand", () => {
780
781
  lastActivity: new Date().toISOString(),
781
782
  escalationLevel: 0,
782
783
  stalledSince: null,
784
+ transcriptPath: null,
783
785
  });
784
786
  sessionStore.close();
785
787
 
@@ -836,6 +838,7 @@ describe("costsCommand", () => {
836
838
  lastActivity: new Date().toISOString(),
837
839
  escalationLevel: 0,
838
840
  stalledSince: null,
841
+ transcriptPath: null,
839
842
  });
840
843
  sessionStore.close();
841
844
 
@@ -900,6 +903,7 @@ describe("costsCommand", () => {
900
903
  lastActivity: new Date().toISOString(),
901
904
  escalationLevel: 0,
902
905
  stalledSince: null,
906
+ transcriptPath: null,
903
907
  });
904
908
  sessionStore.upsert({
905
909
  id: "sess-002",
@@ -918,6 +922,7 @@ describe("costsCommand", () => {
918
922
  lastActivity: new Date().toISOString(),
919
923
  escalationLevel: 0,
920
924
  stalledSince: null,
925
+ transcriptPath: null,
921
926
  });
922
927
  sessionStore.close();
923
928
 
@@ -977,6 +982,7 @@ describe("costsCommand", () => {
977
982
  lastActivity: new Date().toISOString(),
978
983
  escalationLevel: 0,
979
984
  stalledSince: null,
985
+ transcriptPath: null,
980
986
  });
981
987
  sessionStore.close();
982
988
 
@@ -1047,7 +1053,7 @@ describe("costsCommand", () => {
1047
1053
 
1048
1054
  afterEach(async () => {
1049
1055
  process.env.HOME = originalHome;
1050
- await rm(tempHome, { recursive: true, force: true });
1056
+ await cleanupTempDir(tempHome);
1051
1057
  });
1052
1058
 
1053
1059
  test("--self shows orchestrator cost when transcript exists", async () => {