@os-eco/overstory-cli 0.7.9 → 0.8.2

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 (42) hide show
  1. package/README.md +16 -7
  2. package/agents/coordinator.md +41 -0
  3. package/agents/orchestrator.md +239 -0
  4. package/package.json +1 -1
  5. package/src/agents/guard-rules.test.ts +372 -0
  6. package/src/commands/coordinator.test.ts +334 -0
  7. package/src/commands/coordinator.ts +366 -0
  8. package/src/commands/dashboard.test.ts +86 -0
  9. package/src/commands/dashboard.ts +8 -4
  10. package/src/commands/feed.test.ts +8 -0
  11. package/src/commands/init.test.ts +2 -1
  12. package/src/commands/init.ts +2 -2
  13. package/src/commands/inspect.test.ts +156 -1
  14. package/src/commands/inspect.ts +19 -4
  15. package/src/commands/replay.test.ts +8 -0
  16. package/src/commands/sling.ts +218 -121
  17. package/src/commands/status.test.ts +77 -0
  18. package/src/commands/status.ts +6 -3
  19. package/src/commands/stop.test.ts +134 -0
  20. package/src/commands/stop.ts +41 -11
  21. package/src/commands/trace.test.ts +8 -0
  22. package/src/commands/update.test.ts +465 -0
  23. package/src/commands/update.ts +263 -0
  24. package/src/config.test.ts +65 -1
  25. package/src/config.ts +23 -0
  26. package/src/e2e/init-sling-lifecycle.test.ts +3 -2
  27. package/src/index.ts +21 -2
  28. package/src/logging/theme.ts +4 -0
  29. package/src/runtimes/connections.test.ts +74 -0
  30. package/src/runtimes/connections.ts +34 -0
  31. package/src/runtimes/registry.test.ts +1 -1
  32. package/src/runtimes/registry.ts +2 -0
  33. package/src/runtimes/sapling.test.ts +1237 -0
  34. package/src/runtimes/sapling.ts +698 -0
  35. package/src/runtimes/types.ts +45 -0
  36. package/src/types.ts +5 -1
  37. package/src/watchdog/daemon.ts +34 -0
  38. package/src/watchdog/health.test.ts +102 -0
  39. package/src/watchdog/health.ts +140 -69
  40. package/src/worktree/process.test.ts +101 -0
  41. package/src/worktree/process.ts +111 -0
  42. package/src/worktree/tmux.ts +5 -0
