@os-eco/overstory-cli 0.6.1 → 0.6.5
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 +8 -7
- package/package.json +12 -4
- package/src/agents/checkpoint.test.ts +2 -2
- package/src/agents/hooks-deployer.test.ts +131 -16
- package/src/agents/hooks-deployer.ts +33 -1
- package/src/agents/identity.test.ts +27 -27
- package/src/agents/identity.ts +10 -10
- package/src/agents/lifecycle.test.ts +6 -6
- package/src/agents/lifecycle.ts +2 -2
- package/src/agents/manifest.test.ts +86 -0
- package/src/agents/overlay.test.ts +9 -9
- package/src/agents/overlay.ts +4 -4
- package/src/commands/agents.test.ts +8 -8
- package/src/commands/agents.ts +62 -91
- package/src/commands/clean.test.ts +36 -51
- package/src/commands/clean.ts +28 -49
- package/src/commands/completions.ts +14 -0
- package/src/commands/coordinator.test.ts +133 -26
- package/src/commands/coordinator.ts +101 -64
- package/src/commands/costs.test.ts +47 -47
- package/src/commands/costs.ts +96 -75
- package/src/commands/dashboard.test.ts +2 -2
- package/src/commands/dashboard.ts +75 -95
- package/src/commands/doctor.test.ts +2 -2
- package/src/commands/doctor.ts +92 -79
- package/src/commands/errors.test.ts +2 -2
- package/src/commands/errors.ts +56 -50
- package/src/commands/feed.test.ts +2 -2
- package/src/commands/feed.ts +86 -83
- package/src/commands/group.ts +167 -177
- package/src/commands/hooks.test.ts +2 -2
- package/src/commands/hooks.ts +52 -42
- package/src/commands/init.test.ts +19 -19
- package/src/commands/init.ts +7 -16
- package/src/commands/inspect.test.ts +18 -18
- package/src/commands/inspect.ts +55 -58
- package/src/commands/log.test.ts +26 -31
- package/src/commands/log.ts +97 -91
- package/src/commands/logs.test.ts +1 -1
- package/src/commands/logs.ts +101 -104
- package/src/commands/mail.test.ts +5 -5
- package/src/commands/mail.ts +157 -169
- package/src/commands/merge.test.ts +28 -66
- package/src/commands/merge.ts +21 -51
- package/src/commands/metrics.test.ts +8 -8
- package/src/commands/metrics.ts +34 -35
- package/src/commands/monitor.test.ts +3 -3
- package/src/commands/monitor.ts +57 -62
- package/src/commands/nudge.test.ts +1 -1
- package/src/commands/nudge.ts +41 -89
- package/src/commands/prime.test.ts +19 -51
- package/src/commands/prime.ts +13 -50
- package/src/commands/replay.test.ts +2 -2
- package/src/commands/replay.ts +79 -86
- package/src/commands/run.test.ts +1 -1
- package/src/commands/run.ts +97 -77
- package/src/commands/sling.test.ts +201 -5
- package/src/commands/sling.ts +37 -64
- package/src/commands/spec.test.ts +14 -40
- package/src/commands/spec.ts +32 -101
- package/src/commands/status.test.ts +97 -1
- package/src/commands/status.ts +63 -58
- package/src/commands/stop.test.ts +22 -40
- package/src/commands/stop.ts +18 -33
- package/src/commands/supervisor.test.ts +12 -14
- package/src/commands/supervisor.ts +144 -165
- package/src/commands/trace.test.ts +15 -15
- package/src/commands/trace.ts +59 -82
- package/src/commands/watch.test.ts +2 -2
- package/src/commands/watch.ts +38 -45
- package/src/commands/worktree.test.ts +213 -37
- package/src/commands/worktree.ts +110 -55
- package/src/config.test.ts +96 -0
- package/src/doctor/consistency.test.ts +14 -14
- package/src/doctor/databases.test.ts +22 -2
- package/src/doctor/databases.ts +16 -0
- package/src/doctor/dependencies.test.ts +55 -1
- package/src/doctor/dependencies.ts +113 -18
- package/src/doctor/merge-queue.test.ts +4 -4
- package/src/e2e/init-sling-lifecycle.test.ts +8 -8
- package/src/errors.ts +1 -1
- package/src/index.ts +223 -213
- package/src/logging/color.test.ts +74 -91
- package/src/logging/color.ts +52 -46
- package/src/logging/reporter.test.ts +10 -10
- package/src/logging/reporter.ts +6 -5
- package/src/mail/broadcast.test.ts +1 -1
- package/src/mail/client.test.ts +6 -6
- package/src/mail/store.test.ts +3 -3
- package/src/merge/queue.test.ts +73 -7
- package/src/merge/queue.ts +17 -2
- package/src/merge/resolver.test.ts +159 -7
- package/src/merge/resolver.ts +46 -2
- package/src/metrics/store.test.ts +44 -44
- package/src/metrics/store.ts +2 -2
- package/src/metrics/summary.test.ts +35 -35
- package/src/mulch/client.test.ts +1 -1
- package/src/schema-consistency.test.ts +239 -0
- package/src/sessions/compat.test.ts +3 -3
- package/src/sessions/compat.ts +2 -2
- package/src/sessions/store.test.ts +41 -4
- package/src/sessions/store.ts +13 -2
- package/src/types.ts +14 -14
- package/src/watchdog/daemon.test.ts +10 -10
- package/src/watchdog/daemon.ts +1 -1
- package/src/watchdog/health.test.ts +1 -1
- package/src/worktree/manager.test.ts +20 -20
- package/src/worktree/manager.ts +120 -4
- package/src/worktree/tmux.test.ts +98 -9
- package/src/worktree/tmux.ts +18 -0
package/src/commands/merge.ts
CHANGED
|
@@ -19,26 +19,17 @@ import { createMergeResolver } from "../merge/resolver.ts";
|
|
|
19
19
|
import { createMulchClient } from "../mulch/client.ts";
|
|
20
20
|
import type { MergeEntry, MergeResult } from "../types.ts";
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (idx === -1 || idx + 1 >= args.length) {
|
|
29
|
-
return undefined;
|
|
30
|
-
}
|
|
31
|
-
return args[idx + 1];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Check if a boolean flag is present in the args. */
|
|
35
|
-
function hasFlag(args: string[], flag: string): boolean {
|
|
36
|
-
return args.includes(flag);
|
|
22
|
+
export interface MergeOptions {
|
|
23
|
+
branch?: string;
|
|
24
|
+
all?: boolean;
|
|
25
|
+
into?: string;
|
|
26
|
+
dryRun?: boolean;
|
|
27
|
+
json?: boolean;
|
|
37
28
|
}
|
|
38
29
|
|
|
39
30
|
/**
|
|
40
31
|
* Extract agent name from a branch following the overstory naming convention.
|
|
41
|
-
* Pattern: overstory/{agentName}/{
|
|
32
|
+
* Pattern: overstory/{agentName}/{taskId}
|
|
42
33
|
* Falls back to "unknown" if the pattern does not match.
|
|
43
34
|
*/
|
|
44
35
|
function parseAgentName(branchName: string): string {
|
|
@@ -50,8 +41,8 @@ function parseAgentName(branchName: string): string {
|
|
|
50
41
|
}
|
|
51
42
|
|
|
52
43
|
/**
|
|
53
|
-
* Extract
|
|
54
|
-
* Pattern: overstory/{agentName}/{
|
|
44
|
+
* Extract task ID from a branch following the overstory naming convention.
|
|
45
|
+
* Pattern: overstory/{agentName}/{taskId}
|
|
55
46
|
* Falls back to "unknown" if the pattern does not match.
|
|
56
47
|
*/
|
|
57
48
|
function parseBeadId(branchName: string): string {
|
|
@@ -98,7 +89,7 @@ function formatResult(result: MergeResult): string {
|
|
|
98
89
|
const statusIcon = result.success ? "Merged" : "Failed";
|
|
99
90
|
const lines: string[] = [
|
|
100
91
|
`Merging branch: ${result.entry.branchName}`,
|
|
101
|
-
` Agent: ${result.entry.agentName} | Task: ${result.entry.
|
|
92
|
+
` Agent: ${result.entry.agentName} | Task: ${result.entry.taskId}`,
|
|
102
93
|
` Files: ${result.entry.filesModified.length} modified`,
|
|
103
94
|
` Result: ${statusIcon} (tier: ${result.tier})`,
|
|
104
95
|
];
|
|
@@ -118,7 +109,7 @@ function formatResult(result: MergeResult): string {
|
|
|
118
109
|
function formatDryRun(entry: MergeEntry): string {
|
|
119
110
|
const lines: string[] = [
|
|
120
111
|
`[dry-run] Branch: ${entry.branchName}`,
|
|
121
|
-
` Agent: ${entry.agentName} | Task: ${entry.
|
|
112
|
+
` Agent: ${entry.agentName} | Task: ${entry.taskId}`,
|
|
122
113
|
` Status: ${entry.status}`,
|
|
123
114
|
` Files: ${entry.filesModified.length} modified`,
|
|
124
115
|
];
|
|
@@ -135,35 +126,14 @@ function formatDryRun(entry: MergeEntry): string {
|
|
|
135
126
|
/**
|
|
136
127
|
* Entry point for `overstory merge [flags]`.
|
|
137
128
|
*
|
|
138
|
-
*
|
|
139
|
-
* --branch <name> Merge a specific branch
|
|
140
|
-
* --all Merge all pending branches in the queue
|
|
141
|
-
* --dry-run Check for conflicts without actually merging
|
|
142
|
-
* --json Output results as JSON
|
|
129
|
+
* @param opts - Command options
|
|
143
130
|
*/
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
--all Merge all pending branches in the queue
|
|
151
|
-
--into <branch> Target branch to merge into (default: config canonicalBranch)
|
|
152
|
-
--dry-run Check for conflicts without actually merging
|
|
153
|
-
--json Output results as JSON
|
|
154
|
-
--help, -h Show this help`;
|
|
155
|
-
|
|
156
|
-
export async function mergeCommand(args: string[]): Promise<void> {
|
|
157
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
158
|
-
process.stdout.write(`${MERGE_HELP}\n`);
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const branchName = getFlag(args, "--branch");
|
|
163
|
-
const all = hasFlag(args, "--all");
|
|
164
|
-
const into = getFlag(args, "--into");
|
|
165
|
-
const dryRun = hasFlag(args, "--dry-run");
|
|
166
|
-
const json = hasFlag(args, "--json");
|
|
131
|
+
export async function mergeCommand(opts: MergeOptions): Promise<void> {
|
|
132
|
+
const branchName = opts.branch;
|
|
133
|
+
const all = opts.all ?? false;
|
|
134
|
+
const into = opts.into;
|
|
135
|
+
const dryRun = opts.dryRun ?? false;
|
|
136
|
+
const json = opts.json ?? false;
|
|
167
137
|
|
|
168
138
|
if (!branchName && !all) {
|
|
169
139
|
throw new ValidationError("Either --branch <name> or --all is required for overstory merge", {
|
|
@@ -206,7 +176,7 @@ export async function mergeCommand(args: string[]): Promise<void> {
|
|
|
206
176
|
/**
|
|
207
177
|
* Handle merging a specific branch.
|
|
208
178
|
* If the branch is not in the queue, creates a new entry by detecting
|
|
209
|
-
* agent name,
|
|
179
|
+
* agent name, task ID, and modified files from git.
|
|
210
180
|
*/
|
|
211
181
|
async function handleBranch(
|
|
212
182
|
branchName: string,
|
|
@@ -241,12 +211,12 @@ async function handleBranch(
|
|
|
241
211
|
}
|
|
242
212
|
|
|
243
213
|
const agentName = parseAgentName(branchName);
|
|
244
|
-
const
|
|
214
|
+
const taskId = parseBeadId(branchName);
|
|
245
215
|
const filesModified = await detectModifiedFiles(repoRoot, canonicalBranch, branchName);
|
|
246
216
|
|
|
247
217
|
entry = queue.enqueue({
|
|
248
218
|
branchName,
|
|
249
|
-
|
|
219
|
+
taskId,
|
|
250
220
|
agentName,
|
|
251
221
|
filesModified,
|
|
252
222
|
});
|
|
@@ -54,7 +54,7 @@ describe("metricsCommand", () => {
|
|
|
54
54
|
function makeSession(overrides: Partial<SessionMetrics> = {}): SessionMetrics {
|
|
55
55
|
return {
|
|
56
56
|
agentName: "test-agent",
|
|
57
|
-
|
|
57
|
+
taskId: "bead-001",
|
|
58
58
|
capability: "builder",
|
|
59
59
|
startedAt: new Date(Date.now() - 120_000).toISOString(),
|
|
60
60
|
completedAt: new Date().toISOString(),
|
|
@@ -77,7 +77,7 @@ describe("metricsCommand", () => {
|
|
|
77
77
|
await metricsCommand(["--help"]);
|
|
78
78
|
const out = output();
|
|
79
79
|
|
|
80
|
-
expect(out).toContain("
|
|
80
|
+
expect(out).toContain("metrics");
|
|
81
81
|
expect(out).toContain("--last <n>");
|
|
82
82
|
expect(out).toContain("--json");
|
|
83
83
|
expect(out).toContain("--help");
|
|
@@ -87,7 +87,7 @@ describe("metricsCommand", () => {
|
|
|
87
87
|
await metricsCommand(["-h"]);
|
|
88
88
|
const out = output();
|
|
89
89
|
|
|
90
|
-
expect(out).toContain("
|
|
90
|
+
expect(out).toContain("metrics");
|
|
91
91
|
expect(out).toContain("--last <n>");
|
|
92
92
|
});
|
|
93
93
|
|
|
@@ -177,7 +177,7 @@ describe("metricsCommand", () => {
|
|
|
177
177
|
store.recordSession(
|
|
178
178
|
makeSession({
|
|
179
179
|
agentName: "test-builder",
|
|
180
|
-
|
|
180
|
+
taskId: "bead-123",
|
|
181
181
|
capability: "builder",
|
|
182
182
|
}),
|
|
183
183
|
);
|
|
@@ -190,7 +190,7 @@ describe("metricsCommand", () => {
|
|
|
190
190
|
const parsed = JSON.parse(out.trim()) as { sessions: SessionMetrics[] };
|
|
191
191
|
expect(parsed.sessions).toHaveLength(1);
|
|
192
192
|
expect(parsed.sessions[0]?.agentName).toBe("test-builder");
|
|
193
|
-
expect(parsed.sessions[0]?.
|
|
193
|
+
expect(parsed.sessions[0]?.taskId).toBe("bead-123");
|
|
194
194
|
expect(parsed.sessions[0]?.capability).toBe("builder");
|
|
195
195
|
});
|
|
196
196
|
|
|
@@ -203,7 +203,7 @@ describe("metricsCommand", () => {
|
|
|
203
203
|
store.recordSession(
|
|
204
204
|
makeSession({
|
|
205
205
|
agentName: `agent-${i}`,
|
|
206
|
-
|
|
206
|
+
taskId: `bead-${i}`,
|
|
207
207
|
startedAt: new Date(Date.now() - (5 - i) * 1000).toISOString(),
|
|
208
208
|
}),
|
|
209
209
|
);
|
|
@@ -227,7 +227,7 @@ describe("metricsCommand", () => {
|
|
|
227
227
|
store.recordSession(
|
|
228
228
|
makeSession({
|
|
229
229
|
agentName: `agent-${i}`,
|
|
230
|
-
|
|
230
|
+
taskId: `bead-${i}`,
|
|
231
231
|
}),
|
|
232
232
|
);
|
|
233
233
|
}
|
|
@@ -363,7 +363,7 @@ describe("formatDuration helper", () => {
|
|
|
363
363
|
function makeSession(durationMs: number): SessionMetrics {
|
|
364
364
|
return {
|
|
365
365
|
agentName: "test-agent",
|
|
366
|
-
|
|
366
|
+
taskId: "bead-001",
|
|
367
367
|
capability: "builder",
|
|
368
368
|
startedAt: new Date(Date.now() - durationMs).toISOString(),
|
|
369
369
|
completedAt: new Date().toISOString(),
|
package/src/commands/metrics.ts
CHANGED
|
@@ -6,22 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
+
import { Command } from "commander";
|
|
9
10
|
import { loadConfig } from "../config.ts";
|
|
10
11
|
import { createMetricsStore } from "../metrics/store.ts";
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
function getFlag(args: string[], flag: string): string | undefined {
|
|
16
|
-
const idx = args.indexOf(flag);
|
|
17
|
-
if (idx === -1 || idx + 1 >= args.length) {
|
|
18
|
-
return undefined;
|
|
19
|
-
}
|
|
20
|
-
return args[idx + 1];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function hasFlag(args: string[], flag: string): boolean {
|
|
24
|
-
return args.includes(flag);
|
|
13
|
+
interface MetricsOpts {
|
|
14
|
+
last?: string;
|
|
15
|
+
json?: boolean;
|
|
25
16
|
}
|
|
26
17
|
|
|
27
18
|
/**
|
|
@@ -39,27 +30,9 @@ function formatDuration(ms: number): string {
|
|
|
39
30
|
return `${hours}h ${remainMin}m`;
|
|
40
31
|
}
|
|
41
32
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const METRICS_HELP = `overstory metrics — Show session metrics
|
|
46
|
-
|
|
47
|
-
Usage: overstory metrics [--last <n>] [--json]
|
|
48
|
-
|
|
49
|
-
Options:
|
|
50
|
-
--last <n> Number of recent sessions to show (default: 20)
|
|
51
|
-
--json Output as JSON
|
|
52
|
-
--help, -h Show this help`;
|
|
53
|
-
|
|
54
|
-
export async function metricsCommand(args: string[]): Promise<void> {
|
|
55
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
56
|
-
process.stdout.write(`${METRICS_HELP}\n`);
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const lastStr = getFlag(args, "--last");
|
|
61
|
-
const limit = lastStr ? Number.parseInt(lastStr, 10) : 20;
|
|
62
|
-
const json = hasFlag(args, "--json");
|
|
33
|
+
async function executeMetrics(opts: MetricsOpts): Promise<void> {
|
|
34
|
+
const limit = opts.last ? Number.parseInt(opts.last, 10) : 20;
|
|
35
|
+
const json = opts.json ?? false;
|
|
63
36
|
|
|
64
37
|
const cwd = process.cwd();
|
|
65
38
|
const config = await loadConfig(cwd);
|
|
@@ -134,10 +107,36 @@ export async function metricsCommand(args: string[]): Promise<void> {
|
|
|
134
107
|
const status = s.completedAt ? "done" : "running";
|
|
135
108
|
const duration = formatDuration(s.durationMs);
|
|
136
109
|
process.stdout.write(
|
|
137
|
-
` ${s.agentName} [${s.capability}] ${s.
|
|
110
|
+
` ${s.agentName} [${s.capability}] ${s.taskId} | ${status} | ${duration}\n`,
|
|
138
111
|
);
|
|
139
112
|
}
|
|
140
113
|
} finally {
|
|
141
114
|
store.close();
|
|
142
115
|
}
|
|
143
116
|
}
|
|
117
|
+
|
|
118
|
+
export function createMetricsCommand(): Command {
|
|
119
|
+
return new Command("metrics")
|
|
120
|
+
.description("Show session metrics")
|
|
121
|
+
.option("--last <n>", "Number of recent sessions to show (default: 20)")
|
|
122
|
+
.option("--json", "Output as JSON")
|
|
123
|
+
.action(async (opts: MetricsOpts) => {
|
|
124
|
+
await executeMetrics(opts);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function metricsCommand(args: string[]): Promise<void> {
|
|
129
|
+
const cmd = createMetricsCommand();
|
|
130
|
+
cmd.exitOverride();
|
|
131
|
+
try {
|
|
132
|
+
await cmd.parseAsync(args, { from: "user" });
|
|
133
|
+
} catch (err: unknown) {
|
|
134
|
+
if (err && typeof err === "object" && "code" in err) {
|
|
135
|
+
const code = (err as { code: string }).code;
|
|
136
|
+
if (code === "commander.helpDisplayed" || code === "commander.version") {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -89,7 +89,7 @@ describe("monitorCommand", () => {
|
|
|
89
89
|
test("--help prints help text containing 'overstory monitor'", async () => {
|
|
90
90
|
await monitorCommand(["--help"]);
|
|
91
91
|
const output = stdoutWrites.join("");
|
|
92
|
-
expect(output).toContain("
|
|
92
|
+
expect(output).toContain("monitor");
|
|
93
93
|
});
|
|
94
94
|
|
|
95
95
|
test("--help prints help text containing 'start'", async () => {
|
|
@@ -113,13 +113,13 @@ describe("monitorCommand", () => {
|
|
|
113
113
|
test("-h prints help text", async () => {
|
|
114
114
|
await monitorCommand(["-h"]);
|
|
115
115
|
const output = stdoutWrites.join("");
|
|
116
|
-
expect(output).toContain("
|
|
116
|
+
expect(output).toContain("monitor");
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
test("empty args [] shows help (same as --help)", async () => {
|
|
120
120
|
await monitorCommand([]);
|
|
121
121
|
const output = stdoutWrites.join("");
|
|
122
|
-
expect(output).toContain("
|
|
122
|
+
expect(output).toContain("monitor");
|
|
123
123
|
});
|
|
124
124
|
|
|
125
125
|
test("unknown subcommand 'restart' throws ValidationError", async () => {
|
package/src/commands/monitor.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { mkdir } from "node:fs/promises";
|
|
17
17
|
import { join } from "node:path";
|
|
18
|
+
import { Command } from "commander";
|
|
18
19
|
import { deployHooks } from "../agents/hooks-deployer.ts";
|
|
19
20
|
import { createIdentity, loadIdentity } from "../agents/identity.ts";
|
|
20
21
|
import { createManifestLoader, resolveModel } from "../agents/manifest.ts";
|
|
@@ -50,15 +51,6 @@ export function buildMonitorBeacon(): string {
|
|
|
50
51
|
return parts.join(" — ");
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
/**
|
|
54
|
-
* Determine whether to auto-attach to the tmux session after starting.
|
|
55
|
-
*/
|
|
56
|
-
function resolveAttach(args: string[], isTTY: boolean): boolean {
|
|
57
|
-
if (args.includes("--attach")) return true;
|
|
58
|
-
if (args.includes("--no-attach")) return false;
|
|
59
|
-
return isTTY;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
54
|
/**
|
|
63
55
|
* Start the monitor agent.
|
|
64
56
|
*
|
|
@@ -70,9 +62,8 @@ function resolveAttach(args: string[], isTTY: boolean): boolean {
|
|
|
70
62
|
* 6. Send startup beacon
|
|
71
63
|
* 7. Record session in SessionStore (sessions.db)
|
|
72
64
|
*/
|
|
73
|
-
async function startMonitor(
|
|
74
|
-
const json =
|
|
75
|
-
const shouldAttach = resolveAttach(args, !!process.stdout.isTTY);
|
|
65
|
+
async function startMonitor(opts: { json: boolean; attach: boolean }): Promise<void> {
|
|
66
|
+
const { json, attach: shouldAttach } = opts;
|
|
76
67
|
|
|
77
68
|
if (isRunningAsRoot()) {
|
|
78
69
|
throw new AgentError(
|
|
@@ -118,8 +109,6 @@ async function startMonitor(args: string[]): Promise<void> {
|
|
|
118
109
|
}
|
|
119
110
|
|
|
120
111
|
// Deploy monitor-specific hooks to the project root's .claude/ directory.
|
|
121
|
-
// The monitor gets the same structural enforcement as other non-implementation
|
|
122
|
-
// agents (Write/Edit/NotebookEdit blocked, dangerous bash commands blocked).
|
|
123
112
|
await deployHooks(projectRoot, MONITOR_NAME, "monitor");
|
|
124
113
|
|
|
125
114
|
// Create monitor identity if first run
|
|
@@ -146,7 +135,6 @@ async function startMonitor(args: string[]): Promise<void> {
|
|
|
146
135
|
const { model, env } = resolveModel(config, manifest, "monitor", "sonnet");
|
|
147
136
|
|
|
148
137
|
// Spawn tmux session at project root with Claude Code (interactive mode).
|
|
149
|
-
// Inject the monitor base definition via --append-system-prompt.
|
|
150
138
|
const agentDefPath = join(projectRoot, ".overstory", "agent-defs", "monitor.md");
|
|
151
139
|
const agentDefFile = Bun.file(agentDefPath);
|
|
152
140
|
let claudeCmd = `claude --model ${model} --dangerously-skip-permissions`;
|
|
@@ -168,7 +156,7 @@ async function startMonitor(args: string[]): Promise<void> {
|
|
|
168
156
|
capability: "monitor",
|
|
169
157
|
worktreePath: projectRoot, // Monitor uses project root, not a worktree
|
|
170
158
|
branchName: config.project.canonicalBranch, // Operates on canonical branch
|
|
171
|
-
|
|
159
|
+
taskId: "", // No specific bead assignment
|
|
172
160
|
tmuxSession,
|
|
173
161
|
state: "booting",
|
|
174
162
|
pid,
|
|
@@ -226,8 +214,8 @@ async function startMonitor(args: string[]): Promise<void> {
|
|
|
226
214
|
* 2. Kill the tmux session (with process tree cleanup)
|
|
227
215
|
* 3. Mark session as completed in SessionStore
|
|
228
216
|
*/
|
|
229
|
-
async function stopMonitor(
|
|
230
|
-
const json =
|
|
217
|
+
async function stopMonitor(opts: { json: boolean }): Promise<void> {
|
|
218
|
+
const { json } = opts;
|
|
231
219
|
const cwd = process.cwd();
|
|
232
220
|
const config = await loadConfig(cwd);
|
|
233
221
|
const projectRoot = config.project.root;
|
|
@@ -273,8 +261,8 @@ async function stopMonitor(args: string[]): Promise<void> {
|
|
|
273
261
|
*
|
|
274
262
|
* Checks session registry and tmux liveness to report actual state.
|
|
275
263
|
*/
|
|
276
|
-
async function statusMonitor(
|
|
277
|
-
const json =
|
|
264
|
+
async function statusMonitor(opts: { json: boolean }): Promise<void> {
|
|
265
|
+
const { json } = opts;
|
|
278
266
|
const cwd = process.cwd();
|
|
279
267
|
const config = await loadConfig(cwd);
|
|
280
268
|
const projectRoot = config.project.root;
|
|
@@ -301,8 +289,6 @@ async function statusMonitor(args: string[]): Promise<void> {
|
|
|
301
289
|
const alive = await isSessionAlive(session.tmuxSession);
|
|
302
290
|
|
|
303
291
|
// Reconcile state: if session says active but tmux is dead, update.
|
|
304
|
-
// We already filtered out completed/zombie states above, so if tmux is dead
|
|
305
|
-
// this session needs to be marked as zombie.
|
|
306
292
|
if (!alive) {
|
|
307
293
|
store.updateState(MONITOR_NAME, "zombie");
|
|
308
294
|
store.updateLastActivity(MONITOR_NAME);
|
|
@@ -335,56 +321,65 @@ async function statusMonitor(args: string[]): Promise<void> {
|
|
|
335
321
|
}
|
|
336
322
|
}
|
|
337
323
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
324
|
+
export function createMonitorCommand(): Command {
|
|
325
|
+
const cmd = new Command("monitor").description("Manage the persistent Tier 2 monitor agent");
|
|
326
|
+
|
|
327
|
+
cmd
|
|
328
|
+
.command("start")
|
|
329
|
+
.description("Start the monitor (spawns Claude Code at project root)")
|
|
330
|
+
.option("--attach", "Always attach to tmux session after start")
|
|
331
|
+
.option("--no-attach", "Never attach to tmux session after start")
|
|
332
|
+
.option("--json", "Output as JSON")
|
|
333
|
+
.action(async (opts: { attach?: boolean; json?: boolean }) => {
|
|
334
|
+
// opts.attach = true if --attach, false if --no-attach, undefined if neither
|
|
335
|
+
const shouldAttach = opts.attach !== undefined ? opts.attach : !!process.stdout.isTTY;
|
|
336
|
+
await startMonitor({ json: opts.json ?? false, attach: shouldAttach });
|
|
337
|
+
});
|
|
346
338
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
339
|
+
cmd
|
|
340
|
+
.command("stop")
|
|
341
|
+
.description("Stop the monitor (kills tmux session)")
|
|
342
|
+
.option("--json", "Output as JSON")
|
|
343
|
+
.action(async (opts: { json?: boolean }) => {
|
|
344
|
+
await stopMonitor({ json: opts.json ?? false });
|
|
345
|
+
});
|
|
351
346
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
347
|
+
cmd
|
|
348
|
+
.command("status")
|
|
349
|
+
.description("Show monitor state")
|
|
350
|
+
.option("--json", "Output as JSON")
|
|
351
|
+
.action(async (opts: { json?: boolean }) => {
|
|
352
|
+
await statusMonitor({ json: opts.json ?? false });
|
|
353
|
+
});
|
|
355
354
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
- Sending progressive nudges to stalled agents
|
|
359
|
-
- Escalating unresponsive agents to the coordinator
|
|
360
|
-
- Producing periodic health summaries`;
|
|
355
|
+
return cmd;
|
|
356
|
+
}
|
|
361
357
|
|
|
362
358
|
/**
|
|
363
359
|
* Entry point for `overstory monitor <subcommand>`.
|
|
364
360
|
*/
|
|
365
361
|
export async function monitorCommand(args: string[]): Promise<void> {
|
|
366
|
-
|
|
367
|
-
|
|
362
|
+
const cmd = createMonitorCommand();
|
|
363
|
+
cmd.exitOverride();
|
|
364
|
+
|
|
365
|
+
if (args.length === 0) {
|
|
366
|
+
process.stdout.write(cmd.helpInformation());
|
|
368
367
|
return;
|
|
369
368
|
}
|
|
370
369
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
throw new ValidationError(
|
|
386
|
-
`Unknown monitor subcommand: ${subcommand}. Run 'overstory monitor --help' for usage.`,
|
|
387
|
-
{ field: "subcommand", value: subcommand },
|
|
388
|
-
);
|
|
370
|
+
try {
|
|
371
|
+
await cmd.parseAsync(args, { from: "user" });
|
|
372
|
+
} catch (err: unknown) {
|
|
373
|
+
if (err && typeof err === "object" && "code" in err) {
|
|
374
|
+
const code = (err as { code: string }).code;
|
|
375
|
+
if (code === "commander.helpDisplayed" || code === "commander.version") {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (code === "commander.unknownCommand") {
|
|
379
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
380
|
+
throw new ValidationError(message, { field: "subcommand" });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
throw err;
|
|
389
384
|
}
|
|
390
385
|
}
|
|
@@ -46,7 +46,7 @@ function makeSession(overrides: Partial<AgentSession> = {}): AgentSession {
|
|
|
46
46
|
capability: "builder",
|
|
47
47
|
worktreePath: "/tmp/wt",
|
|
48
48
|
branchName: "overstory/test-agent/task-1",
|
|
49
|
-
|
|
49
|
+
taskId: "task-1",
|
|
50
50
|
tmuxSession: "overstory-test-agent",
|
|
51
51
|
state: "working",
|
|
52
52
|
pid: 12345,
|