@os-eco/overstory-cli 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +381 -0
- package/agents/builder.md +137 -0
- package/agents/coordinator.md +263 -0
- package/agents/lead.md +301 -0
- package/agents/merger.md +160 -0
- package/agents/monitor.md +214 -0
- package/agents/reviewer.md +140 -0
- package/agents/scout.md +119 -0
- package/agents/supervisor.md +423 -0
- package/package.json +47 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +101 -0
- package/src/agents/hooks-deployer.test.ts +2040 -0
- package/src/agents/hooks-deployer.ts +607 -0
- package/src/agents/identity.test.ts +603 -0
- package/src/agents/identity.ts +384 -0
- package/src/agents/lifecycle.test.ts +196 -0
- package/src/agents/lifecycle.ts +183 -0
- package/src/agents/manifest.test.ts +746 -0
- package/src/agents/manifest.ts +354 -0
- package/src/agents/overlay.test.ts +676 -0
- package/src/agents/overlay.ts +308 -0
- package/src/beads/client.test.ts +217 -0
- package/src/beads/client.ts +202 -0
- package/src/beads/molecules.test.ts +338 -0
- package/src/beads/molecules.ts +198 -0
- package/src/commands/agents.test.ts +322 -0
- package/src/commands/agents.ts +287 -0
- package/src/commands/clean.test.ts +670 -0
- package/src/commands/clean.ts +618 -0
- package/src/commands/completions.test.ts +342 -0
- package/src/commands/completions.ts +887 -0
- package/src/commands/coordinator.test.ts +1530 -0
- package/src/commands/coordinator.ts +733 -0
- package/src/commands/costs.test.ts +1119 -0
- package/src/commands/costs.ts +564 -0
- package/src/commands/dashboard.test.ts +308 -0
- package/src/commands/dashboard.ts +838 -0
- package/src/commands/doctor.test.ts +294 -0
- package/src/commands/doctor.ts +213 -0
- package/src/commands/errors.test.ts +647 -0
- package/src/commands/errors.ts +248 -0
- package/src/commands/feed.test.ts +578 -0
- package/src/commands/feed.ts +361 -0
- package/src/commands/group.test.ts +262 -0
- package/src/commands/group.ts +511 -0
- package/src/commands/hooks.test.ts +458 -0
- package/src/commands/hooks.ts +253 -0
- package/src/commands/init.test.ts +347 -0
- package/src/commands/init.ts +650 -0
- package/src/commands/inspect.test.ts +670 -0
- package/src/commands/inspect.ts +431 -0
- package/src/commands/log.test.ts +1454 -0
- package/src/commands/log.ts +724 -0
- package/src/commands/logs.test.ts +379 -0
- package/src/commands/logs.ts +546 -0
- package/src/commands/mail.test.ts +1270 -0
- package/src/commands/mail.ts +771 -0
- package/src/commands/merge.test.ts +670 -0
- package/src/commands/merge.ts +355 -0
- package/src/commands/metrics.test.ts +444 -0
- package/src/commands/metrics.ts +143 -0
- package/src/commands/monitor.test.ts +191 -0
- package/src/commands/monitor.ts +390 -0
- package/src/commands/nudge.test.ts +230 -0
- package/src/commands/nudge.ts +372 -0
- package/src/commands/prime.test.ts +470 -0
- package/src/commands/prime.ts +381 -0
- package/src/commands/replay.test.ts +741 -0
- package/src/commands/replay.ts +360 -0
- package/src/commands/run.test.ts +431 -0
- package/src/commands/run.ts +351 -0
- package/src/commands/sling.test.ts +657 -0
- package/src/commands/sling.ts +661 -0
- package/src/commands/spec.test.ts +203 -0
- package/src/commands/spec.ts +168 -0
- package/src/commands/status.test.ts +430 -0
- package/src/commands/status.ts +398 -0
- package/src/commands/stop.test.ts +420 -0
- package/src/commands/stop.ts +151 -0
- package/src/commands/supervisor.test.ts +187 -0
- package/src/commands/supervisor.ts +535 -0
- package/src/commands/trace.test.ts +745 -0
- package/src/commands/trace.ts +325 -0
- package/src/commands/watch.test.ts +145 -0
- package/src/commands/watch.ts +247 -0
- package/src/commands/worktree.test.ts +786 -0
- package/src/commands/worktree.ts +311 -0
- package/src/config.test.ts +822 -0
- package/src/config.ts +829 -0
- package/src/doctor/agents.test.ts +454 -0
- package/src/doctor/agents.ts +396 -0
- package/src/doctor/config-check.test.ts +190 -0
- package/src/doctor/config-check.ts +183 -0
- package/src/doctor/consistency.test.ts +651 -0
- package/src/doctor/consistency.ts +294 -0
- package/src/doctor/databases.test.ts +290 -0
- package/src/doctor/databases.ts +218 -0
- package/src/doctor/dependencies.test.ts +184 -0
- package/src/doctor/dependencies.ts +175 -0
- package/src/doctor/logs.test.ts +251 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +216 -0
- package/src/doctor/merge-queue.ts +144 -0
- package/src/doctor/structure.test.ts +291 -0
- package/src/doctor/structure.ts +198 -0
- package/src/doctor/types.ts +37 -0
- package/src/doctor/version.test.ts +136 -0
- package/src/doctor/version.ts +129 -0
- package/src/e2e/init-sling-lifecycle.test.ts +277 -0
- package/src/errors.ts +217 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +369 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/index.ts +316 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/logging/color.test.ts +142 -0
- package/src/logging/color.ts +71 -0
- package/src/logging/logger.test.ts +813 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +259 -0
- package/src/logging/reporter.ts +109 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/mail/broadcast.test.ts +203 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +773 -0
- package/src/mail/client.ts +223 -0
- package/src/mail/store.test.ts +705 -0
- package/src/mail/store.ts +387 -0
- package/src/merge/queue.test.ts +359 -0
- package/src/merge/queue.ts +231 -0
- package/src/merge/resolver.test.ts +1345 -0
- package/src/merge/resolver.ts +645 -0
- package/src/metrics/store.test.ts +667 -0
- package/src/metrics/store.ts +445 -0
- package/src/metrics/summary.test.ts +398 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +356 -0
- package/src/metrics/transcript.ts +175 -0
- package/src/mulch/client.test.ts +671 -0
- package/src/mulch/client.ts +332 -0
- package/src/sessions/compat.test.ts +280 -0
- package/src/sessions/compat.ts +104 -0
- package/src/sessions/store.test.ts +873 -0
- package/src/sessions/store.ts +494 -0
- package/src/test-helpers.test.ts +124 -0
- package/src/test-helpers.ts +126 -0
- package/src/tracker/beads.ts +56 -0
- package/src/tracker/factory.test.ts +80 -0
- package/src/tracker/factory.ts +64 -0
- package/src/tracker/seeds.ts +182 -0
- package/src/tracker/types.ts +52 -0
- package/src/types.ts +724 -0
- package/src/watchdog/daemon.test.ts +1975 -0
- package/src/watchdog/daemon.ts +671 -0
- package/src/watchdog/health.test.ts +431 -0
- package/src/watchdog/health.ts +264 -0
- package/src/watchdog/triage.test.ts +164 -0
- package/src/watchdog/triage.ts +179 -0
- package/src/worktree/manager.test.ts +439 -0
- package/src/worktree/manager.ts +198 -0
- package/src/worktree/tmux.test.ts +1009 -0
- package/src/worktree/tmux.ts +509 -0
- package/templates/CLAUDE.md.tmpl +89 -0
- package/templates/hooks.json.tmpl +105 -0
- package/templates/overlay.md.tmpl +81 -0
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: overstory init [--force]
|
|
3
|
+
*
|
|
4
|
+
* Scaffolds the `.overstory/` directory in the current project with:
|
|
5
|
+
* - config.yaml (serialized from DEFAULT_CONFIG)
|
|
6
|
+
* - agent-manifest.json (starter agent definitions)
|
|
7
|
+
* - hooks.json (central hooks config)
|
|
8
|
+
* - Required subdirectories (agents/, worktrees/, specs/, logs/)
|
|
9
|
+
* - .gitignore entries for transient files
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Database } from "bun:sqlite";
|
|
13
|
+
import { mkdir, readdir } from "node:fs/promises";
|
|
14
|
+
import { basename, join } from "node:path";
|
|
15
|
+
import { DEFAULT_CONFIG } from "../config.ts";
|
|
16
|
+
import { ValidationError } from "../errors.ts";
|
|
17
|
+
import type { AgentManifest, OverstoryConfig } from "../types.ts";
|
|
18
|
+
|
|
19
|
+
const OVERSTORY_DIR = ".overstory";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Detect the project name from git or fall back to directory name.
|
|
23
|
+
*/
|
|
24
|
+
async function detectProjectName(root: string): Promise<string> {
|
|
25
|
+
// Try git remote origin
|
|
26
|
+
try {
|
|
27
|
+
const proc = Bun.spawn(["git", "remote", "get-url", "origin"], {
|
|
28
|
+
cwd: root,
|
|
29
|
+
stdout: "pipe",
|
|
30
|
+
stderr: "pipe",
|
|
31
|
+
});
|
|
32
|
+
const exitCode = await proc.exited;
|
|
33
|
+
if (exitCode === 0) {
|
|
34
|
+
const url = (await new Response(proc.stdout).text()).trim();
|
|
35
|
+
// Extract repo name from URL: git@host:user/repo.git or https://host/user/repo.git
|
|
36
|
+
const match = url.match(/\/([^/]+?)(?:\.git)?$/);
|
|
37
|
+
if (match?.[1]) {
|
|
38
|
+
return match[1];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// Git not available or not a git repo
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return basename(root);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Detect the canonical branch name from git.
|
|
50
|
+
*/
|
|
51
|
+
async function detectCanonicalBranch(root: string): Promise<string> {
|
|
52
|
+
try {
|
|
53
|
+
const proc = Bun.spawn(["git", "symbolic-ref", "refs/remotes/origin/HEAD"], {
|
|
54
|
+
cwd: root,
|
|
55
|
+
stdout: "pipe",
|
|
56
|
+
stderr: "pipe",
|
|
57
|
+
});
|
|
58
|
+
const exitCode = await proc.exited;
|
|
59
|
+
if (exitCode === 0) {
|
|
60
|
+
const ref = (await new Response(proc.stdout).text()).trim();
|
|
61
|
+
// refs/remotes/origin/main -> main
|
|
62
|
+
const branch = ref.split("/").pop();
|
|
63
|
+
if (branch) {
|
|
64
|
+
return branch;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// Not available
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Fall back to checking current branch
|
|
72
|
+
try {
|
|
73
|
+
const proc = Bun.spawn(["git", "branch", "--show-current"], {
|
|
74
|
+
cwd: root,
|
|
75
|
+
stdout: "pipe",
|
|
76
|
+
stderr: "pipe",
|
|
77
|
+
});
|
|
78
|
+
const exitCode = await proc.exited;
|
|
79
|
+
if (exitCode === 0) {
|
|
80
|
+
const branch = (await new Response(proc.stdout).text()).trim();
|
|
81
|
+
if (branch) {
|
|
82
|
+
return branch;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// Not available
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return "main";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Serialize an OverstoryConfig to YAML format.
|
|
94
|
+
*
|
|
95
|
+
* Handles nested objects with indentation, scalar values,
|
|
96
|
+
* arrays with `- item` syntax, and empty arrays as `[]`.
|
|
97
|
+
*/
|
|
98
|
+
function serializeConfigToYaml(config: OverstoryConfig): string {
|
|
99
|
+
const lines: string[] = [];
|
|
100
|
+
lines.push("# Overstory configuration");
|
|
101
|
+
lines.push("# See: https://github.com/overstory/overstory");
|
|
102
|
+
lines.push("");
|
|
103
|
+
|
|
104
|
+
serializeObject(config as unknown as Record<string, unknown>, lines, 0);
|
|
105
|
+
|
|
106
|
+
return `${lines.join("\n")}\n`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Recursively serialize an object to YAML lines.
|
|
111
|
+
*/
|
|
112
|
+
function serializeObject(obj: Record<string, unknown>, lines: string[], depth: number): void {
|
|
113
|
+
const indent = " ".repeat(depth);
|
|
114
|
+
|
|
115
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
116
|
+
if (value === null || value === undefined) {
|
|
117
|
+
lines.push(`${indent}${key}: null`);
|
|
118
|
+
} else if (typeof value === "object" && !Array.isArray(value)) {
|
|
119
|
+
lines.push(`${indent}${key}:`);
|
|
120
|
+
serializeObject(value as Record<string, unknown>, lines, depth + 1);
|
|
121
|
+
} else if (Array.isArray(value)) {
|
|
122
|
+
if (value.length === 0) {
|
|
123
|
+
lines.push(`${indent}${key}: []`);
|
|
124
|
+
} else {
|
|
125
|
+
lines.push(`${indent}${key}:`);
|
|
126
|
+
const itemIndent = " ".repeat(depth + 1);
|
|
127
|
+
const propIndent = " ".repeat(depth + 2);
|
|
128
|
+
for (const item of value) {
|
|
129
|
+
if (item !== null && typeof item === "object" && !Array.isArray(item)) {
|
|
130
|
+
// Object array item: "- firstKey: firstVal\n otherKey: otherVal"
|
|
131
|
+
const entries = Object.entries(item as Record<string, unknown>);
|
|
132
|
+
if (entries.length > 0) {
|
|
133
|
+
const [firstKey, firstVal] = entries[0] ?? [];
|
|
134
|
+
lines.push(`${itemIndent}- ${firstKey}: ${formatYamlValue(firstVal)}`);
|
|
135
|
+
for (let j = 1; j < entries.length; j++) {
|
|
136
|
+
const [k, v] = entries[j] ?? [];
|
|
137
|
+
lines.push(`${propIndent}${k}: ${formatYamlValue(v)}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
lines.push(`${itemIndent}- ${formatYamlValue(item)}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
lines.push(`${indent}${key}: ${formatYamlValue(value)}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Format a scalar value for YAML output.
|
|
153
|
+
*/
|
|
154
|
+
function formatYamlValue(value: unknown): string {
|
|
155
|
+
if (typeof value === "string") {
|
|
156
|
+
// Quote strings that could be misinterpreted
|
|
157
|
+
if (
|
|
158
|
+
value === "" ||
|
|
159
|
+
value === "true" ||
|
|
160
|
+
value === "false" ||
|
|
161
|
+
value === "null" ||
|
|
162
|
+
value.includes(":") ||
|
|
163
|
+
value.includes("#") ||
|
|
164
|
+
value.includes("'") ||
|
|
165
|
+
value.includes('"') ||
|
|
166
|
+
value.includes("\n") ||
|
|
167
|
+
/^\d/.test(value)
|
|
168
|
+
) {
|
|
169
|
+
// Use double quotes, escaping inner double quotes
|
|
170
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
171
|
+
}
|
|
172
|
+
return value;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (typeof value === "number") {
|
|
176
|
+
return String(value);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (typeof value === "boolean") {
|
|
180
|
+
return value ? "true" : "false";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (value === null || value === undefined) {
|
|
184
|
+
return "null";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return String(value);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Build the starter agent manifest.
|
|
192
|
+
*/
|
|
193
|
+
function buildAgentManifest(): AgentManifest {
|
|
194
|
+
const agents: AgentManifest["agents"] = {
|
|
195
|
+
scout: {
|
|
196
|
+
file: "scout.md",
|
|
197
|
+
model: "haiku",
|
|
198
|
+
tools: ["Read", "Glob", "Grep", "Bash"],
|
|
199
|
+
capabilities: ["explore", "research"],
|
|
200
|
+
canSpawn: false,
|
|
201
|
+
constraints: ["read-only"],
|
|
202
|
+
},
|
|
203
|
+
builder: {
|
|
204
|
+
file: "builder.md",
|
|
205
|
+
model: "sonnet",
|
|
206
|
+
tools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"],
|
|
207
|
+
capabilities: ["implement", "refactor", "fix"],
|
|
208
|
+
canSpawn: false,
|
|
209
|
+
constraints: [],
|
|
210
|
+
},
|
|
211
|
+
reviewer: {
|
|
212
|
+
file: "reviewer.md",
|
|
213
|
+
model: "sonnet",
|
|
214
|
+
tools: ["Read", "Glob", "Grep", "Bash"],
|
|
215
|
+
capabilities: ["review", "validate"],
|
|
216
|
+
canSpawn: false,
|
|
217
|
+
constraints: ["read-only"],
|
|
218
|
+
},
|
|
219
|
+
lead: {
|
|
220
|
+
file: "lead.md",
|
|
221
|
+
model: "opus",
|
|
222
|
+
tools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash", "Task"],
|
|
223
|
+
capabilities: ["coordinate", "implement", "review"],
|
|
224
|
+
canSpawn: true,
|
|
225
|
+
constraints: [],
|
|
226
|
+
},
|
|
227
|
+
merger: {
|
|
228
|
+
file: "merger.md",
|
|
229
|
+
model: "sonnet",
|
|
230
|
+
tools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"],
|
|
231
|
+
capabilities: ["merge", "resolve-conflicts"],
|
|
232
|
+
canSpawn: false,
|
|
233
|
+
constraints: [],
|
|
234
|
+
},
|
|
235
|
+
coordinator: {
|
|
236
|
+
file: "coordinator.md",
|
|
237
|
+
model: "opus",
|
|
238
|
+
tools: ["Read", "Glob", "Grep", "Bash"],
|
|
239
|
+
capabilities: ["coordinate", "dispatch", "escalate"],
|
|
240
|
+
canSpawn: true,
|
|
241
|
+
constraints: ["read-only", "no-worktree"],
|
|
242
|
+
},
|
|
243
|
+
supervisor: {
|
|
244
|
+
file: "supervisor.md",
|
|
245
|
+
model: "opus",
|
|
246
|
+
tools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash", "Task"],
|
|
247
|
+
capabilities: ["coordinate", "supervise"],
|
|
248
|
+
canSpawn: true,
|
|
249
|
+
constraints: [],
|
|
250
|
+
},
|
|
251
|
+
monitor: {
|
|
252
|
+
file: "monitor.md",
|
|
253
|
+
model: "sonnet",
|
|
254
|
+
tools: ["Read", "Glob", "Grep", "Bash"],
|
|
255
|
+
capabilities: ["monitor", "patrol"],
|
|
256
|
+
canSpawn: false,
|
|
257
|
+
constraints: ["read-only", "no-worktree"],
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// Build capability index: map each capability to agent names that declare it
|
|
262
|
+
const capabilityIndex: Record<string, string[]> = {};
|
|
263
|
+
for (const [name, def] of Object.entries(agents)) {
|
|
264
|
+
for (const cap of def.capabilities) {
|
|
265
|
+
const existing = capabilityIndex[cap];
|
|
266
|
+
if (existing) {
|
|
267
|
+
existing.push(name);
|
|
268
|
+
} else {
|
|
269
|
+
capabilityIndex[cap] = [name];
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return { version: "1.0", agents, capabilityIndex };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Build the hooks.json content for the project orchestrator.
|
|
279
|
+
*
|
|
280
|
+
* Always generates from scratch (not from the agent template, which contains
|
|
281
|
+
* {{AGENT_NAME}} placeholders and space indentation). Uses tab indentation
|
|
282
|
+
* to match Biome formatting rules.
|
|
283
|
+
*/
|
|
284
|
+
function buildHooksJson(): string {
|
|
285
|
+
// Tool name extraction: reads hook stdin JSON and extracts tool_name field.
|
|
286
|
+
// Claude Code sends {"tool_name":"Bash","tool_input":{...}} on stdin for
|
|
287
|
+
// PreToolUse/PostToolUse hooks.
|
|
288
|
+
const toolNameExtract =
|
|
289
|
+
'read -r INPUT; TOOL_NAME=$(echo "$INPUT" | sed \'s/.*"tool_name": *"\\([^"]*\\)".*/\\1/\');';
|
|
290
|
+
|
|
291
|
+
const hooks = {
|
|
292
|
+
hooks: {
|
|
293
|
+
SessionStart: [
|
|
294
|
+
{
|
|
295
|
+
matcher: "",
|
|
296
|
+
hooks: [
|
|
297
|
+
{
|
|
298
|
+
type: "command",
|
|
299
|
+
command: "overstory prime --agent orchestrator",
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
UserPromptSubmit: [
|
|
305
|
+
{
|
|
306
|
+
matcher: "",
|
|
307
|
+
hooks: [
|
|
308
|
+
{
|
|
309
|
+
type: "command",
|
|
310
|
+
command: "overstory mail check --inject --agent orchestrator",
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
PreToolUse: [
|
|
316
|
+
{
|
|
317
|
+
matcher: "Bash",
|
|
318
|
+
hooks: [
|
|
319
|
+
{
|
|
320
|
+
type: "command",
|
|
321
|
+
command:
|
|
322
|
+
'read -r INPUT; CMD=$(echo "$INPUT" | sed \'s/.*"command": *"\\([^"]*\\)".*/\\1/\'); if echo "$CMD" | grep -qE \'\\bgit\\s+push\\b\'; then echo \'{"decision":"block","reason":"git push is blocked by overstory — merge locally, push manually when ready"}\'; exit 0; fi;',
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
matcher: "",
|
|
328
|
+
hooks: [
|
|
329
|
+
{
|
|
330
|
+
type: "command",
|
|
331
|
+
command: `${toolNameExtract} overstory log tool-start --agent orchestrator --tool-name "$TOOL_NAME"`,
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
PostToolUse: [
|
|
337
|
+
{
|
|
338
|
+
matcher: "",
|
|
339
|
+
hooks: [
|
|
340
|
+
{
|
|
341
|
+
type: "command",
|
|
342
|
+
command: `${toolNameExtract} overstory log tool-end --agent orchestrator --tool-name "$TOOL_NAME"`,
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
matcher: "Bash",
|
|
348
|
+
hooks: [
|
|
349
|
+
{
|
|
350
|
+
type: "command",
|
|
351
|
+
command:
|
|
352
|
+
"read -r INPUT; if echo \"$INPUT\" | grep -q 'git commit'; then mulch diff HEAD~1 2>/dev/null || true; fi",
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
Stop: [
|
|
358
|
+
{
|
|
359
|
+
matcher: "",
|
|
360
|
+
hooks: [
|
|
361
|
+
{
|
|
362
|
+
type: "command",
|
|
363
|
+
command: "overstory log session-end --agent orchestrator",
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
type: "command",
|
|
367
|
+
command: "mulch learn",
|
|
368
|
+
},
|
|
369
|
+
],
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
PreCompact: [
|
|
373
|
+
{
|
|
374
|
+
matcher: "",
|
|
375
|
+
hooks: [
|
|
376
|
+
{
|
|
377
|
+
type: "command",
|
|
378
|
+
command: "overstory prime --agent orchestrator --compact",
|
|
379
|
+
},
|
|
380
|
+
],
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
return `${JSON.stringify(hooks, null, "\t")}\n`;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Migrate existing SQLite databases on --force reinit.
|
|
391
|
+
*
|
|
392
|
+
* Opens each DB, enables WAL mode, and re-runs CREATE TABLE/INDEX IF NOT EXISTS
|
|
393
|
+
* to apply any schema additions without losing existing data.
|
|
394
|
+
*/
|
|
395
|
+
async function migrateExistingDatabases(overstoryPath: string): Promise<string[]> {
|
|
396
|
+
const migrated: string[] = [];
|
|
397
|
+
|
|
398
|
+
// Migrate mail.db
|
|
399
|
+
const mailDbPath = join(overstoryPath, "mail.db");
|
|
400
|
+
if (await Bun.file(mailDbPath).exists()) {
|
|
401
|
+
const db = new Database(mailDbPath);
|
|
402
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
403
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
404
|
+
db.exec(`
|
|
405
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
406
|
+
id TEXT PRIMARY KEY,
|
|
407
|
+
from_agent TEXT NOT NULL,
|
|
408
|
+
to_agent TEXT NOT NULL,
|
|
409
|
+
subject TEXT NOT NULL,
|
|
410
|
+
body TEXT NOT NULL,
|
|
411
|
+
type TEXT NOT NULL DEFAULT 'status',
|
|
412
|
+
priority TEXT NOT NULL DEFAULT 'normal',
|
|
413
|
+
thread_id TEXT,
|
|
414
|
+
read INTEGER NOT NULL DEFAULT 0,
|
|
415
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
416
|
+
)`);
|
|
417
|
+
db.exec(`
|
|
418
|
+
CREATE INDEX IF NOT EXISTS idx_inbox ON messages(to_agent, read);
|
|
419
|
+
CREATE INDEX IF NOT EXISTS idx_thread ON messages(thread_id)`);
|
|
420
|
+
db.close();
|
|
421
|
+
migrated.push("mail.db");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Migrate metrics.db
|
|
425
|
+
const metricsDbPath = join(overstoryPath, "metrics.db");
|
|
426
|
+
if (await Bun.file(metricsDbPath).exists()) {
|
|
427
|
+
const db = new Database(metricsDbPath);
|
|
428
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
429
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
430
|
+
db.exec(`
|
|
431
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
432
|
+
agent_name TEXT NOT NULL,
|
|
433
|
+
task_id TEXT NOT NULL,
|
|
434
|
+
capability TEXT NOT NULL,
|
|
435
|
+
started_at TEXT NOT NULL,
|
|
436
|
+
completed_at TEXT,
|
|
437
|
+
duration_ms INTEGER NOT NULL DEFAULT 0,
|
|
438
|
+
exit_code INTEGER,
|
|
439
|
+
merge_result TEXT,
|
|
440
|
+
parent_agent TEXT,
|
|
441
|
+
PRIMARY KEY (agent_name, task_id)
|
|
442
|
+
)`);
|
|
443
|
+
db.close();
|
|
444
|
+
migrated.push("metrics.db");
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return migrated;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Content for .overstory/.gitignore — runtime state that should not be tracked.
|
|
452
|
+
* Uses wildcard+whitelist pattern: ignore everything, whitelist tracked files.
|
|
453
|
+
* Auto-healed by overstory prime on each session start.
|
|
454
|
+
* Config files (config.yaml, agent-manifest.json, hooks.json) remain tracked.
|
|
455
|
+
*/
|
|
456
|
+
export const OVERSTORY_GITIGNORE = `# Wildcard+whitelist: ignore everything, whitelist tracked files
|
|
457
|
+
# Auto-healed by overstory prime on each session start
|
|
458
|
+
*
|
|
459
|
+
!.gitignore
|
|
460
|
+
!config.yaml
|
|
461
|
+
!agent-manifest.json
|
|
462
|
+
!hooks.json
|
|
463
|
+
!groups.json
|
|
464
|
+
!agent-defs/
|
|
465
|
+
!README.md
|
|
466
|
+
`;
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Content for .overstory/README.md — explains the directory to contributors.
|
|
470
|
+
*/
|
|
471
|
+
export const OVERSTORY_README = `# .overstory/
|
|
472
|
+
|
|
473
|
+
This directory is managed by [overstory](https://github.com/jayminwest/overstory) — a multi-agent orchestration system for Claude Code.
|
|
474
|
+
|
|
475
|
+
Overstory turns a single Claude Code 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.
|
|
476
|
+
|
|
477
|
+
## Key Commands
|
|
478
|
+
|
|
479
|
+
- \`overstory init\` — Initialize this directory
|
|
480
|
+
- \`overstory status\` — Show active agents and state
|
|
481
|
+
- \`overstory sling <id>\` — Spawn a worker agent
|
|
482
|
+
- \`overstory mail check\` — Check agent messages
|
|
483
|
+
- \`overstory merge\` — Merge agent work back
|
|
484
|
+
- \`overstory dashboard\` — Live TUI monitoring
|
|
485
|
+
- \`overstory doctor\` — Run health checks
|
|
486
|
+
|
|
487
|
+
## Structure
|
|
488
|
+
|
|
489
|
+
- \`config.yaml\` — Project configuration
|
|
490
|
+
- \`agent-manifest.json\` — Agent registry
|
|
491
|
+
- \`hooks.json\` — Claude Code hooks config
|
|
492
|
+
- \`agent-defs/\` — Agent definition files (.md)
|
|
493
|
+
- \`specs/\` — Task specifications
|
|
494
|
+
- \`agents/\` — Per-agent state and identity
|
|
495
|
+
- \`worktrees/\` — Git worktrees (gitignored)
|
|
496
|
+
- \`logs/\` — Agent logs (gitignored)
|
|
497
|
+
`;
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Write .overstory/.gitignore for runtime state files.
|
|
501
|
+
* Always overwrites to support --force reinit and auto-healing via prime.
|
|
502
|
+
*/
|
|
503
|
+
export async function writeOverstoryGitignore(overstoryPath: string): Promise<void> {
|
|
504
|
+
const gitignorePath = join(overstoryPath, ".gitignore");
|
|
505
|
+
await Bun.write(gitignorePath, OVERSTORY_GITIGNORE);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Write .overstory/README.md explaining the directory to contributors.
|
|
510
|
+
* Always overwrites to support --force reinit.
|
|
511
|
+
*/
|
|
512
|
+
export async function writeOverstoryReadme(overstoryPath: string): Promise<void> {
|
|
513
|
+
const readmePath = join(overstoryPath, "README.md");
|
|
514
|
+
await Bun.write(readmePath, OVERSTORY_README);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Print a success status line.
|
|
519
|
+
*/
|
|
520
|
+
function printCreated(relativePath: string): void {
|
|
521
|
+
process.stdout.write(` \u2713 Created ${relativePath}\n`);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Entry point for `overstory init [--force]`.
|
|
526
|
+
*
|
|
527
|
+
* Scaffolds the .overstory/ directory structure in the current working directory.
|
|
528
|
+
*
|
|
529
|
+
* @param args - CLI arguments after "init" subcommand
|
|
530
|
+
*/
|
|
531
|
+
const INIT_HELP = `overstory init — Initialize .overstory/ in current project
|
|
532
|
+
|
|
533
|
+
Usage: overstory init [--force]
|
|
534
|
+
|
|
535
|
+
Options:
|
|
536
|
+
--force Reinitialize even if .overstory/ already exists
|
|
537
|
+
--help, -h Show this help`;
|
|
538
|
+
|
|
539
|
+
export async function initCommand(args: string[]): Promise<void> {
|
|
540
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
541
|
+
process.stdout.write(`${INIT_HELP}\n`);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const force = args.includes("--force");
|
|
546
|
+
const projectRoot = process.cwd();
|
|
547
|
+
const overstoryPath = join(projectRoot, OVERSTORY_DIR);
|
|
548
|
+
|
|
549
|
+
// 0. Verify we're inside a git repository
|
|
550
|
+
const gitCheck = Bun.spawn(["git", "rev-parse", "--is-inside-work-tree"], {
|
|
551
|
+
cwd: projectRoot,
|
|
552
|
+
stdout: "pipe",
|
|
553
|
+
stderr: "pipe",
|
|
554
|
+
});
|
|
555
|
+
const gitCheckExit = await gitCheck.exited;
|
|
556
|
+
if (gitCheckExit !== 0) {
|
|
557
|
+
throw new ValidationError("overstory requires a git repository. Run 'git init' first.", {
|
|
558
|
+
field: "git",
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// 1. Check if .overstory/ already exists
|
|
563
|
+
const existingDir = Bun.file(join(overstoryPath, "config.yaml"));
|
|
564
|
+
if (await existingDir.exists()) {
|
|
565
|
+
if (!force) {
|
|
566
|
+
process.stdout.write(
|
|
567
|
+
"Warning: .overstory/ already initialized in this project.\n" +
|
|
568
|
+
"Use --force to reinitialize.\n",
|
|
569
|
+
);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
process.stdout.write("Reinitializing .overstory/ (--force)\n\n");
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// 2. Detect project info
|
|
576
|
+
const projectName = await detectProjectName(projectRoot);
|
|
577
|
+
const canonicalBranch = await detectCanonicalBranch(projectRoot);
|
|
578
|
+
|
|
579
|
+
process.stdout.write(`Initializing overstory for "${projectName}"...\n\n`);
|
|
580
|
+
|
|
581
|
+
// 3. Create directory structure
|
|
582
|
+
const dirs = [
|
|
583
|
+
OVERSTORY_DIR,
|
|
584
|
+
join(OVERSTORY_DIR, "agents"),
|
|
585
|
+
join(OVERSTORY_DIR, "agent-defs"),
|
|
586
|
+
join(OVERSTORY_DIR, "worktrees"),
|
|
587
|
+
join(OVERSTORY_DIR, "specs"),
|
|
588
|
+
join(OVERSTORY_DIR, "logs"),
|
|
589
|
+
];
|
|
590
|
+
|
|
591
|
+
for (const dir of dirs) {
|
|
592
|
+
await mkdir(join(projectRoot, dir), { recursive: true });
|
|
593
|
+
printCreated(`${dir}/`);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// 3b. Deploy agent definition .md files from overstory install directory
|
|
597
|
+
const overstoryAgentsDir = join(import.meta.dir, "..", "..", "agents");
|
|
598
|
+
const agentDefsTarget = join(overstoryPath, "agent-defs");
|
|
599
|
+
const agentDefFiles = await readdir(overstoryAgentsDir);
|
|
600
|
+
for (const fileName of agentDefFiles) {
|
|
601
|
+
if (!fileName.endsWith(".md")) continue;
|
|
602
|
+
const source = Bun.file(join(overstoryAgentsDir, fileName));
|
|
603
|
+
const content = await source.text();
|
|
604
|
+
await Bun.write(join(agentDefsTarget, fileName), content);
|
|
605
|
+
printCreated(`${OVERSTORY_DIR}/agent-defs/${fileName}`);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// 4. Write config.yaml
|
|
609
|
+
const config = structuredClone(DEFAULT_CONFIG);
|
|
610
|
+
config.project.name = projectName;
|
|
611
|
+
config.project.root = projectRoot;
|
|
612
|
+
config.project.canonicalBranch = canonicalBranch;
|
|
613
|
+
|
|
614
|
+
const configYaml = serializeConfigToYaml(config);
|
|
615
|
+
const configPath = join(overstoryPath, "config.yaml");
|
|
616
|
+
await Bun.write(configPath, configYaml);
|
|
617
|
+
printCreated(`${OVERSTORY_DIR}/config.yaml`);
|
|
618
|
+
|
|
619
|
+
// 5. Write agent-manifest.json
|
|
620
|
+
const manifest = buildAgentManifest();
|
|
621
|
+
const manifestPath = join(overstoryPath, "agent-manifest.json");
|
|
622
|
+
await Bun.write(manifestPath, `${JSON.stringify(manifest, null, "\t")}\n`);
|
|
623
|
+
printCreated(`${OVERSTORY_DIR}/agent-manifest.json`);
|
|
624
|
+
|
|
625
|
+
// 6. Write hooks.json
|
|
626
|
+
const hooksContent = buildHooksJson();
|
|
627
|
+
const hooksPath = join(overstoryPath, "hooks.json");
|
|
628
|
+
await Bun.write(hooksPath, hooksContent);
|
|
629
|
+
printCreated(`${OVERSTORY_DIR}/hooks.json`);
|
|
630
|
+
|
|
631
|
+
// 7. Write .overstory/.gitignore for runtime state
|
|
632
|
+
await writeOverstoryGitignore(overstoryPath);
|
|
633
|
+
printCreated(`${OVERSTORY_DIR}/.gitignore`);
|
|
634
|
+
|
|
635
|
+
// 7b. Write .overstory/README.md
|
|
636
|
+
await writeOverstoryReadme(overstoryPath);
|
|
637
|
+
printCreated(`${OVERSTORY_DIR}/README.md`);
|
|
638
|
+
|
|
639
|
+
// 8. Migrate existing SQLite databases on --force reinit
|
|
640
|
+
if (force) {
|
|
641
|
+
const migrated = await migrateExistingDatabases(overstoryPath);
|
|
642
|
+
for (const dbName of migrated) {
|
|
643
|
+
process.stdout.write(` \u2713 Migrated ${OVERSTORY_DIR}/${dbName} (schema validated)\n`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
process.stdout.write("\nDone.\n");
|
|
648
|
+
process.stdout.write(" Next: run `overstory hooks install` to enable Claude Code hooks.\n");
|
|
649
|
+
process.stdout.write(" Then: run `overstory status` to see the current state.\n");
|
|
650
|
+
}
|