@os-eco/overstory-cli 0.6.9 → 0.6.11
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 +161 -265
- package/agents/builder.md +6 -15
- package/agents/lead.md +13 -6
- package/agents/merger.md +5 -13
- package/agents/reviewer.md +2 -9
- package/package.json +1 -1
- package/src/agents/hooks-deployer.test.ts +105 -0
- package/src/agents/hooks-deployer.ts +26 -11
- package/src/agents/manifest.test.ts +1 -0
- package/src/agents/overlay.test.ts +235 -1
- package/src/agents/overlay.ts +107 -9
- package/src/commands/completions.test.ts +8 -20
- package/src/commands/completions.ts +7 -5
- package/src/commands/coordinator.ts +4 -4
- package/src/commands/doctor.ts +97 -48
- package/src/commands/ecosystem.ts +291 -0
- package/src/commands/feed.ts +2 -2
- package/src/commands/group.ts +4 -4
- package/src/commands/mail.test.ts +63 -1
- package/src/commands/mail.ts +18 -1
- package/src/commands/merge.ts +2 -2
- package/src/commands/monitor.ts +2 -2
- package/src/commands/sling.test.ts +174 -27
- package/src/commands/sling.ts +96 -12
- package/src/commands/status.ts +1 -1
- package/src/commands/supervisor.ts +4 -4
- package/src/commands/trace.ts +2 -2
- package/src/commands/upgrade.test.ts +46 -0
- package/src/commands/upgrade.ts +259 -0
- package/src/config.test.ts +22 -0
- package/src/config.ts +12 -0
- package/src/doctor/agents.test.ts +1 -0
- package/src/doctor/config-check.test.ts +1 -0
- package/src/doctor/consistency.test.ts +1 -0
- package/src/doctor/databases.test.ts +39 -0
- package/src/doctor/databases.ts +7 -10
- package/src/doctor/dependencies.test.ts +1 -0
- package/src/doctor/ecosystem.test.ts +308 -0
- package/src/doctor/ecosystem.ts +155 -0
- package/src/doctor/logs.test.ts +1 -0
- package/src/doctor/merge-queue.test.ts +99 -0
- package/src/doctor/merge-queue.ts +23 -0
- package/src/doctor/structure.test.ts +131 -1
- package/src/doctor/structure.ts +87 -1
- package/src/doctor/types.ts +5 -2
- package/src/doctor/version.test.ts +1 -0
- package/src/index.ts +29 -4
- package/src/types.ts +11 -0
- package/templates/overlay.md.tmpl +3 -1
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: ov ecosystem
|
|
3
|
+
*
|
|
4
|
+
* Shows a summary dashboard of all installed os-eco tools: version, update
|
|
5
|
+
* status (latest vs outdated), and doctor health (overstory only).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import { jsonError, jsonOutput } from "../json.ts";
|
|
10
|
+
import { accent, brand, color, muted } from "../logging/color.ts";
|
|
11
|
+
|
|
12
|
+
const TOOLS = [
|
|
13
|
+
{ name: "overstory", cli: "ov", npm: "@os-eco/overstory-cli" },
|
|
14
|
+
{ name: "mulch", cli: "ml", npm: "@os-eco/mulch-cli" },
|
|
15
|
+
{ name: "seeds", cli: "sd", npm: "@os-eco/seeds-cli" },
|
|
16
|
+
{ name: "canopy", cli: "cn", npm: "@os-eco/canopy-cli" },
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
export interface EcosystemOptions {
|
|
20
|
+
json?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface DoctorSummary {
|
|
24
|
+
pass: number;
|
|
25
|
+
warn: number;
|
|
26
|
+
fail: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ToolResult {
|
|
30
|
+
name: string;
|
|
31
|
+
cli: string;
|
|
32
|
+
npm: string;
|
|
33
|
+
installed: boolean;
|
|
34
|
+
version?: string;
|
|
35
|
+
latest?: string;
|
|
36
|
+
upToDate?: boolean;
|
|
37
|
+
doctorSummary?: DoctorSummary;
|
|
38
|
+
latestError?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function getInstalledVersion(cli: string): Promise<string | null> {
|
|
42
|
+
// Try --version --json first
|
|
43
|
+
try {
|
|
44
|
+
const proc = Bun.spawn([cli, "--version", "--json"], {
|
|
45
|
+
stdout: "pipe",
|
|
46
|
+
stderr: "pipe",
|
|
47
|
+
});
|
|
48
|
+
const exitCode = await proc.exited;
|
|
49
|
+
if (exitCode === 0) {
|
|
50
|
+
const stdout = await new Response(proc.stdout).text();
|
|
51
|
+
try {
|
|
52
|
+
const data = JSON.parse(stdout.trim()) as { version?: string };
|
|
53
|
+
if (data.version) return data.version;
|
|
54
|
+
} catch {
|
|
55
|
+
// Not valid JSON, fall through to plain text
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// CLI not found — fall through to plain text fallback
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Fallback: --version plain text
|
|
63
|
+
try {
|
|
64
|
+
const proc = Bun.spawn([cli, "--version"], {
|
|
65
|
+
stdout: "pipe",
|
|
66
|
+
stderr: "pipe",
|
|
67
|
+
});
|
|
68
|
+
const exitCode = await proc.exited;
|
|
69
|
+
if (exitCode === 0) {
|
|
70
|
+
const stdout = await new Response(proc.stdout).text();
|
|
71
|
+
const match = stdout.match(/(\d+\.\d+\.\d+)/);
|
|
72
|
+
if (match?.[1]) return match[1];
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// CLI not found
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function fetchLatestVersion(packageName: string): Promise<string> {
|
|
82
|
+
const res = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
|
|
83
|
+
if (!res.ok) {
|
|
84
|
+
throw new Error(`npm registry error: ${res.status} ${res.statusText}`);
|
|
85
|
+
}
|
|
86
|
+
const data = (await res.json()) as { version: string };
|
|
87
|
+
return data.version;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function getDoctorSummary(): Promise<DoctorSummary | undefined> {
|
|
91
|
+
try {
|
|
92
|
+
const proc = Bun.spawn(["ov", "doctor", "--json"], {
|
|
93
|
+
stdout: "pipe",
|
|
94
|
+
stderr: "pipe",
|
|
95
|
+
});
|
|
96
|
+
await proc.exited;
|
|
97
|
+
const stdout = await new Response(proc.stdout).text();
|
|
98
|
+
const trimmed = stdout.trim();
|
|
99
|
+
if (trimmed) {
|
|
100
|
+
const data = JSON.parse(trimmed) as {
|
|
101
|
+
summary?: { pass?: number; warn?: number; fail?: number };
|
|
102
|
+
};
|
|
103
|
+
if (data.summary) {
|
|
104
|
+
return {
|
|
105
|
+
pass: data.summary.pass ?? 0,
|
|
106
|
+
warn: data.summary.warn ?? 0,
|
|
107
|
+
fail: data.summary.fail ?? 0,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// Doctor failed — report nothing
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function checkTool(tool: { name: string; cli: string; npm: string }): Promise<ToolResult> {
|
|
118
|
+
const version = await getInstalledVersion(tool.cli);
|
|
119
|
+
|
|
120
|
+
if (version === null) {
|
|
121
|
+
return { name: tool.name, cli: tool.cli, npm: tool.npm, installed: false };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let latest: string | undefined;
|
|
125
|
+
let latestError: string | undefined;
|
|
126
|
+
let doctorSummary: DoctorSummary | undefined;
|
|
127
|
+
|
|
128
|
+
const latestPromise = fetchLatestVersion(tool.npm)
|
|
129
|
+
.then((v) => {
|
|
130
|
+
latest = v;
|
|
131
|
+
})
|
|
132
|
+
.catch((err) => {
|
|
133
|
+
latestError = err instanceof Error ? err.message : String(err);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const doctorPromise =
|
|
137
|
+
tool.name === "overstory"
|
|
138
|
+
? getDoctorSummary().then((d) => {
|
|
139
|
+
doctorSummary = d;
|
|
140
|
+
})
|
|
141
|
+
: Promise.resolve();
|
|
142
|
+
|
|
143
|
+
await Promise.all([latestPromise, doctorPromise]);
|
|
144
|
+
|
|
145
|
+
const upToDate = latest !== undefined ? version === latest : undefined;
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
name: tool.name,
|
|
149
|
+
cli: tool.cli,
|
|
150
|
+
npm: tool.npm,
|
|
151
|
+
installed: true,
|
|
152
|
+
version,
|
|
153
|
+
latest,
|
|
154
|
+
upToDate,
|
|
155
|
+
doctorSummary,
|
|
156
|
+
latestError,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function formatDoctorLine(summary: DoctorSummary): string {
|
|
161
|
+
const parts: string[] = [];
|
|
162
|
+
if (summary.pass > 0) parts.push(color.green(`${summary.pass} passed`));
|
|
163
|
+
if (summary.warn > 0) parts.push(color.yellow(`${summary.warn} warn`));
|
|
164
|
+
if (summary.fail > 0) parts.push(color.red(`${summary.fail} fail`));
|
|
165
|
+
return parts.length > 0 ? parts.join(", ") : "no checks";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function printHumanOutput(results: ToolResult[]): void {
|
|
169
|
+
process.stdout.write(`${brand.bold("os-eco Ecosystem")}\n`);
|
|
170
|
+
process.stdout.write(`${"═".repeat(60)}\n`);
|
|
171
|
+
process.stdout.write("\n");
|
|
172
|
+
|
|
173
|
+
for (const tool of results) {
|
|
174
|
+
if (!tool.installed) {
|
|
175
|
+
process.stdout.write(
|
|
176
|
+
` ${color.red("x")} ${accent(tool.name)} ${muted(`(${tool.cli})`)} ${color.red("not installed")}\n`,
|
|
177
|
+
);
|
|
178
|
+
process.stdout.write(` ${muted(`npm i -g ${tool.npm}`)}\n`);
|
|
179
|
+
process.stdout.write("\n");
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Determine status icon
|
|
184
|
+
let icon: string;
|
|
185
|
+
if (tool.latestError !== undefined || tool.upToDate === undefined) {
|
|
186
|
+
icon = muted("-");
|
|
187
|
+
} else if (tool.upToDate) {
|
|
188
|
+
icon = color.green("-");
|
|
189
|
+
} else {
|
|
190
|
+
icon = color.yellow("!");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
process.stdout.write(` ${icon} ${accent(tool.name)} ${muted(`(${tool.cli})`)}\n`);
|
|
194
|
+
|
|
195
|
+
// Version line
|
|
196
|
+
let versionLine = `Version: ${tool.version}`;
|
|
197
|
+
if (tool.latestError !== undefined) {
|
|
198
|
+
versionLine += ` ${muted("(version check failed)")}`;
|
|
199
|
+
} else if (tool.upToDate === true) {
|
|
200
|
+
versionLine += ` ${color.green("(up to date)")}`;
|
|
201
|
+
} else if (tool.upToDate === false) {
|
|
202
|
+
versionLine += ` ${color.yellow(`(outdated, latest: ${tool.latest})`)}`;
|
|
203
|
+
}
|
|
204
|
+
process.stdout.write(` ${versionLine}\n`);
|
|
205
|
+
|
|
206
|
+
// Doctor summary (overstory only)
|
|
207
|
+
if (tool.doctorSummary !== undefined) {
|
|
208
|
+
process.stdout.write(` Doctor: ${formatDoctorLine(tool.doctorSummary)}\n`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
process.stdout.write("\n");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const installed = results.filter((t) => t.installed).length;
|
|
215
|
+
const missing = results.filter((t) => !t.installed).length;
|
|
216
|
+
const outdated = results.filter(
|
|
217
|
+
(t) => t.installed && t.upToDate === false && t.latestError === undefined,
|
|
218
|
+
).length;
|
|
219
|
+
|
|
220
|
+
let summary = `Summary: ${installed}/${results.length} installed`;
|
|
221
|
+
if (missing > 0) summary += `, ${missing} missing`;
|
|
222
|
+
if (outdated > 0) summary += `, ${outdated} outdated`;
|
|
223
|
+
process.stdout.write(`${summary}\n`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function executeEcosystem(opts: EcosystemOptions): Promise<void> {
|
|
227
|
+
const json = opts.json ?? false;
|
|
228
|
+
|
|
229
|
+
let results: ToolResult[];
|
|
230
|
+
try {
|
|
231
|
+
results = await Promise.all(TOOLS.map((tool) => checkTool(tool)));
|
|
232
|
+
} catch (err) {
|
|
233
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
234
|
+
if (json) {
|
|
235
|
+
jsonError("ecosystem", msg);
|
|
236
|
+
} else {
|
|
237
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
238
|
+
}
|
|
239
|
+
process.exitCode = 1;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (json) {
|
|
244
|
+
const installed = results.filter((t) => t.installed).length;
|
|
245
|
+
const missing = results.filter((t) => !t.installed).length;
|
|
246
|
+
const outdated = results.filter(
|
|
247
|
+
(t) => t.installed && t.upToDate === false && t.latestError === undefined,
|
|
248
|
+
).length;
|
|
249
|
+
|
|
250
|
+
jsonOutput("ecosystem", {
|
|
251
|
+
tools: results.map((t) => {
|
|
252
|
+
const entry: Record<string, unknown> = {
|
|
253
|
+
name: t.name,
|
|
254
|
+
cli: t.cli,
|
|
255
|
+
npm: t.npm,
|
|
256
|
+
installed: t.installed,
|
|
257
|
+
};
|
|
258
|
+
if (t.installed) {
|
|
259
|
+
entry.version = t.version;
|
|
260
|
+
entry.latest = t.latest;
|
|
261
|
+
entry.upToDate = t.upToDate;
|
|
262
|
+
if (t.doctorSummary !== undefined) {
|
|
263
|
+
entry.doctorSummary = t.doctorSummary;
|
|
264
|
+
}
|
|
265
|
+
if (t.latestError !== undefined) {
|
|
266
|
+
entry.latestError = t.latestError;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return entry;
|
|
270
|
+
}),
|
|
271
|
+
summary: {
|
|
272
|
+
total: results.length,
|
|
273
|
+
installed,
|
|
274
|
+
missing,
|
|
275
|
+
outdated,
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
printHumanOutput(results);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function createEcosystemCommand(): Command {
|
|
285
|
+
return new Command("ecosystem")
|
|
286
|
+
.description("Show a summary dashboard of all installed os-eco tools")
|
|
287
|
+
.option("--json", "Output as JSON")
|
|
288
|
+
.action(async (opts: EcosystemOptions) => {
|
|
289
|
+
await executeEcosystem(opts);
|
|
290
|
+
});
|
|
291
|
+
}
|
package/src/commands/feed.ts
CHANGED
|
@@ -264,7 +264,7 @@ async function executeFeed(opts: FeedOpts): Promise<void> {
|
|
|
264
264
|
} else {
|
|
265
265
|
// JSON mode: print each event as a line
|
|
266
266
|
for (const event of initialEvents) {
|
|
267
|
-
|
|
267
|
+
jsonOutput("feed", { event });
|
|
268
268
|
}
|
|
269
269
|
if (initialEvents.length > 0) {
|
|
270
270
|
const lastEvent = initialEvents[initialEvents.length - 1];
|
|
@@ -308,7 +308,7 @@ async function executeFeed(opts: FeedOpts): Promise<void> {
|
|
|
308
308
|
} else {
|
|
309
309
|
// JSON mode: print each event as a line
|
|
310
310
|
for (const event of newEvents) {
|
|
311
|
-
|
|
311
|
+
jsonOutput("feed", { event });
|
|
312
312
|
}
|
|
313
313
|
}
|
|
314
314
|
|
package/src/commands/group.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* CLI command: ov group create|status|add|remove|list
|
|
3
3
|
*
|
|
4
4
|
* Manages TaskGroups for batch work coordination. Groups track collections
|
|
5
|
-
* of
|
|
5
|
+
* of issues and auto-close when all member issues are closed.
|
|
6
6
|
*
|
|
7
7
|
* Storage: `.overstory/groups.json` (array of TaskGroup objects).
|
|
8
8
|
*/
|
|
@@ -312,7 +312,7 @@ export function createGroupCommand(): Command {
|
|
|
312
312
|
.argument("<name>", "Group name")
|
|
313
313
|
.argument("<ids...>", "Issue IDs to include")
|
|
314
314
|
.option("--json", "Output as JSON")
|
|
315
|
-
.option("--skip-validation", "Skip
|
|
315
|
+
.option("--skip-validation", "Skip task validation (for offline use)")
|
|
316
316
|
.action(
|
|
317
317
|
async (name: string, ids: string[], opts: { json?: boolean; skipValidation?: boolean }) => {
|
|
318
318
|
const config = await loadConfig(process.cwd());
|
|
@@ -343,7 +343,7 @@ export function createGroupCommand(): Command {
|
|
|
343
343
|
.description("Show progress for one or all groups")
|
|
344
344
|
.argument("[group-id]", "Group ID (optional, shows all if omitted)")
|
|
345
345
|
.option("--json", "Output as JSON")
|
|
346
|
-
.option("--skip-validation", "Skip
|
|
346
|
+
.option("--skip-validation", "Skip task validation (for offline use)")
|
|
347
347
|
.action(
|
|
348
348
|
async (groupId: string | undefined, opts: { json?: boolean; skipValidation?: boolean }) => {
|
|
349
349
|
const config = await loadConfig(process.cwd());
|
|
@@ -398,7 +398,7 @@ export function createGroupCommand(): Command {
|
|
|
398
398
|
.argument("<group-id>", "Group ID")
|
|
399
399
|
.argument("<ids...>", "Issue IDs to add")
|
|
400
400
|
.option("--json", "Output as JSON")
|
|
401
|
-
.option("--skip-validation", "Skip
|
|
401
|
+
.option("--skip-validation", "Skip task validation (for offline use)")
|
|
402
402
|
.action(
|
|
403
403
|
async (
|
|
404
404
|
groupId: string,
|
|
@@ -14,7 +14,7 @@ import { stripAnsi } from "../logging/color.ts";
|
|
|
14
14
|
import { createMailClient } from "../mail/client.ts";
|
|
15
15
|
import { createMailStore } from "../mail/store.ts";
|
|
16
16
|
import type { StoredEvent } from "../types.ts";
|
|
17
|
-
import { mailCommand } from "./mail.ts";
|
|
17
|
+
import { AUTO_NUDGE_TYPES, isDispatchNudge, mailCommand, shouldAutoNudge } from "./mail.ts";
|
|
18
18
|
|
|
19
19
|
describe("mailCommand", () => {
|
|
20
20
|
let tempDir: string;
|
|
@@ -1269,3 +1269,65 @@ describe("mailCommand", () => {
|
|
|
1269
1269
|
});
|
|
1270
1270
|
});
|
|
1271
1271
|
});
|
|
1272
|
+
|
|
1273
|
+
describe("shouldAutoNudge", () => {
|
|
1274
|
+
test("returns true for urgent priority regardless of type", () => {
|
|
1275
|
+
expect(shouldAutoNudge("status", "urgent")).toBe(true);
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
test("returns true for high priority regardless of type", () => {
|
|
1279
|
+
expect(shouldAutoNudge("status", "high")).toBe(true);
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
test("returns true for worker_done type at normal priority", () => {
|
|
1283
|
+
expect(shouldAutoNudge("worker_done", "normal")).toBe(true);
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
test("returns true for merge_ready type at normal priority", () => {
|
|
1287
|
+
expect(shouldAutoNudge("merge_ready", "normal")).toBe(true);
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
test("returns true for error type at normal priority", () => {
|
|
1291
|
+
expect(shouldAutoNudge("error", "normal")).toBe(true);
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
test("returns false for status type at normal priority", () => {
|
|
1295
|
+
expect(shouldAutoNudge("status", "normal")).toBe(false);
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
test("returns false for question type at low priority", () => {
|
|
1299
|
+
expect(shouldAutoNudge("question", "low")).toBe(false);
|
|
1300
|
+
});
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
describe("isDispatchNudge", () => {
|
|
1304
|
+
test("returns true for dispatch type", () => {
|
|
1305
|
+
expect(isDispatchNudge("dispatch")).toBe(true);
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
test("returns false for worker_done type", () => {
|
|
1309
|
+
expect(isDispatchNudge("worker_done")).toBe(false);
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
test("returns false for status type", () => {
|
|
1313
|
+
expect(isDispatchNudge("status")).toBe(false);
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
test("returns false for error type", () => {
|
|
1317
|
+
expect(isDispatchNudge("error")).toBe(false);
|
|
1318
|
+
});
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
describe("AUTO_NUDGE_TYPES", () => {
|
|
1322
|
+
test("contains worker_done, merge_ready, and error", () => {
|
|
1323
|
+
expect(AUTO_NUDGE_TYPES.has("worker_done")).toBe(true);
|
|
1324
|
+
expect(AUTO_NUDGE_TYPES.has("merge_ready")).toBe(true);
|
|
1325
|
+
expect(AUTO_NUDGE_TYPES.has("error")).toBe(true);
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
test("does not contain regular semantic types", () => {
|
|
1329
|
+
expect(AUTO_NUDGE_TYPES.has("status")).toBe(false);
|
|
1330
|
+
expect(AUTO_NUDGE_TYPES.has("question")).toBe(false);
|
|
1331
|
+
expect(AUTO_NUDGE_TYPES.has("result")).toBe(false);
|
|
1332
|
+
});
|
|
1333
|
+
});
|
package/src/commands/mail.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { MAIL_MESSAGE_TYPES } from "../types.ts";
|
|
|
24
24
|
* Protocol message types that require immediate recipient attention.
|
|
25
25
|
* These trigger auto-nudge regardless of priority level.
|
|
26
26
|
*/
|
|
27
|
-
const AUTO_NUDGE_TYPES: ReadonlySet<MailMessageType> = new Set([
|
|
27
|
+
export const AUTO_NUDGE_TYPES: ReadonlySet<MailMessageType> = new Set([
|
|
28
28
|
"worker_done",
|
|
29
29
|
"merge_ready",
|
|
30
30
|
"error",
|
|
@@ -32,6 +32,23 @@ const AUTO_NUDGE_TYPES: ReadonlySet<MailMessageType> = new Set([
|
|
|
32
32
|
"merge_failed",
|
|
33
33
|
]);
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Check if a message type/priority combination should trigger a pending nudge.
|
|
37
|
+
* Exported for testability.
|
|
38
|
+
*/
|
|
39
|
+
export function shouldAutoNudge(type: MailMessageType, priority: MailMessage["priority"]): boolean {
|
|
40
|
+
return priority === "urgent" || priority === "high" || AUTO_NUDGE_TYPES.has(type);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if a message type should trigger an immediate tmux dispatch nudge.
|
|
45
|
+
* Dispatch nudges target newly spawned agents at the welcome screen.
|
|
46
|
+
* Exported for testability.
|
|
47
|
+
*/
|
|
48
|
+
export function isDispatchNudge(type: MailMessageType): boolean {
|
|
49
|
+
return type === "dispatch";
|
|
50
|
+
}
|
|
51
|
+
|
|
35
52
|
/** Format a single message for human-readable output. */
|
|
36
53
|
function formatMessage(msg: MailMessage): string {
|
|
37
54
|
const readMarker = msg.read ? " " : "*";
|
package/src/commands/merge.ts
CHANGED
|
@@ -47,7 +47,7 @@ function parseAgentName(branchName: string): string {
|
|
|
47
47
|
* Pattern: overstory/{agentName}/{taskId}
|
|
48
48
|
* Falls back to "unknown" if the pattern does not match.
|
|
49
49
|
*/
|
|
50
|
-
function
|
|
50
|
+
function parseTaskId(branchName: string): string {
|
|
51
51
|
const parts = branchName.split("/");
|
|
52
52
|
if (parts[0] === "overstory" && parts[2] !== undefined) {
|
|
53
53
|
return parts[2];
|
|
@@ -213,7 +213,7 @@ async function handleBranch(
|
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
const agentName = parseAgentName(branchName);
|
|
216
|
-
const taskId =
|
|
216
|
+
const taskId = parseTaskId(branchName);
|
|
217
217
|
const filesModified = await detectModifiedFiles(repoRoot, canonicalBranch, branchName);
|
|
218
218
|
|
|
219
219
|
entry = queue.enqueue({
|
package/src/commands/monitor.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Unlike regular agents spawned by sling, the monitor:
|
|
10
10
|
* - Has no worktree (operates on the main working tree)
|
|
11
|
-
* - Has no
|
|
11
|
+
* - Has no task assignment (it monitors, not implements)
|
|
12
12
|
* - Has no overlay CLAUDE.md (context comes via ov status + mail)
|
|
13
13
|
* - Persists across patrol cycles
|
|
14
14
|
*/
|
|
@@ -158,7 +158,7 @@ async function startMonitor(opts: { json: boolean; attach: boolean }): Promise<v
|
|
|
158
158
|
capability: "monitor",
|
|
159
159
|
worktreePath: projectRoot, // Monitor uses project root, not a worktree
|
|
160
160
|
branchName: config.project.canonicalBranch, // Operates on canonical branch
|
|
161
|
-
taskId: "", // No specific
|
|
161
|
+
taskId: "", // No specific task assignment
|
|
162
162
|
tmuxSession,
|
|
163
163
|
state: "booting",
|
|
164
164
|
pid,
|