@os-eco/overstory-cli 0.8.7 → 0.9.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.
- package/README.md +26 -8
- package/agents/coordinator.md +30 -6
- package/agents/lead.md +11 -1
- package/agents/ov-co-creation.md +90 -0
- package/package.json +1 -1
- package/src/agents/hooks-deployer.test.ts +9 -1
- package/src/agents/hooks-deployer.ts +2 -1
- package/src/agents/overlay.test.ts +26 -0
- package/src/agents/overlay.ts +31 -4
- package/src/canopy/client.test.ts +107 -0
- package/src/canopy/client.ts +179 -0
- package/src/commands/agents.ts +1 -1
- package/src/commands/clean.test.ts +3 -0
- package/src/commands/clean.ts +1 -58
- package/src/commands/completions.test.ts +18 -6
- package/src/commands/completions.ts +40 -1
- package/src/commands/coordinator.test.ts +77 -4
- package/src/commands/coordinator.ts +304 -146
- package/src/commands/dashboard.ts +47 -10
- package/src/commands/discover.test.ts +288 -0
- package/src/commands/discover.ts +202 -0
- package/src/commands/doctor.ts +3 -1
- package/src/commands/ecosystem.test.ts +126 -1
- package/src/commands/ecosystem.ts +7 -53
- package/src/commands/feed.test.ts +117 -2
- package/src/commands/feed.ts +46 -30
- package/src/commands/group.test.ts +274 -155
- package/src/commands/group.ts +11 -5
- package/src/commands/init.test.ts +2 -1
- package/src/commands/init.ts +8 -0
- package/src/commands/log.test.ts +35 -0
- package/src/commands/log.ts +10 -6
- package/src/commands/logs.test.ts +423 -1
- package/src/commands/logs.ts +99 -104
- package/src/commands/orchestrator.ts +42 -0
- package/src/commands/prime.test.ts +177 -2
- package/src/commands/prime.ts +4 -2
- package/src/commands/sling.ts +23 -3
- package/src/commands/update.test.ts +1 -0
- package/src/commands/upgrade.test.ts +2 -0
- package/src/commands/upgrade.ts +1 -17
- package/src/commands/watch.test.ts +67 -1
- package/src/commands/watch.ts +13 -88
- package/src/config.test.ts +250 -0
- package/src/config.ts +43 -0
- package/src/doctor/agents.test.ts +72 -5
- package/src/doctor/agents.ts +10 -10
- package/src/doctor/consistency.test.ts +35 -0
- package/src/doctor/consistency.ts +7 -3
- package/src/doctor/dependencies.test.ts +58 -1
- package/src/doctor/dependencies.ts +4 -2
- package/src/doctor/providers.test.ts +41 -5
- package/src/doctor/types.ts +2 -1
- package/src/doctor/version.test.ts +106 -2
- package/src/doctor/version.ts +4 -2
- package/src/doctor/watchdog.test.ts +167 -0
- package/src/doctor/watchdog.ts +158 -0
- package/src/e2e/init-sling-lifecycle.test.ts +4 -2
- package/src/errors.test.ts +350 -0
- package/src/events/tailer.test.ts +25 -0
- package/src/events/tailer.ts +8 -1
- package/src/index.ts +9 -1
- package/src/mail/store.test.ts +110 -0
- package/src/mail/store.ts +2 -1
- package/src/runtimes/aider.test.ts +124 -0
- package/src/runtimes/aider.ts +147 -0
- package/src/runtimes/amp.test.ts +164 -0
- package/src/runtimes/amp.ts +154 -0
- package/src/runtimes/claude.test.ts +4 -2
- package/src/runtimes/goose.test.ts +133 -0
- package/src/runtimes/goose.ts +157 -0
- package/src/runtimes/pi-guards.ts +2 -1
- package/src/runtimes/pi.test.ts +9 -9
- package/src/runtimes/pi.ts +6 -7
- package/src/runtimes/registry.test.ts +1 -1
- package/src/runtimes/registry.ts +13 -4
- package/src/runtimes/sapling.ts +2 -1
- package/src/runtimes/types.ts +2 -2
- package/src/schema-consistency.test.ts +1 -0
- package/src/sessions/store.ts +25 -4
- package/src/types.ts +65 -1
- package/src/utils/bin.test.ts +10 -0
- package/src/utils/bin.ts +37 -0
- package/src/utils/fs.test.ts +119 -0
- package/src/utils/fs.ts +62 -0
- package/src/utils/pid.test.ts +68 -0
- package/src/utils/pid.ts +45 -0
- package/src/utils/time.test.ts +43 -0
- package/src/utils/time.ts +37 -0
- package/src/utils/version.test.ts +33 -0
- package/src/utils/version.ts +70 -0
- package/src/watchdog/daemon.test.ts +255 -1
- package/src/watchdog/daemon.ts +87 -9
- package/src/watchdog/health.test.ts +15 -1
- package/src/watchdog/health.ts +1 -1
- package/src/watchdog/triage.test.ts +49 -9
- package/src/watchdog/triage.ts +21 -5
- package/templates/overlay.md.tmpl +2 -0
|
@@ -59,10 +59,13 @@ const PKG_VERSION: string = JSON.parse(await Bun.file(pkgPath).text()).version ?
|
|
|
59
59
|
* These are not colors, so they stay separate from the color module.
|
|
60
60
|
*/
|
|
61
61
|
const CURSOR = {
|
|
62
|
-
clear: "\x1b[
|
|
62
|
+
clear: "\x1b[H\x1b[J", // Home cursor then clear from cursor to end
|
|
63
|
+
home: "\x1b[H", // Home cursor only (for redraw without full clear)
|
|
63
64
|
cursorTo: (row: number, col: number) => `\x1b[${row};${col}H`,
|
|
64
65
|
hideCursor: "\x1b[?25l",
|
|
65
66
|
showCursor: "\x1b[?25h",
|
|
67
|
+
enterAltScreen: "\x1b[?1049h", // Enter alternate screen buffer
|
|
68
|
+
leaveAltScreen: "\x1b[?1049l", // Leave alternate screen buffer
|
|
66
69
|
} as const;
|
|
67
70
|
|
|
68
71
|
/**
|
|
@@ -960,11 +963,13 @@ function renderMetricsPanel(
|
|
|
960
963
|
/**
|
|
961
964
|
* Render the full dashboard.
|
|
962
965
|
*/
|
|
963
|
-
function renderDashboard(data: DashboardData, interval: number): void {
|
|
966
|
+
function renderDashboard(data: DashboardData, interval: number, isFirstRender: boolean): void {
|
|
964
967
|
const width = process.stdout.columns ?? 100;
|
|
965
968
|
const height = process.stdout.rows ?? 30;
|
|
966
969
|
|
|
967
|
-
|
|
970
|
+
// First render: clear entire alt screen. Subsequent: just home cursor
|
|
971
|
+
// and overwrite in-place (avoids Warp's block-per-clear issue).
|
|
972
|
+
let output = isFirstRender ? CURSOR.clear : CURSOR.home;
|
|
968
973
|
|
|
969
974
|
// Header (rows 1-2)
|
|
970
975
|
output += renderHeader(width, interval, data.currentRunId);
|
|
@@ -1050,20 +1055,44 @@ async function executeDashboard(opts: DashboardOpts): Promise<void> {
|
|
|
1050
1055
|
zombieMs: config.watchdog.zombieThresholdMs,
|
|
1051
1056
|
};
|
|
1052
1057
|
|
|
1053
|
-
//
|
|
1058
|
+
// Enter alternate screen buffer (like vim/htop) + hide cursor + raw stdin
|
|
1059
|
+
process.stdout.write(CURSOR.enterAltScreen);
|
|
1054
1060
|
process.stdout.write(CURSOR.hideCursor);
|
|
1061
|
+
if (process.stdin.isTTY) {
|
|
1062
|
+
process.stdin.setRawMode(true);
|
|
1063
|
+
process.stdin.resume();
|
|
1064
|
+
}
|
|
1055
1065
|
|
|
1056
|
-
// Clean exit on Ctrl+C
|
|
1066
|
+
// Clean exit on Ctrl+C or 'q': restore original screen
|
|
1057
1067
|
let running = true;
|
|
1058
|
-
|
|
1068
|
+
const cleanup = () => {
|
|
1059
1069
|
running = false;
|
|
1070
|
+
if (process.stdin.isTTY) {
|
|
1071
|
+
process.stdin.setRawMode(false);
|
|
1072
|
+
process.stdin.pause();
|
|
1073
|
+
}
|
|
1060
1074
|
closeDashboardStores(stores);
|
|
1061
1075
|
process.stdout.write(CURSOR.showCursor);
|
|
1062
|
-
process.stdout.write(CURSOR.
|
|
1063
|
-
|
|
1076
|
+
process.stdout.write(CURSOR.leaveAltScreen);
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1079
|
+
process.on("SIGINT", () => {
|
|
1080
|
+
cleanup();
|
|
1081
|
+
process.exitCode = 0;
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
// Allow 'q' to quit the dashboard
|
|
1085
|
+
process.stdin.on("data", (data: Buffer) => {
|
|
1086
|
+
const key = data.toString();
|
|
1087
|
+
if (key === "q" || key === "\x03") {
|
|
1088
|
+
// 'q' or Ctrl+C
|
|
1089
|
+
cleanup();
|
|
1090
|
+
process.exitCode = 0;
|
|
1091
|
+
}
|
|
1064
1092
|
});
|
|
1065
1093
|
|
|
1066
1094
|
// Poll loop — errors are caught per-tick so transient DB failures never crash the dashboard.
|
|
1095
|
+
let isFirstRender = true;
|
|
1067
1096
|
let lastGoodData: DashboardData | null = null;
|
|
1068
1097
|
let lastErrorMsg: string | null = null;
|
|
1069
1098
|
while (running) {
|
|
@@ -1077,12 +1106,20 @@ async function executeDashboard(opts: DashboardOpts): Promise<void> {
|
|
|
1077
1106
|
config.runtime,
|
|
1078
1107
|
);
|
|
1079
1108
|
lastGoodData = data;
|
|
1109
|
+
// If recovering from an error, clear the stale error line at the bottom
|
|
1110
|
+
if (lastErrorMsg !== null) {
|
|
1111
|
+
const w = process.stdout.columns ?? 100;
|
|
1112
|
+
const h = process.stdout.rows ?? 30;
|
|
1113
|
+
process.stdout.write(`${CURSOR.cursorTo(h, 1)}${" ".repeat(w)}`);
|
|
1114
|
+
}
|
|
1080
1115
|
lastErrorMsg = null;
|
|
1081
|
-
renderDashboard(data, interval);
|
|
1116
|
+
renderDashboard(data, interval, isFirstRender);
|
|
1117
|
+
isFirstRender = false;
|
|
1082
1118
|
} catch (err) {
|
|
1083
1119
|
// Render last good frame so the TUI stays alive, then show the error inline.
|
|
1084
1120
|
if (lastGoodData) {
|
|
1085
|
-
renderDashboard(lastGoodData, interval);
|
|
1121
|
+
renderDashboard(lastGoodData, interval, isFirstRender);
|
|
1122
|
+
isFirstRender = false;
|
|
1086
1123
|
}
|
|
1087
1124
|
lastErrorMsg = err instanceof Error ? err.message : String(err);
|
|
1088
1125
|
const w = process.stdout.columns ?? 100;
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for ov discover command.
|
|
3
|
+
*
|
|
4
|
+
* Tests cover the pure functions and command structure.
|
|
5
|
+
* The coordinator session startup is not tested here (requires tmux and
|
|
6
|
+
* external processes). Use dependency injection via DiscoverDeps to test
|
|
7
|
+
* that discoverCommand() delegates correctly.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, test } from "bun:test";
|
|
11
|
+
import { ValidationError } from "../errors.ts";
|
|
12
|
+
import type { CoordinatorSessionOptions } from "./coordinator.ts";
|
|
13
|
+
import {
|
|
14
|
+
buildDiscoveryBeacon,
|
|
15
|
+
buildScoutArgs,
|
|
16
|
+
createDiscoverCommand,
|
|
17
|
+
DISCOVERY_CATEGORIES,
|
|
18
|
+
type DiscoverDeps,
|
|
19
|
+
discoverCommand,
|
|
20
|
+
VALID_CATEGORY_NAMES,
|
|
21
|
+
} from "./discover.ts";
|
|
22
|
+
|
|
23
|
+
describe("DISCOVERY_CATEGORIES", () => {
|
|
24
|
+
test("has exactly 6 categories", () => {
|
|
25
|
+
expect(DISCOVERY_CATEGORIES).toHaveLength(6);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("each category has name, subject, and body", () => {
|
|
29
|
+
for (const category of DISCOVERY_CATEGORIES) {
|
|
30
|
+
expect(category.name).toBeTruthy();
|
|
31
|
+
expect(category.subject).toBeTruthy();
|
|
32
|
+
expect(category.body).toBeTruthy();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("contains all expected category names", () => {
|
|
37
|
+
const names = DISCOVERY_CATEGORIES.map((c) => c.name);
|
|
38
|
+
expect(names).toContain("architecture");
|
|
39
|
+
expect(names).toContain("dependencies");
|
|
40
|
+
expect(names).toContain("testing");
|
|
41
|
+
expect(names).toContain("apis");
|
|
42
|
+
expect(names).toContain("config");
|
|
43
|
+
expect(names).toContain("implicit");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("VALID_CATEGORY_NAMES", () => {
|
|
48
|
+
test("contains all 6 category names", () => {
|
|
49
|
+
expect(VALID_CATEGORY_NAMES.size).toBe(6);
|
|
50
|
+
expect(VALID_CATEGORY_NAMES.has("architecture")).toBe(true);
|
|
51
|
+
expect(VALID_CATEGORY_NAMES.has("dependencies")).toBe(true);
|
|
52
|
+
expect(VALID_CATEGORY_NAMES.has("testing")).toBe(true);
|
|
53
|
+
expect(VALID_CATEGORY_NAMES.has("apis")).toBe(true);
|
|
54
|
+
expect(VALID_CATEGORY_NAMES.has("config")).toBe(true);
|
|
55
|
+
expect(VALID_CATEGORY_NAMES.has("implicit")).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("does not contain invalid category names", () => {
|
|
59
|
+
expect(VALID_CATEGORY_NAMES.has("unknown")).toBe(false);
|
|
60
|
+
expect(VALID_CATEGORY_NAMES.has("")).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("buildScoutArgs()", () => {
|
|
65
|
+
test("returns correct args array for a category", () => {
|
|
66
|
+
const category = DISCOVERY_CATEGORIES[0];
|
|
67
|
+
if (!category) throw new Error("DISCOVERY_CATEGORIES is empty");
|
|
68
|
+
const args = buildScoutArgs(category, "task-123", "discover-coordinator");
|
|
69
|
+
expect(args).toContain("ov");
|
|
70
|
+
expect(args).toContain("sling");
|
|
71
|
+
expect(args).toContain("task-123");
|
|
72
|
+
expect(args).toContain("--capability");
|
|
73
|
+
expect(args).toContain("scout");
|
|
74
|
+
expect(args).toContain("--name");
|
|
75
|
+
expect(args).toContain(`discover-${category.name}`);
|
|
76
|
+
expect(args).toContain("--profile");
|
|
77
|
+
expect(args).toContain("ov-discovery");
|
|
78
|
+
expect(args).toContain("--parent");
|
|
79
|
+
expect(args).toContain("discover-coordinator");
|
|
80
|
+
expect(args).toContain("--skip-task-check");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("buildDiscoveryBeacon()", () => {
|
|
85
|
+
test("includes coordinator name", () => {
|
|
86
|
+
const beacon = buildDiscoveryBeacon(DISCOVERY_CATEGORIES, "discover-coordinator");
|
|
87
|
+
expect(beacon).toContain("discover-coordinator");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("includes all active category names", () => {
|
|
91
|
+
const beacon = buildDiscoveryBeacon(DISCOVERY_CATEGORIES, "discover-coordinator");
|
|
92
|
+
for (const cat of DISCOVERY_CATEGORIES) {
|
|
93
|
+
expect(beacon).toContain(cat.name);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("includes timestamp marker", () => {
|
|
98
|
+
const beacon = buildDiscoveryBeacon(DISCOVERY_CATEGORIES, "discover-coordinator");
|
|
99
|
+
expect(beacon).toContain("[OVERSTORY]");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("excludes skipped categories", () => {
|
|
103
|
+
const active = DISCOVERY_CATEGORIES.filter((c) => c.name !== "testing");
|
|
104
|
+
const beacon = buildDiscoveryBeacon(active, "discover-coordinator");
|
|
105
|
+
// All active categories present
|
|
106
|
+
for (const cat of active) {
|
|
107
|
+
expect(beacon).toContain(cat.name);
|
|
108
|
+
}
|
|
109
|
+
// The skipped category body text should not appear as a standalone discovery target
|
|
110
|
+
// (the name "testing" may appear inside other category descriptions, so check body)
|
|
111
|
+
const testingCat = DISCOVERY_CATEGORIES.find((c) => c.name === "testing");
|
|
112
|
+
if (!testingCat) throw new Error("testing category not found");
|
|
113
|
+
expect(beacon).not.toContain(testingCat.body);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("includes startup instructions", () => {
|
|
117
|
+
const beacon = buildDiscoveryBeacon(DISCOVERY_CATEGORIES, "discover-coordinator");
|
|
118
|
+
expect(beacon).toContain("mulch prime");
|
|
119
|
+
expect(beacon).toContain("spawn one lead per");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("createDiscoverCommand()", () => {
|
|
124
|
+
test("returns a Command with name 'discover'", () => {
|
|
125
|
+
const cmd = createDiscoverCommand();
|
|
126
|
+
expect(cmd.name()).toBe("discover");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("has --skip option", () => {
|
|
130
|
+
const cmd = createDiscoverCommand();
|
|
131
|
+
const option = cmd.options.find((o) => o.long === "--skip");
|
|
132
|
+
expect(option).toBeDefined();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("has --name option", () => {
|
|
136
|
+
const cmd = createDiscoverCommand();
|
|
137
|
+
const option = cmd.options.find((o) => o.long === "--name");
|
|
138
|
+
expect(option).toBeDefined();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("has --task-id option", () => {
|
|
142
|
+
const cmd = createDiscoverCommand();
|
|
143
|
+
const option = cmd.options.find((o) => o.long === "--task-id");
|
|
144
|
+
expect(option).toBeDefined();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("has --json option", () => {
|
|
148
|
+
const cmd = createDiscoverCommand();
|
|
149
|
+
const option = cmd.options.find((o) => o.long === "--json");
|
|
150
|
+
expect(option).toBeDefined();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("has --attach option", () => {
|
|
154
|
+
const cmd = createDiscoverCommand();
|
|
155
|
+
const option = cmd.options.find((o) => o.long === "--attach");
|
|
156
|
+
expect(option).toBeDefined();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("has --watchdog option", () => {
|
|
160
|
+
const cmd = createDiscoverCommand();
|
|
161
|
+
const option = cmd.options.find((o) => o.long === "--watchdog");
|
|
162
|
+
expect(option).toBeDefined();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("has a description", () => {
|
|
166
|
+
const cmd = createDiscoverCommand();
|
|
167
|
+
expect(cmd.description()).toBeTruthy();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("discoverCommand() skip validation", () => {
|
|
172
|
+
test("throws ValidationError for invalid category name", async () => {
|
|
173
|
+
await expect(discoverCommand({ skip: "notacategory" })).rejects.toThrow(ValidationError);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("throws ValidationError for mixed valid and invalid categories", async () => {
|
|
177
|
+
await expect(discoverCommand({ skip: "architecture,notacategory" })).rejects.toThrow(
|
|
178
|
+
ValidationError,
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("throws ValidationError when all categories are skipped", async () => {
|
|
183
|
+
const allCategories = DISCOVERY_CATEGORIES.map((c) => c.name).join(",");
|
|
184
|
+
await expect(discoverCommand({ skip: allCategories })).rejects.toThrow(ValidationError);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("throws ValidationError with helpful message for invalid category", async () => {
|
|
188
|
+
let thrownError: unknown;
|
|
189
|
+
try {
|
|
190
|
+
await discoverCommand({ skip: "badcategory" });
|
|
191
|
+
} catch (err) {
|
|
192
|
+
thrownError = err;
|
|
193
|
+
}
|
|
194
|
+
expect(thrownError).toBeInstanceOf(ValidationError);
|
|
195
|
+
const ve = thrownError as ValidationError;
|
|
196
|
+
expect(ve.message).toContain("badcategory");
|
|
197
|
+
expect(ve.message).toContain("Valid categories");
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("discoverCommand() delegation", () => {
|
|
202
|
+
test("calls startCoordinatorSession with ov-discovery profile", async () => {
|
|
203
|
+
let capturedOpts: CoordinatorSessionOptions | undefined;
|
|
204
|
+
const deps: DiscoverDeps = {
|
|
205
|
+
_startCoordinatorSession: async (opts) => {
|
|
206
|
+
capturedOpts = opts;
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
await discoverCommand({ attach: false }, deps);
|
|
211
|
+
|
|
212
|
+
expect(capturedOpts).toBeDefined();
|
|
213
|
+
expect(capturedOpts?.profile).toBe("ov-discovery");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("uses default coordinator name 'discover-coordinator'", async () => {
|
|
217
|
+
let capturedOpts: CoordinatorSessionOptions | undefined;
|
|
218
|
+
const deps: DiscoverDeps = {
|
|
219
|
+
_startCoordinatorSession: async (opts) => {
|
|
220
|
+
capturedOpts = opts;
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
await discoverCommand({ attach: false }, deps);
|
|
225
|
+
|
|
226
|
+
expect(capturedOpts?.coordinatorName).toBe("discover-coordinator");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("uses custom name when provided", async () => {
|
|
230
|
+
let capturedOpts: CoordinatorSessionOptions | undefined;
|
|
231
|
+
const deps: DiscoverDeps = {
|
|
232
|
+
_startCoordinatorSession: async (opts) => {
|
|
233
|
+
capturedOpts = opts;
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
await discoverCommand({ name: "my-discover", attach: false }, deps);
|
|
238
|
+
|
|
239
|
+
expect(capturedOpts?.coordinatorName).toBe("my-discover");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("beacon builder returns string containing active category names", async () => {
|
|
243
|
+
let capturedOpts: CoordinatorSessionOptions | undefined;
|
|
244
|
+
const deps: DiscoverDeps = {
|
|
245
|
+
_startCoordinatorSession: async (opts) => {
|
|
246
|
+
capturedOpts = opts;
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
await discoverCommand({ skip: "testing,config,implicit", attach: false }, deps);
|
|
251
|
+
|
|
252
|
+
expect(capturedOpts?.beaconBuilder).toBeDefined();
|
|
253
|
+
const beacon = capturedOpts?.beaconBuilder?.("bd") ?? "";
|
|
254
|
+
expect(beacon).toContain("architecture");
|
|
255
|
+
expect(beacon).toContain("dependencies");
|
|
256
|
+
expect(beacon).toContain("apis");
|
|
257
|
+
// Skipped categories should not appear as category targets in the beacon
|
|
258
|
+
const testingCat = DISCOVERY_CATEGORIES.find((c) => c.name === "testing");
|
|
259
|
+
if (!testingCat) throw new Error("testing category not found");
|
|
260
|
+
expect(beacon).not.toContain(testingCat.body);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("sets monitor: false", async () => {
|
|
264
|
+
let capturedOpts: CoordinatorSessionOptions | undefined;
|
|
265
|
+
const deps: DiscoverDeps = {
|
|
266
|
+
_startCoordinatorSession: async (opts) => {
|
|
267
|
+
capturedOpts = opts;
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
await discoverCommand({ attach: false }, deps);
|
|
272
|
+
|
|
273
|
+
expect(capturedOpts?.monitor).toBe(false);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("forwards watchdog option", async () => {
|
|
277
|
+
let capturedOpts: CoordinatorSessionOptions | undefined;
|
|
278
|
+
const deps: DiscoverDeps = {
|
|
279
|
+
_startCoordinatorSession: async (opts) => {
|
|
280
|
+
capturedOpts = opts;
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
await discoverCommand({ watchdog: true, attach: false }, deps);
|
|
285
|
+
|
|
286
|
+
expect(capturedOpts?.watchdog).toBe(true);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: ov discover
|
|
3
|
+
*
|
|
4
|
+
* Launches a coordinator session with the ov-discovery profile to explore a
|
|
5
|
+
* brownfield codebase and produce structured mulch records. The coordinator
|
|
6
|
+
* autonomously spawns leads, which spawn scouts per category, synthesizes
|
|
7
|
+
* results, and writes mulch records.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
import { ValidationError } from "../errors.ts";
|
|
12
|
+
import type { CoordinatorDeps, CoordinatorSessionOptions } from "./coordinator.ts";
|
|
13
|
+
import { startCoordinatorSession } from "./coordinator.ts";
|
|
14
|
+
|
|
15
|
+
/** A single discovery category with its research focus. */
|
|
16
|
+
export interface DiscoveryCategory {
|
|
17
|
+
name: string;
|
|
18
|
+
subject: string;
|
|
19
|
+
body: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** All discovery categories that scouts will explore. */
|
|
23
|
+
export const DISCOVERY_CATEGORIES: DiscoveryCategory[] = [
|
|
24
|
+
{
|
|
25
|
+
name: "architecture",
|
|
26
|
+
subject: "Discover: architecture",
|
|
27
|
+
body: "Explore directory structure, module boundaries, layering conventions, and design patterns. Identify the core architectural style (monolith, layered, hexagonal, etc.), note major subsystems and their relationships, and document any implicit layering rules or boundary conventions.",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "dependencies",
|
|
31
|
+
subject: "Discover: dependencies",
|
|
32
|
+
body: "Catalog all npm packages, CLI tool dependencies, and version constraints. Identify runtime vs dev dependencies, note any unusual or pinned versions, and flag deprecated or potentially problematic packages. Document any external CLIs invoked as subprocesses.",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "testing",
|
|
36
|
+
subject: "Discover: testing",
|
|
37
|
+
body: "Map the test framework, file locations, mock strategy, and coverage gaps. Identify what test runner is used, where tests live relative to source, what mocking patterns are used (and why), and which subsystems lack adequate test coverage.",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "apis",
|
|
41
|
+
subject: "Discover: apis",
|
|
42
|
+
body: "Document exported functions and types, interfaces, error handling patterns, and CLI structure. Identify the public API surface, note how errors are typed and propagated, and document any conventions around return types or async patterns.",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "config",
|
|
46
|
+
subject: "Discover: config",
|
|
47
|
+
body: "Catalog config file formats, environment variables, loading and validation patterns, and default values. Note how configuration is structured (YAML, JSON, env), how it's validated at runtime, and what the expected defaults are.",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "implicit",
|
|
51
|
+
subject: "Discover: implicit",
|
|
52
|
+
body: "Surface naming conventions, error handling style, TODOs, and unwritten rules. Look for patterns in variable naming, file naming, comment style, and any informal conventions that aren't documented. Note recurring TODOs or FIXMEs that indicate known debt.",
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/** Set of valid category names for validation. */
|
|
57
|
+
export const VALID_CATEGORY_NAMES: ReadonlySet<string> = new Set(
|
|
58
|
+
DISCOVERY_CATEGORIES.map((c) => c.name),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
export interface DiscoverOptions {
|
|
62
|
+
skip?: string;
|
|
63
|
+
name?: string;
|
|
64
|
+
taskId?: string;
|
|
65
|
+
attach?: boolean;
|
|
66
|
+
watchdog?: boolean;
|
|
67
|
+
json?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Dependency injection for discoverCommand. Used in tests. */
|
|
71
|
+
export interface DiscoverDeps {
|
|
72
|
+
_startCoordinatorSession?: (
|
|
73
|
+
opts: CoordinatorSessionOptions,
|
|
74
|
+
deps: CoordinatorDeps,
|
|
75
|
+
) => Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Parse and validate the --skip option, returning a set of category names to skip. */
|
|
79
|
+
function parseSkipCategories(skip: string | undefined): Set<string> {
|
|
80
|
+
if (!skip) return new Set();
|
|
81
|
+
const names = skip
|
|
82
|
+
.split(",")
|
|
83
|
+
.map((s) => s.trim())
|
|
84
|
+
.filter((s) => s.length > 0);
|
|
85
|
+
const invalid = names.filter((n) => !VALID_CATEGORY_NAMES.has(n));
|
|
86
|
+
if (invalid.length > 0) {
|
|
87
|
+
throw new ValidationError(
|
|
88
|
+
`Invalid category name(s): ${invalid.join(", ")}. Valid categories: ${[...VALID_CATEGORY_NAMES].join(", ")}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return new Set(names);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Build the discovery beacon — the initial prompt sent to the discover coordinator
|
|
96
|
+
* after Claude Code initializes. Instructs it to spawn one lead per category.
|
|
97
|
+
*/
|
|
98
|
+
export function buildDiscoveryBeacon(
|
|
99
|
+
categories: DiscoveryCategory[],
|
|
100
|
+
coordinatorName: string,
|
|
101
|
+
): string {
|
|
102
|
+
const timestamp = new Date().toISOString();
|
|
103
|
+
const categoryNames = categories.map((c) => c.name).join(", ");
|
|
104
|
+
const categoryDetails = categories.map((c) => `${c.name}: ${c.body}`).join(" | ");
|
|
105
|
+
const parts = [
|
|
106
|
+
`[OVERSTORY] ${coordinatorName} (coordinator) ${timestamp}`,
|
|
107
|
+
`Role: discovery coordinator | Categories: ${categoryNames}`,
|
|
108
|
+
`Startup: run mulch prime, then spawn one lead per active category. Each lead spawns a scout to explore its category area. After all scouts report back, synthesize findings into mulch records.`,
|
|
109
|
+
`Categories: ${categoryDetails}`,
|
|
110
|
+
];
|
|
111
|
+
return parts.join(" — ");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Build the scout args for a given discovery category and task ID.
|
|
116
|
+
* Kept for reference and for callers that need per-category sling arguments.
|
|
117
|
+
*/
|
|
118
|
+
export function buildScoutArgs(
|
|
119
|
+
category: DiscoveryCategory,
|
|
120
|
+
taskId: string,
|
|
121
|
+
parentName: string,
|
|
122
|
+
): string[] {
|
|
123
|
+
return [
|
|
124
|
+
"ov",
|
|
125
|
+
"sling",
|
|
126
|
+
taskId,
|
|
127
|
+
"--capability",
|
|
128
|
+
"scout",
|
|
129
|
+
"--name",
|
|
130
|
+
`discover-${category.name}`,
|
|
131
|
+
"--profile",
|
|
132
|
+
"ov-discovery",
|
|
133
|
+
"--parent",
|
|
134
|
+
parentName,
|
|
135
|
+
"--depth",
|
|
136
|
+
"1",
|
|
137
|
+
"--skip-task-check",
|
|
138
|
+
];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Main handler for ov discover. */
|
|
142
|
+
export async function discoverCommand(
|
|
143
|
+
opts: DiscoverOptions,
|
|
144
|
+
deps: DiscoverDeps = {},
|
|
145
|
+
): Promise<void> {
|
|
146
|
+
const json = opts.json ?? false;
|
|
147
|
+
const coordinatorName = opts.name ?? "discover-coordinator";
|
|
148
|
+
|
|
149
|
+
// Validate and parse skip list
|
|
150
|
+
const skipSet = parseSkipCategories(opts.skip);
|
|
151
|
+
const categories = DISCOVERY_CATEGORIES.filter((c) => !skipSet.has(c.name));
|
|
152
|
+
|
|
153
|
+
if (categories.length === 0) {
|
|
154
|
+
throw new ValidationError("All categories skipped — nothing to discover.");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const attach = opts.attach !== undefined ? opts.attach : !!process.stdout.isTTY;
|
|
158
|
+
|
|
159
|
+
const startSession = deps._startCoordinatorSession ?? startCoordinatorSession;
|
|
160
|
+
|
|
161
|
+
await startSession(
|
|
162
|
+
{
|
|
163
|
+
json,
|
|
164
|
+
attach,
|
|
165
|
+
watchdog: opts.watchdog ?? false,
|
|
166
|
+
monitor: false,
|
|
167
|
+
profile: "ov-discovery",
|
|
168
|
+
coordinatorName,
|
|
169
|
+
beaconBuilder: (_trackerCli) => buildDiscoveryBeacon(categories, coordinatorName),
|
|
170
|
+
},
|
|
171
|
+
{},
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Commander command factory. */
|
|
176
|
+
export function createDiscoverCommand(): Command {
|
|
177
|
+
return new Command("discover")
|
|
178
|
+
.description("Discover a brownfield codebase via coordinator-driven scout swarm")
|
|
179
|
+
.option(
|
|
180
|
+
"--skip <categories>",
|
|
181
|
+
"Skip specific categories (comma-separated: architecture,dependencies,testing,apis,config,implicit)",
|
|
182
|
+
)
|
|
183
|
+
.option("--name <name>", "Coordinator agent name (default: discover-coordinator)")
|
|
184
|
+
.option("--task-id <id>", "Task ID (unused — kept for backward compatibility)")
|
|
185
|
+
.option("--attach", "Always attach to tmux session after start")
|
|
186
|
+
.option("--no-attach", "Never attach to tmux session after start")
|
|
187
|
+
.option("--watchdog", "Auto-start watchdog daemon with coordinator")
|
|
188
|
+
.option("--json", "Output as JSON")
|
|
189
|
+
.action(
|
|
190
|
+
async (opts: {
|
|
191
|
+
skip?: string;
|
|
192
|
+
name?: string;
|
|
193
|
+
taskId?: string;
|
|
194
|
+
attach?: boolean;
|
|
195
|
+
watchdog?: boolean;
|
|
196
|
+
json?: boolean;
|
|
197
|
+
}) => {
|
|
198
|
+
const attach = opts.attach !== undefined ? opts.attach : !!process.stdout.isTTY;
|
|
199
|
+
await discoverCommand({ ...opts, attach });
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
}
|
package/src/commands/doctor.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { checkProviders } from "../doctor/providers.ts";
|
|
|
19
19
|
import { checkStructure } from "../doctor/structure.ts";
|
|
20
20
|
import type { DoctorCategory, DoctorCheck, DoctorCheckFn } from "../doctor/types.ts";
|
|
21
21
|
import { checkVersion } from "../doctor/version.ts";
|
|
22
|
+
import { checkWatchdog } from "../doctor/watchdog.ts";
|
|
22
23
|
import { ValidationError } from "../errors.ts";
|
|
23
24
|
import { jsonOutput } from "../json.ts";
|
|
24
25
|
import { color } from "../logging/color.ts";
|
|
@@ -37,6 +38,7 @@ const ALL_CHECKS: Array<{ category: DoctorCategory; fn: DoctorCheckFn }> = [
|
|
|
37
38
|
{ category: "version", fn: checkVersion },
|
|
38
39
|
{ category: "ecosystem", fn: checkEcosystem },
|
|
39
40
|
{ category: "providers", fn: checkProviders },
|
|
41
|
+
{ category: "watchdog", fn: checkWatchdog },
|
|
40
42
|
];
|
|
41
43
|
|
|
42
44
|
/**
|
|
@@ -168,7 +170,7 @@ export function createDoctorCommand(options?: DoctorCommandOptions): Command {
|
|
|
168
170
|
.option("--fix", "Attempt to auto-fix issues")
|
|
169
171
|
.addHelpText(
|
|
170
172
|
"after",
|
|
171
|
-
"\nCategories: dependencies, structure, config, databases, consistency, agents, merge, logs, version, ecosystem, providers",
|
|
173
|
+
"\nCategories: dependencies, structure, config, databases, consistency, agents, merge, logs, version, ecosystem, providers, watchdog",
|
|
172
174
|
)
|
|
173
175
|
.action(
|
|
174
176
|
async (opts: { json?: boolean; verbose?: boolean; category?: string; fix?: boolean }) => {
|