@@ -0,0 +1,372 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ DANGEROUS_BASH_PATTERNS,
4
+ INTERACTIVE_TOOLS,
5
+ NATIVE_TEAM_TOOLS,
6
+ SAFE_BASH_PREFIXES,
7
+ WRITE_TOOLS,
8
+ } from "./guard-rules.ts";
9
+
10
+ // ─── NATIVE_TEAM_TOOLS ───────────────────────────────────────────────────────
11
+
12
+ describe("NATIVE_TEAM_TOOLS", () => {
13
+ test("is a non-empty array", () => {
14
+ expect(Array.isArray(NATIVE_TEAM_TOOLS)).toBe(true);
15
+ expect(NATIVE_TEAM_TOOLS.length).toBeGreaterThan(0);
16
+ });
17
+
18
+ test("contains all expected Claude Code team/task tools", () => {
19
+ const expected = [
20
+ "Task",
21
+ "TeamCreate",
22
+ "TeamDelete",
23
+ "SendMessage",
24
+ "TaskCreate",
25
+ "TaskUpdate",
26
+ "TaskList",
27
+ "TaskGet",
28
+ "TaskOutput",
29
+ "TaskStop",
30
+ ];
31
+ for (const tool of expected) {
32
+ expect(NATIVE_TEAM_TOOLS).toContain(tool);
33
+ }
34
+ });
35
+
36
+ test("has exactly 10 entries", () => {
37
+ expect(NATIVE_TEAM_TOOLS.length).toBe(10);
38
+ });
39
+
40
+ test("has no duplicate entries", () => {
41
+ const unique = new Set(NATIVE_TEAM_TOOLS);
42
+ expect(unique.size).toBe(NATIVE_TEAM_TOOLS.length);
43
+ });
44
+
45
+ test("all entries are non-empty strings", () => {
46
+ for (const tool of NATIVE_TEAM_TOOLS) {
47
+ expect(typeof tool).toBe("string");
48
+ expect(tool.length).toBeGreaterThan(0);
49
+ }
50
+ });
51
+ });
52
+
53
+ // ─── INTERACTIVE_TOOLS ───────────────────────────────────────────────────────
54
+
55
+ describe("INTERACTIVE_TOOLS", () => {
56
+ test("is a non-empty array", () => {
57
+ expect(Array.isArray(INTERACTIVE_TOOLS)).toBe(true);
58
+ expect(INTERACTIVE_TOOLS.length).toBeGreaterThan(0);
59
+ });
60
+
61
+ test("contains AskUserQuestion", () => {
62
+ expect(INTERACTIVE_TOOLS).toContain("AskUserQuestion");
63
+ });
64
+
65
+ test("contains EnterPlanMode", () => {
66
+ expect(INTERACTIVE_TOOLS).toContain("EnterPlanMode");
67
+ });
68
+
69
+ test("contains EnterWorktree", () => {
70
+ expect(INTERACTIVE_TOOLS).toContain("EnterWorktree");
71
+ });
72
+
73
+ test("has no duplicate entries", () => {
74
+ const unique = new Set(INTERACTIVE_TOOLS);
75
+ expect(unique.size).toBe(INTERACTIVE_TOOLS.length);
76
+ });
77
+
78
+ test("all entries are non-empty strings", () => {
79
+ for (const tool of INTERACTIVE_TOOLS) {
80
+ expect(typeof tool).toBe("string");
81
+ expect(tool.length).toBeGreaterThan(0);
82
+ }
83
+ });
84
+
85
+ test("does not contain any NATIVE_TEAM_TOOLS (no overlap)", () => {
86
+ const nativeSet = new Set(NATIVE_TEAM_TOOLS);
87
+ for (const tool of INTERACTIVE_TOOLS) {
88
+ expect(nativeSet.has(tool)).toBe(false);
89
+ }
90
+ });
91
+ });
92
+
93
+ // ─── WRITE_TOOLS ─────────────────────────────────────────────────────────────
94
+
95
+ describe("WRITE_TOOLS", () => {
96
+ test("is a non-empty array", () => {
97
+ expect(Array.isArray(WRITE_TOOLS)).toBe(true);
98
+ expect(WRITE_TOOLS.length).toBeGreaterThan(0);
99
+ });
100
+
101
+ test("contains Write", () => {
102
+ expect(WRITE_TOOLS).toContain("Write");
103
+ });
104
+
105
+ test("contains Edit", () => {
106
+ expect(WRITE_TOOLS).toContain("Edit");
107
+ });
108
+
109
+ test("contains NotebookEdit", () => {
110
+ expect(WRITE_TOOLS).toContain("NotebookEdit");
111
+ });
112
+
113
+ test("has no duplicate entries", () => {
114
+ const unique = new Set(WRITE_TOOLS);
115
+ expect(unique.size).toBe(WRITE_TOOLS.length);
116
+ });
117
+
118
+ test("all entries are non-empty strings", () => {
119
+ for (const tool of WRITE_TOOLS) {
120
+ expect(typeof tool).toBe("string");
121
+ expect(tool.length).toBeGreaterThan(0);
122
+ }
123
+ });
124
+
125
+ test("does not overlap with NATIVE_TEAM_TOOLS", () => {
126
+ const nativeSet = new Set(NATIVE_TEAM_TOOLS);
127
+ for (const tool of WRITE_TOOLS) {
128
+ expect(nativeSet.has(tool)).toBe(false);
129
+ }
130
+ });
131
+
132
+ test("does not overlap with INTERACTIVE_TOOLS", () => {
133
+ const interactiveSet = new Set(INTERACTIVE_TOOLS);
134
+ for (const tool of WRITE_TOOLS) {
135
+ expect(interactiveSet.has(tool)).toBe(false);
136
+ }
137
+ });
138
+ });
139
+
140
+ // ─── DANGEROUS_BASH_PATTERNS ─────────────────────────────────────────────────
141
+
142
+ describe("DANGEROUS_BASH_PATTERNS", () => {
143
+ test("is a non-empty array", () => {
144
+ expect(Array.isArray(DANGEROUS_BASH_PATTERNS)).toBe(true);
145
+ expect(DANGEROUS_BASH_PATTERNS.length).toBeGreaterThan(0);
146
+ });
147
+
148
+ test("all entries are non-empty strings", () => {
149
+ for (const pattern of DANGEROUS_BASH_PATTERNS) {
150
+ expect(typeof pattern).toBe("string");
151
+ expect(pattern.length).toBeGreaterThan(0);
152
+ }
153
+ });
154
+
155
+ test("all entries are valid regex patterns", () => {
156
+ for (const pattern of DANGEROUS_BASH_PATTERNS) {
157
+ expect(() => new RegExp(pattern)).not.toThrow();
158
+ }
159
+ });
160
+
161
+ test("has no duplicate entries", () => {
162
+ const unique = new Set(DANGEROUS_BASH_PATTERNS);
163
+ expect(unique.size).toBe(DANGEROUS_BASH_PATTERNS.length);
164
+ });
165
+
166
+ // Verify key dangerous operations are covered
167
+ test("contains sed -i pattern", () => {
168
+ const pattern = DANGEROUS_BASH_PATTERNS.find((p) => p.includes("sed") && p.includes("-i"));
169
+ expect(pattern).toBeDefined();
170
+ });
171
+
172
+ test("contains echo redirect pattern", () => {
173
+ const pattern = DANGEROUS_BASH_PATTERNS.find((p) => p.includes("echo") && p.includes(">"));
174
+ expect(pattern).toBeDefined();
175
+ });
176
+
177
+ test("contains printf redirect pattern", () => {
178
+ const pattern = DANGEROUS_BASH_PATTERNS.find((p) => p.includes("printf") && p.includes(">"));
179
+ expect(pattern).toBeDefined();
180
+ });
181
+
182
+ test("contains cat redirect pattern", () => {
183
+ const pattern = DANGEROUS_BASH_PATTERNS.find((p) => p.includes("cat") && p.includes(">"));
184
+ expect(pattern).toBeDefined();
185
+ });
186
+
187
+ test("contains tee pattern", () => {
188
+ const pattern = DANGEROUS_BASH_PATTERNS.find((p) => p.includes("tee"));
189
+ expect(pattern).toBeDefined();
190
+ });
191
+
192
+ test("contains rm pattern", () => {
193
+ const pattern = DANGEROUS_BASH_PATTERNS.find((p) => p.includes("rm"));
194
+ expect(pattern).toBeDefined();
195
+ });
196
+
197
+ test("contains mv pattern", () => {
198
+ const pattern = DANGEROUS_BASH_PATTERNS.find((p) => p.includes("mv"));
199
+ expect(pattern).toBeDefined();
200
+ });
201
+
202
+ test("contains cp pattern", () => {
203
+ const pattern = DANGEROUS_BASH_PATTERNS.find((p) => p.includes("cp"));
204
+ expect(pattern).toBeDefined();
205
+ });
206
+
207
+ test("contains mkdir pattern", () => {
208
+ const pattern = DANGEROUS_BASH_PATTERNS.find((p) => p.includes("mkdir"));
209
+ expect(pattern).toBeDefined();
210
+ });
211
+
212
+ test("contains git add pattern", () => {
213
+ const pattern = DANGEROUS_BASH_PATTERNS.find((p) => p.includes("git") && p.includes("add"));
214
+ expect(pattern).toBeDefined();
215
+ });
216
+
217
+ test("contains git commit pattern", () => {
218
+ const pattern = DANGEROUS_BASH_PATTERNS.find((p) => p.includes("git") && p.includes("commit"));
219
+ expect(pattern).toBeDefined();
220
+ });
221
+
222
+ test("contains git push pattern", () => {
223
+ const pattern = DANGEROUS_BASH_PATTERNS.find((p) => p.includes("git") && p.includes("push"));
224
+ expect(pattern).toBeDefined();
225
+ });
226
+
227
+ test("contains git reset pattern", () => {
228
+ const pattern = DANGEROUS_BASH_PATTERNS.find((p) => p.includes("git") && p.includes("reset"));
229
+ expect(pattern).toBeDefined();
230
+ });
231
+
232
+ test("contains npm install pattern", () => {
233
+ const pattern = DANGEROUS_BASH_PATTERNS.find((p) => p.includes("npm") && p.includes("install"));
234
+ expect(pattern).toBeDefined();
235
+ });
236
+
237
+ test("contains bun install pattern", () => {
238
+ const pattern = DANGEROUS_BASH_PATTERNS.find((p) => p.includes("bun") && p.includes("install"));
239
+ expect(pattern).toBeDefined();
240
+ });
241
+
242
+ // Runtime eval bypass patterns
243
+ test("contains bun -e / --eval pattern (runtime eval bypass)", () => {
244
+ const hasEval = DANGEROUS_BASH_PATTERNS.some(
245
+ (p) => p.includes("bun") && (p.includes("-e") || p.includes("eval")),
246
+ );
247
+ expect(hasEval).toBe(true);
248
+ });
249
+
250
+ test("contains node -e / --eval pattern (runtime eval bypass)", () => {
251
+ const hasEval = DANGEROUS_BASH_PATTERNS.some(
252
+ (p) => p.includes("node") && (p.includes("-e") || p.includes("eval")),
253
+ );
254
+ expect(hasEval).toBe(true);
255
+ });
256
+
257
+ test("contains python -c pattern (runtime eval bypass)", () => {
258
+ const hasEval = DANGEROUS_BASH_PATTERNS.some((p) => p.includes("python") && p.includes("-c"));
259
+ expect(hasEval).toBe(true);
260
+ });
261
+
262
+ // Functional: combined pattern matches dangerous commands
263
+ test("combined pattern matches 'sed -i' command", () => {
264
+ const combined = new RegExp(DANGEROUS_BASH_PATTERNS.join("|"));
265
+ expect(combined.test("sed -i 's/foo/bar/' file.txt")).toBe(true);
266
+ });
267
+
268
+ test("combined pattern matches 'echo foo > file' command", () => {
269
+ const combined = new RegExp(DANGEROUS_BASH_PATTERNS.join("|"));
270
+ expect(combined.test("echo foo > file.txt")).toBe(true);
271
+ });
272
+
273
+ test("combined pattern matches 'rm -rf' command", () => {
274
+ const combined = new RegExp(DANGEROUS_BASH_PATTERNS.join("|"));
275
+ expect(combined.test("rm -rf /tmp/foo")).toBe(true);
276
+ });
277
+
278
+ test("combined pattern matches 'git commit' command", () => {
279
+ const combined = new RegExp(DANGEROUS_BASH_PATTERNS.join("|"));
280
+ expect(combined.test("git commit -m 'message'")).toBe(true);
281
+ });
282
+
283
+ test("combined pattern matches 'git push' command", () => {
284
+ const combined = new RegExp(DANGEROUS_BASH_PATTERNS.join("|"));
285
+ expect(combined.test("git push origin main")).toBe(true);
286
+ });
287
+
288
+ test("combined pattern matches 'bun --eval' command", () => {
289
+ const combined = new RegExp(DANGEROUS_BASH_PATTERNS.join("|"));
290
+ expect(combined.test("bun --eval 'console.log(1)'")).toBe(true);
291
+ });
292
+
293
+ test("combined pattern matches 'node -e' command", () => {
294
+ const combined = new RegExp(DANGEROUS_BASH_PATTERNS.join("|"));
295
+ expect(combined.test("node -e 'process.exit(1)'")).toBe(true);
296
+ });
297
+
298
+ test("combined pattern does NOT match safe read commands", () => {
299
+ const combined = new RegExp(DANGEROUS_BASH_PATTERNS.join("|"));
300
+ expect(combined.test("cat README.md")).toBe(false);
301
+ expect(combined.test("grep -r 'foo' src/")).toBe(false);
302
+ expect(combined.test("ls -la")).toBe(false);
303
+ });
304
+ });
305
+
306
+ // ─── SAFE_BASH_PREFIXES ──────────────────────────────────────────────────────
307
+
308
+ describe("SAFE_BASH_PREFIXES", () => {
309
+ test("is a non-empty array", () => {
310
+ expect(Array.isArray(SAFE_BASH_PREFIXES)).toBe(true);
311
+ expect(SAFE_BASH_PREFIXES.length).toBeGreaterThan(0);
312
+ });
313
+
314
+ test("all entries are non-empty strings", () => {
315
+ for (const prefix of SAFE_BASH_PREFIXES) {
316
+ expect(typeof prefix).toBe("string");
317
+ expect(prefix.length).toBeGreaterThan(0);
318
+ }
319
+ });
320
+
321
+ test("has no duplicate entries", () => {
322
+ const unique = new Set(SAFE_BASH_PREFIXES);
323
+ expect(unique.size).toBe(SAFE_BASH_PREFIXES.length);
324
+ });
325
+
326
+ test("includes overstory CLI shorthand 'ov '", () => {
327
+ expect(SAFE_BASH_PREFIXES).toContain("ov ");
328
+ });
329
+
330
+ test("includes overstory CLI full name 'overstory '", () => {
331
+ expect(SAFE_BASH_PREFIXES).toContain("overstory ");
332
+ });
333
+
334
+ test("includes beads CLI 'bd '", () => {
335
+ expect(SAFE_BASH_PREFIXES).toContain("bd ");
336
+ });
337
+
338
+ test("includes seeds CLI 'sd '", () => {
339
+ expect(SAFE_BASH_PREFIXES).toContain("sd ");
340
+ });
341
+
342
+ test("includes mulch CLI 'mulch '", () => {
343
+ expect(SAFE_BASH_PREFIXES).toContain("mulch ");
344
+ });
345
+
346
+ test("includes read-only git commands", () => {
347
+ expect(SAFE_BASH_PREFIXES).toContain("git status");
348
+ expect(SAFE_BASH_PREFIXES).toContain("git log");
349
+ expect(SAFE_BASH_PREFIXES).toContain("git diff");
350
+ });
351
+
352
+ test("does not include destructive git commands as safe prefixes", () => {
353
+ // git push, git reset, git commit should NOT be safe (builders can commit
354
+ // but non-implementation agents should not)
355
+ expect(SAFE_BASH_PREFIXES).not.toContain("git push");
356
+ expect(SAFE_BASH_PREFIXES).not.toContain("git reset");
357
+ });
358
+
359
+ test("safe prefixes match expected commands via startsWith", () => {
360
+ const isSafe = (cmd: string) =>
361
+ SAFE_BASH_PREFIXES.some((prefix) => cmd.trimStart().startsWith(prefix));
362
+
363
+ expect(isSafe("ov mail send --to parent --subject test")).toBe(true);
364
+ expect(isSafe("overstory status")).toBe(true);
365
+ expect(isSafe("sd close overstory-1234")).toBe(true);
366
+ expect(isSafe("bd ready")).toBe(true);
367
+ expect(isSafe("mulch record cli --type convention")).toBe(true);
368
+ expect(isSafe("git status")).toBe(true);
369
+ expect(isSafe("git log --oneline")).toBe(true);
370
+ expect(isSafe("git diff HEAD")).toBe(true);
371
+ });
372
+ });