@os-eco/overstory-cli 0.6.8 → 0.6.10
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 +19 -5
- package/agents/builder.md +6 -15
- package/agents/lead.md +4 -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 +232 -0
- package/src/agents/hooks-deployer.ts +54 -8
- package/src/agents/overlay.test.ts +156 -1
- package/src/agents/overlay.ts +67 -7
- package/src/commands/agents.ts +9 -6
- package/src/commands/clean.ts +2 -1
- package/src/commands/completions.test.ts +8 -20
- package/src/commands/completions.ts +7 -6
- package/src/commands/coordinator.test.ts +8 -0
- package/src/commands/coordinator.ts +11 -8
- package/src/commands/costs.test.ts +48 -38
- package/src/commands/costs.ts +48 -38
- package/src/commands/dashboard.ts +7 -7
- package/src/commands/doctor.test.ts +8 -0
- package/src/commands/doctor.ts +96 -51
- package/src/commands/ecosystem.ts +291 -0
- package/src/commands/errors.test.ts +47 -40
- package/src/commands/errors.ts +5 -4
- package/src/commands/feed.test.ts +40 -33
- package/src/commands/feed.ts +5 -4
- package/src/commands/group.ts +23 -14
- package/src/commands/hooks.ts +2 -1
- package/src/commands/init.test.ts +104 -0
- package/src/commands/init.ts +11 -7
- package/src/commands/inspect.test.ts +2 -0
- package/src/commands/inspect.ts +9 -8
- package/src/commands/logs.test.ts +5 -6
- package/src/commands/logs.ts +2 -1
- package/src/commands/mail.test.ts +11 -10
- package/src/commands/mail.ts +11 -12
- package/src/commands/merge.ts +11 -12
- package/src/commands/metrics.test.ts +15 -2
- package/src/commands/metrics.ts +3 -2
- package/src/commands/monitor.ts +5 -4
- package/src/commands/nudge.ts +2 -3
- package/src/commands/prime.test.ts +1 -6
- package/src/commands/prime.ts +2 -3
- package/src/commands/replay.test.ts +62 -55
- package/src/commands/replay.ts +3 -2
- package/src/commands/run.ts +17 -20
- package/src/commands/sling.ts +3 -2
- package/src/commands/status.test.ts +2 -1
- package/src/commands/status.ts +7 -6
- package/src/commands/stop.test.ts +2 -0
- package/src/commands/stop.ts +10 -11
- package/src/commands/supervisor.ts +7 -6
- package/src/commands/trace.test.ts +52 -44
- package/src/commands/trace.ts +5 -4
- package/src/commands/upgrade.test.ts +46 -0
- package/src/commands/upgrade.ts +259 -0
- package/src/commands/watch.ts +8 -10
- package/src/commands/worktree.test.ts +21 -15
- package/src/commands/worktree.ts +10 -4
- package/src/doctor/databases.test.ts +38 -0
- package/src/doctor/databases.ts +7 -10
- package/src/doctor/ecosystem.test.ts +307 -0
- package/src/doctor/ecosystem.ts +155 -0
- package/src/doctor/merge-queue.test.ts +98 -0
- package/src/doctor/merge-queue.ts +23 -0
- package/src/doctor/structure.test.ts +130 -1
- package/src/doctor/structure.ts +87 -1
- package/src/doctor/types.ts +5 -2
- package/src/index.ts +25 -1
package/src/commands/feed.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { Command } from "commander";
|
|
|
11
11
|
import { loadConfig } from "../config.ts";
|
|
12
12
|
import { ValidationError } from "../errors.ts";
|
|
13
13
|
import { createEventStore } from "../events/store.ts";
|
|
14
|
+
import { jsonOutput } from "../json.ts";
|
|
14
15
|
import type { ColorFn } from "../logging/color.ts";
|
|
15
16
|
import { color } from "../logging/color.ts";
|
|
16
17
|
import type { EventType, StoredEvent } from "../types.ts";
|
|
@@ -189,7 +190,7 @@ async function executeFeed(opts: FeedOpts): Promise<void> {
|
|
|
189
190
|
const eventsFile = Bun.file(eventsDbPath);
|
|
190
191
|
if (!(await eventsFile.exists())) {
|
|
191
192
|
if (json) {
|
|
192
|
-
|
|
193
|
+
jsonOutput("feed", { events: [] });
|
|
193
194
|
} else {
|
|
194
195
|
process.stdout.write("No events data yet.\n");
|
|
195
196
|
}
|
|
@@ -228,7 +229,7 @@ async function executeFeed(opts: FeedOpts): Promise<void> {
|
|
|
228
229
|
const events = queryEvents({ since, limit });
|
|
229
230
|
|
|
230
231
|
if (json) {
|
|
231
|
-
|
|
232
|
+
jsonOutput("feed", { events });
|
|
232
233
|
return;
|
|
233
234
|
}
|
|
234
235
|
|
|
@@ -263,7 +264,7 @@ async function executeFeed(opts: FeedOpts): Promise<void> {
|
|
|
263
264
|
} else {
|
|
264
265
|
// JSON mode: print each event as a line
|
|
265
266
|
for (const event of initialEvents) {
|
|
266
|
-
|
|
267
|
+
jsonOutput("feed", { event });
|
|
267
268
|
}
|
|
268
269
|
if (initialEvents.length > 0) {
|
|
269
270
|
const lastEvent = initialEvents[initialEvents.length - 1];
|
|
@@ -307,7 +308,7 @@ async function executeFeed(opts: FeedOpts): Promise<void> {
|
|
|
307
308
|
} else {
|
|
308
309
|
// JSON mode: print each event as a line
|
|
309
310
|
for (const event of newEvents) {
|
|
310
|
-
|
|
311
|
+
jsonOutput("feed", { event });
|
|
311
312
|
}
|
|
312
313
|
}
|
|
313
314
|
|
package/src/commands/group.ts
CHANGED
|
@@ -11,7 +11,8 @@ import { join } from "node:path";
|
|
|
11
11
|
import { Command } from "commander";
|
|
12
12
|
import { loadConfig } from "../config.ts";
|
|
13
13
|
import { GroupError, ValidationError } from "../errors.ts";
|
|
14
|
-
import {
|
|
14
|
+
import { jsonOutput } from "../json.ts";
|
|
15
|
+
import { accent, printHint, printSuccess } from "../logging/color.ts";
|
|
15
16
|
import { createTrackerClient, resolveBackend, type TrackerClient } from "../tracker/factory.ts";
|
|
16
17
|
import type { TaskGroup, TaskGroupProgress } from "../types.ts";
|
|
17
18
|
|
|
@@ -247,7 +248,9 @@ async function getGroupProgress(
|
|
|
247
248
|
group.status = "completed";
|
|
248
249
|
group.completedAt = new Date().toISOString();
|
|
249
250
|
await saveGroups(projectRoot, groups);
|
|
250
|
-
process.stdout.write(
|
|
251
|
+
process.stdout.write(
|
|
252
|
+
`Group "${group.name}" (${accent(group.id)}) auto-closed: all issues done\n`,
|
|
253
|
+
);
|
|
251
254
|
|
|
252
255
|
// Notify coordinator via mail (best-effort)
|
|
253
256
|
try {
|
|
@@ -286,7 +289,7 @@ function printGroupProgress(progress: TaskGroupProgress): void {
|
|
|
286
289
|
const w = process.stdout.write.bind(process.stdout);
|
|
287
290
|
const { group, total, completed, inProgress, blocked, open } = progress;
|
|
288
291
|
const status = group.status === "completed" ? "[completed]" : "[active]";
|
|
289
|
-
w(`${group.name} (${group.id}) ${status}\n`);
|
|
292
|
+
w(`${group.name} (${accent(group.id)}) ${status}\n`);
|
|
290
293
|
w(` Issues: ${total} total`);
|
|
291
294
|
w(` | ${completed} completed`);
|
|
292
295
|
w(` | ${inProgress} in_progress`);
|
|
@@ -325,10 +328,12 @@ export function createGroupCommand(): Command {
|
|
|
325
328
|
tracker,
|
|
326
329
|
);
|
|
327
330
|
if (opts.json) {
|
|
328
|
-
|
|
331
|
+
jsonOutput("group create", { ...group });
|
|
329
332
|
} else {
|
|
330
333
|
printSuccess("Created group", group.name);
|
|
331
|
-
process.stdout.write(
|
|
334
|
+
process.stdout.write(
|
|
335
|
+
` Members: ${group.memberIssueIds.map((id) => accent(id)).join(", ")}\n`,
|
|
336
|
+
);
|
|
332
337
|
}
|
|
333
338
|
},
|
|
334
339
|
);
|
|
@@ -356,7 +361,7 @@ export function createGroupCommand(): Command {
|
|
|
356
361
|
}
|
|
357
362
|
const progress = await getGroupProgress(projectRoot, group, groups, tracker);
|
|
358
363
|
if (json) {
|
|
359
|
-
|
|
364
|
+
jsonOutput("group status", { ...progress });
|
|
360
365
|
} else {
|
|
361
366
|
printGroupProgress(progress);
|
|
362
367
|
}
|
|
@@ -364,7 +369,7 @@ export function createGroupCommand(): Command {
|
|
|
364
369
|
const activeGroups = groups.filter((g) => g.status === "active");
|
|
365
370
|
if (activeGroups.length === 0) {
|
|
366
371
|
if (json) {
|
|
367
|
-
|
|
372
|
+
jsonOutput("group status", { groups: [] });
|
|
368
373
|
} else {
|
|
369
374
|
printHint("No active groups");
|
|
370
375
|
}
|
|
@@ -376,7 +381,7 @@ export function createGroupCommand(): Command {
|
|
|
376
381
|
progressList.push(progress);
|
|
377
382
|
}
|
|
378
383
|
if (json) {
|
|
379
|
-
|
|
384
|
+
jsonOutput("group status", { groups: progressList });
|
|
380
385
|
} else {
|
|
381
386
|
for (const progress of progressList) {
|
|
382
387
|
printGroupProgress(progress);
|
|
@@ -413,10 +418,12 @@ export function createGroupCommand(): Command {
|
|
|
413
418
|
tracker,
|
|
414
419
|
);
|
|
415
420
|
if (opts.json) {
|
|
416
|
-
|
|
421
|
+
jsonOutput("group add", { ...group });
|
|
417
422
|
} else {
|
|
418
423
|
printSuccess("Added to group", group.name);
|
|
419
|
-
process.stdout.write(
|
|
424
|
+
process.stdout.write(
|
|
425
|
+
` Members: ${group.memberIssueIds.map((id) => accent(id)).join(", ")}\n`,
|
|
426
|
+
);
|
|
420
427
|
}
|
|
421
428
|
},
|
|
422
429
|
);
|
|
@@ -433,10 +440,12 @@ export function createGroupCommand(): Command {
|
|
|
433
440
|
|
|
434
441
|
const group = await removeFromGroup(projectRoot, groupId, ids);
|
|
435
442
|
if (opts.json) {
|
|
436
|
-
|
|
443
|
+
jsonOutput("group remove", { ...group });
|
|
437
444
|
} else {
|
|
438
445
|
printSuccess("Removed from group", group.name);
|
|
439
|
-
process.stdout.write(
|
|
446
|
+
process.stdout.write(
|
|
447
|
+
` Members: ${group.memberIssueIds.map((id) => accent(id)).join(", ")}\n`,
|
|
448
|
+
);
|
|
440
449
|
}
|
|
441
450
|
});
|
|
442
451
|
|
|
@@ -458,12 +467,12 @@ export function createGroupCommand(): Command {
|
|
|
458
467
|
return;
|
|
459
468
|
}
|
|
460
469
|
if (opts.json) {
|
|
461
|
-
|
|
470
|
+
jsonOutput("group list", { groups });
|
|
462
471
|
} else {
|
|
463
472
|
for (const group of groups) {
|
|
464
473
|
const status = group.status === "completed" ? "[completed]" : "[active]";
|
|
465
474
|
process.stdout.write(
|
|
466
|
-
`${group.id} ${status} "${group.name}" (${group.memberIssueIds.length} issues)\n`,
|
|
475
|
+
`${accent(group.id)} ${status} "${group.name}" (${group.memberIssueIds.length} issues)\n`,
|
|
467
476
|
);
|
|
468
477
|
}
|
|
469
478
|
}
|
package/src/commands/hooks.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { join } from "node:path";
|
|
|
15
15
|
import { Command } from "commander";
|
|
16
16
|
import { loadConfig } from "../config.ts";
|
|
17
17
|
import { ValidationError } from "../errors.ts";
|
|
18
|
+
import { jsonOutput } from "../json.ts";
|
|
18
19
|
import { printHint, printSuccess, printWarning } from "../logging/color.ts";
|
|
19
20
|
|
|
20
21
|
interface HookEntry {
|
|
@@ -189,7 +190,7 @@ async function statusHooks(json: boolean): Promise<void> {
|
|
|
189
190
|
}
|
|
190
191
|
|
|
191
192
|
if (json) {
|
|
192
|
-
|
|
193
|
+
jsonOutput("hooks status", { sourceExists, installed });
|
|
193
194
|
} else {
|
|
194
195
|
process.stdout.write(
|
|
195
196
|
`Hooks source (.overstory/hooks.json): ${sourceExists ? "present" : "missing"}\n`,
|
|
@@ -345,3 +345,107 @@ describe("initCommand: canonical branch detection", () => {
|
|
|
345
345
|
expect(content).toContain("canonicalBranch: main");
|
|
346
346
|
});
|
|
347
347
|
});
|
|
348
|
+
|
|
349
|
+
describe("initCommand: --yes flag", () => {
|
|
350
|
+
let tempDir: string;
|
|
351
|
+
let originalCwd: string;
|
|
352
|
+
let originalWrite: typeof process.stdout.write;
|
|
353
|
+
|
|
354
|
+
beforeEach(async () => {
|
|
355
|
+
tempDir = await createTempGitRepo();
|
|
356
|
+
originalCwd = process.cwd();
|
|
357
|
+
process.chdir(tempDir);
|
|
358
|
+
|
|
359
|
+
// Suppress stdout noise from initCommand
|
|
360
|
+
originalWrite = process.stdout.write;
|
|
361
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
afterEach(async () => {
|
|
365
|
+
process.chdir(originalCwd);
|
|
366
|
+
process.stdout.write = originalWrite;
|
|
367
|
+
await cleanupTempDir(tempDir);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("--yes reinitializes when .overstory/ already exists", async () => {
|
|
371
|
+
// First init
|
|
372
|
+
await initCommand({});
|
|
373
|
+
|
|
374
|
+
// Tamper with config to verify reinit happens
|
|
375
|
+
const configPath = join(tempDir, ".overstory", "config.yaml");
|
|
376
|
+
await Bun.write(configPath, "# tampered\n");
|
|
377
|
+
|
|
378
|
+
// Second init with --yes should reinitialize (not return early)
|
|
379
|
+
await initCommand({ yes: true });
|
|
380
|
+
|
|
381
|
+
// Verify config was regenerated (not the tampered content)
|
|
382
|
+
const content = await Bun.file(configPath).text();
|
|
383
|
+
expect(content).not.toBe("# tampered\n");
|
|
384
|
+
expect(content).toContain("# Overstory configuration");
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("--yes works on fresh project (no .overstory/ yet)", async () => {
|
|
388
|
+
await initCommand({ yes: true });
|
|
389
|
+
|
|
390
|
+
const configPath = join(tempDir, ".overstory", "config.yaml");
|
|
391
|
+
const exists = await Bun.file(configPath).exists();
|
|
392
|
+
expect(exists).toBe(true);
|
|
393
|
+
|
|
394
|
+
const content = await Bun.file(configPath).text();
|
|
395
|
+
expect(content).toContain("# Overstory configuration");
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("--yes overwrites agent-defs on reinit", async () => {
|
|
399
|
+
// First init
|
|
400
|
+
await initCommand({});
|
|
401
|
+
|
|
402
|
+
// Tamper with an agent def
|
|
403
|
+
const scoutPath = join(tempDir, ".overstory", "agent-defs", "scout.md");
|
|
404
|
+
await Bun.write(scoutPath, "TAMPERED CONTENT");
|
|
405
|
+
|
|
406
|
+
// Reinit with --yes should overwrite
|
|
407
|
+
await initCommand({ yes: true });
|
|
408
|
+
|
|
409
|
+
const restored = await Bun.file(scoutPath).text();
|
|
410
|
+
expect(restored).not.toBe("TAMPERED CONTENT");
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
describe("initCommand: --name flag", () => {
|
|
415
|
+
let tempDir: string;
|
|
416
|
+
let originalCwd: string;
|
|
417
|
+
let originalWrite: typeof process.stdout.write;
|
|
418
|
+
|
|
419
|
+
beforeEach(async () => {
|
|
420
|
+
tempDir = await createTempGitRepo();
|
|
421
|
+
originalCwd = process.cwd();
|
|
422
|
+
process.chdir(tempDir);
|
|
423
|
+
|
|
424
|
+
// Suppress stdout noise from initCommand
|
|
425
|
+
originalWrite = process.stdout.write;
|
|
426
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
afterEach(async () => {
|
|
430
|
+
process.chdir(originalCwd);
|
|
431
|
+
process.stdout.write = originalWrite;
|
|
432
|
+
await cleanupTempDir(tempDir);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test("--name overrides auto-detected project name", async () => {
|
|
436
|
+
await initCommand({ name: "custom-project" });
|
|
437
|
+
|
|
438
|
+
const configPath = join(tempDir, ".overstory", "config.yaml");
|
|
439
|
+
const content = await Bun.file(configPath).text();
|
|
440
|
+
expect(content).toContain("name: custom-project");
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test("--name combined with --yes works for fully non-interactive init", async () => {
|
|
444
|
+
await initCommand({ yes: true, name: "scripted-project" });
|
|
445
|
+
|
|
446
|
+
const configPath = join(tempDir, ".overstory", "config.yaml");
|
|
447
|
+
const content = await Bun.file(configPath).text();
|
|
448
|
+
expect(content).toContain("name: scripted-project");
|
|
449
|
+
expect(content).toContain("# Overstory configuration");
|
|
450
|
+
});
|
|
451
|
+
});
|
package/src/commands/init.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI command: ov init [--force]
|
|
2
|
+
* CLI command: ov init [--force] [--yes|-y] [--name <name>]
|
|
3
3
|
*
|
|
4
4
|
* Scaffolds the `.overstory/` directory in the current project with:
|
|
5
5
|
* - config.yaml (serialized from DEFAULT_CONFIG)
|
|
@@ -516,6 +516,8 @@ export async function writeOverstoryReadme(overstoryPath: string): Promise<void>
|
|
|
516
516
|
}
|
|
517
517
|
|
|
518
518
|
export interface InitOptions {
|
|
519
|
+
yes?: boolean;
|
|
520
|
+
name?: string;
|
|
519
521
|
force?: boolean;
|
|
520
522
|
}
|
|
521
523
|
|
|
@@ -527,7 +529,7 @@ function printCreated(relativePath: string): void {
|
|
|
527
529
|
}
|
|
528
530
|
|
|
529
531
|
/**
|
|
530
|
-
* Entry point for `ov init [--force]`.
|
|
532
|
+
* Entry point for `ov init [--force] [--yes|-y] [--name <name>]`.
|
|
531
533
|
*
|
|
532
534
|
* Scaffolds the .overstory/ directory structure in the current working directory.
|
|
533
535
|
*
|
|
@@ -535,6 +537,7 @@ function printCreated(relativePath: string): void {
|
|
|
535
537
|
*/
|
|
536
538
|
export async function initCommand(opts: InitOptions): Promise<void> {
|
|
537
539
|
const force = opts.force ?? false;
|
|
540
|
+
const yes = opts.yes ?? false;
|
|
538
541
|
const projectRoot = process.cwd();
|
|
539
542
|
const overstoryPath = join(projectRoot, OVERSTORY_DIR);
|
|
540
543
|
|
|
@@ -554,18 +557,19 @@ export async function initCommand(opts: InitOptions): Promise<void> {
|
|
|
554
557
|
// 1. Check if .overstory/ already exists
|
|
555
558
|
const existingDir = Bun.file(join(overstoryPath, "config.yaml"));
|
|
556
559
|
if (await existingDir.exists()) {
|
|
557
|
-
if (!force) {
|
|
560
|
+
if (!force && !yes) {
|
|
558
561
|
process.stdout.write(
|
|
559
562
|
"Warning: .overstory/ already initialized in this project.\n" +
|
|
560
|
-
"Use --force to reinitialize.\n",
|
|
563
|
+
"Use --force or --yes to reinitialize.\n",
|
|
561
564
|
);
|
|
562
565
|
return;
|
|
563
566
|
}
|
|
564
|
-
|
|
567
|
+
const flag = yes ? "--yes" : "--force";
|
|
568
|
+
process.stdout.write(`Reinitializing .overstory/ (${flag})\n\n`);
|
|
565
569
|
}
|
|
566
570
|
|
|
567
571
|
// 2. Detect project info
|
|
568
|
-
const projectName = await detectProjectName(projectRoot);
|
|
572
|
+
const projectName = opts.name ?? (await detectProjectName(projectRoot));
|
|
569
573
|
const canonicalBranch = await detectCanonicalBranch(projectRoot);
|
|
570
574
|
|
|
571
575
|
process.stdout.write(`Initializing overstory for "${projectName}"...\n\n`);
|
|
@@ -629,7 +633,7 @@ export async function initCommand(opts: InitOptions): Promise<void> {
|
|
|
629
633
|
printCreated(`${OVERSTORY_DIR}/README.md`);
|
|
630
634
|
|
|
631
635
|
// 8. Migrate existing SQLite databases on --force reinit
|
|
632
|
-
if (force) {
|
|
636
|
+
if (force || yes) {
|
|
633
637
|
const migrated = await migrateExistingDatabases(overstoryPath);
|
|
634
638
|
for (const dbName of migrated) {
|
|
635
639
|
printSuccess("Migrated", dbName);
|
|
@@ -546,6 +546,8 @@ describe("inspectCommand", () => {
|
|
|
546
546
|
const out = output();
|
|
547
547
|
|
|
548
548
|
const parsed = JSON.parse(out);
|
|
549
|
+
expect(parsed.success).toBe(true);
|
|
550
|
+
expect(parsed.command).toBe("inspect");
|
|
549
551
|
expect(parsed.session.agentName).toBe("builder-1");
|
|
550
552
|
expect(parsed.timeSinceLastActivity).toBeGreaterThan(0);
|
|
551
553
|
});
|
package/src/commands/inspect.ts
CHANGED
|
@@ -10,7 +10,8 @@ import { Command } from "commander";
|
|
|
10
10
|
import { loadConfig } from "../config.ts";
|
|
11
11
|
import { ValidationError } from "../errors.ts";
|
|
12
12
|
import { createEventStore } from "../events/store.ts";
|
|
13
|
-
import {
|
|
13
|
+
import { jsonOutput } from "../json.ts";
|
|
14
|
+
import { accent, color } from "../logging/color.ts";
|
|
14
15
|
import { createMetricsStore } from "../metrics/store.ts";
|
|
15
16
|
import { openSessionStore } from "../sessions/compat.ts";
|
|
16
17
|
import type { AgentSession, StoredEvent, ToolStats } from "../types.ts";
|
|
@@ -252,21 +253,21 @@ export function printInspectData(data: InspectData): void {
|
|
|
252
253
|
const w = process.stdout.write.bind(process.stdout);
|
|
253
254
|
const { session } = data;
|
|
254
255
|
|
|
255
|
-
w(`\nAgent Inspection: ${session.agentName}\n`);
|
|
256
|
+
w(`\nAgent Inspection: ${accent(session.agentName)}\n`);
|
|
256
257
|
w(`${"═".repeat(80)}\n\n`);
|
|
257
258
|
|
|
258
259
|
// Agent state and metadata
|
|
259
260
|
const stateIcon = getStateIcon(session.state);
|
|
260
261
|
w(`${stateIcon} State: ${session.state}\n`);
|
|
261
262
|
w(`Last activity: ${formatDuration(data.timeSinceLastActivity)} ago\n`);
|
|
262
|
-
w(`Task: ${session.taskId}\n`);
|
|
263
|
+
w(`Task: ${accent(session.taskId)}\n`);
|
|
263
264
|
w(`Capability: ${session.capability}\n`);
|
|
264
|
-
w(`Branch: ${session.branchName}\n`);
|
|
265
|
+
w(`Branch: ${accent(session.branchName)}\n`);
|
|
265
266
|
if (session.parentAgent) {
|
|
266
|
-
w(`Parent: ${session.parentAgent} (depth: ${session.depth})\n`);
|
|
267
|
+
w(`Parent: ${accent(session.parentAgent)} (depth: ${session.depth})\n`);
|
|
267
268
|
}
|
|
268
269
|
w(`Started: ${session.startedAt}\n`);
|
|
269
|
-
w(`Tmux: ${session.tmuxSession}\n`);
|
|
270
|
+
w(`Tmux: ${accent(session.tmuxSession)}\n`);
|
|
270
271
|
w("\n");
|
|
271
272
|
|
|
272
273
|
// Current file
|
|
@@ -376,7 +377,7 @@ async function executeInspect(agentName: string, opts: InspectOpts): Promise<voi
|
|
|
376
377
|
tmuxLines: 30,
|
|
377
378
|
});
|
|
378
379
|
if (json) {
|
|
379
|
-
|
|
380
|
+
jsonOutput("inspect", data as unknown as Record<string, unknown>);
|
|
380
381
|
} else {
|
|
381
382
|
printInspectData(data);
|
|
382
383
|
}
|
|
@@ -386,7 +387,7 @@ async function executeInspect(agentName: string, opts: InspectOpts): Promise<voi
|
|
|
386
387
|
// Single snapshot
|
|
387
388
|
const data = await gatherInspectData(root, agentName, { limit, noTmux, tmuxLines: 30 });
|
|
388
389
|
if (json) {
|
|
389
|
-
|
|
390
|
+
jsonOutput("inspect", data as unknown as Record<string, unknown>);
|
|
390
391
|
} else {
|
|
391
392
|
printInspectData(data);
|
|
392
393
|
}
|
|
@@ -281,13 +281,12 @@ describe("logsCommand", () => {
|
|
|
281
281
|
});
|
|
282
282
|
|
|
283
283
|
// Parse JSON output
|
|
284
|
-
const parsed
|
|
285
|
-
expect(Array.isArray(parsed)).toBe(true);
|
|
284
|
+
const parsed = JSON.parse(output.trim()) as { entries: LogEvent[] };
|
|
285
|
+
expect(Array.isArray(parsed.entries)).toBe(true);
|
|
286
286
|
|
|
287
|
-
|
|
288
|
-
expect(
|
|
289
|
-
expect(
|
|
290
|
-
expect(arr[1]?.event).toBe("spawn.failed");
|
|
287
|
+
expect(parsed.entries).toHaveLength(2);
|
|
288
|
+
expect(parsed.entries[0]?.event).toBe("tool.start");
|
|
289
|
+
expect(parsed.entries[1]?.event).toBe("spawn.failed");
|
|
291
290
|
});
|
|
292
291
|
|
|
293
292
|
test("filters by --since with ISO timestamp", async () => {
|
package/src/commands/logs.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { join } from "node:path";
|
|
|
13
13
|
import { Command } from "commander";
|
|
14
14
|
import { loadConfig } from "../config.ts";
|
|
15
15
|
import { ValidationError } from "../errors.ts";
|
|
16
|
+
import { jsonOutput } from "../json.ts";
|
|
16
17
|
import type { ColorFn } from "../logging/color.ts";
|
|
17
18
|
import { color } from "../logging/color.ts";
|
|
18
19
|
import type { LogEvent } from "../types.ts";
|
|
@@ -500,7 +501,7 @@ async function executeLogs(opts: LogsOpts): Promise<void> {
|
|
|
500
501
|
const limited = filtered.slice(-limit);
|
|
501
502
|
|
|
502
503
|
if (json) {
|
|
503
|
-
|
|
504
|
+
jsonOutput("logs", { entries: limited });
|
|
504
505
|
return;
|
|
505
506
|
}
|
|
506
507
|
|
|
@@ -10,6 +10,7 @@ import { mkdir, mkdtemp, readdir, rm } from "node:fs/promises";
|
|
|
10
10
|
import { tmpdir } from "node:os";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { createEventStore } from "../events/store.ts";
|
|
13
|
+
import { stripAnsi } from "../logging/color.ts";
|
|
13
14
|
import { createMailClient } from "../mail/client.ts";
|
|
14
15
|
import { createMailStore } from "../mail/store.ts";
|
|
15
16
|
import type { StoredEvent } from "../types.ts";
|
|
@@ -851,9 +852,9 @@ describe("mailCommand", () => {
|
|
|
851
852
|
]);
|
|
852
853
|
|
|
853
854
|
expect(output).toContain("Broadcast sent to 3 recipients (@all)");
|
|
854
|
-
expect(output).toContain("→ builder-1");
|
|
855
|
-
expect(output).toContain("→ builder-2");
|
|
856
|
-
expect(output).toContain("→ scout-1");
|
|
855
|
+
expect(stripAnsi(output)).toContain("→ builder-1");
|
|
856
|
+
expect(stripAnsi(output)).toContain("→ builder-2");
|
|
857
|
+
expect(stripAnsi(output)).toContain("→ scout-1");
|
|
857
858
|
expect(output).not.toContain("orchestrator"); // sender excluded
|
|
858
859
|
|
|
859
860
|
// Verify messages were actually stored
|
|
@@ -881,8 +882,8 @@ describe("mailCommand", () => {
|
|
|
881
882
|
]);
|
|
882
883
|
|
|
883
884
|
expect(output).toContain("Broadcast sent to 2 recipients (@builders)");
|
|
884
|
-
expect(output).toContain("→ builder-1");
|
|
885
|
-
expect(output).toContain("→ builder-2");
|
|
885
|
+
expect(stripAnsi(output)).toContain("→ builder-1");
|
|
886
|
+
expect(stripAnsi(output)).toContain("→ builder-2");
|
|
886
887
|
expect(output).not.toContain("scout-1");
|
|
887
888
|
|
|
888
889
|
// Verify messages
|
|
@@ -909,7 +910,7 @@ describe("mailCommand", () => {
|
|
|
909
910
|
]);
|
|
910
911
|
|
|
911
912
|
expect(output).toContain("Broadcast sent to 1 recipient (@scouts)");
|
|
912
|
-
expect(output).toContain("→ scout-1");
|
|
913
|
+
expect(stripAnsi(output)).toContain("→ scout-1");
|
|
913
914
|
|
|
914
915
|
const store = createMailStore(join(tempDir, ".overstory", "mail.db"));
|
|
915
916
|
const client = createMailClient(store);
|
|
@@ -935,8 +936,8 @@ describe("mailCommand", () => {
|
|
|
935
936
|
]);
|
|
936
937
|
|
|
937
938
|
expect(output).toContain("Broadcast sent to 2 recipients (@builder)");
|
|
938
|
-
expect(output).toContain("→ builder-1");
|
|
939
|
-
expect(output).toContain("→ builder-2");
|
|
939
|
+
expect(stripAnsi(output)).toContain("→ builder-1");
|
|
940
|
+
expect(stripAnsi(output)).toContain("→ builder-2");
|
|
940
941
|
});
|
|
941
942
|
|
|
942
943
|
test("sender is excluded from broadcast recipients", async () => {
|
|
@@ -956,8 +957,8 @@ describe("mailCommand", () => {
|
|
|
956
957
|
]);
|
|
957
958
|
|
|
958
959
|
expect(output).toContain("Broadcast sent to 1 recipient (@builders)");
|
|
959
|
-
expect(output).toContain("→ builder-2");
|
|
960
|
-
expect(output).not.toContain("builder-1");
|
|
960
|
+
expect(stripAnsi(output)).toContain("→ builder-2");
|
|
961
|
+
expect(stripAnsi(output)).not.toContain("→ builder-1");
|
|
961
962
|
|
|
962
963
|
const store = createMailStore(join(tempDir, ".overstory", "mail.db"));
|
|
963
964
|
const client = createMailClient(store);
|
package/src/commands/mail.ts
CHANGED
|
@@ -11,7 +11,8 @@ import { Command } from "commander";
|
|
|
11
11
|
import { resolveProjectRoot } from "../config.ts";
|
|
12
12
|
import { ValidationError } from "../errors.ts";
|
|
13
13
|
import { createEventStore } from "../events/store.ts";
|
|
14
|
-
import {
|
|
14
|
+
import { jsonOutput } from "../json.ts";
|
|
15
|
+
import { accent, printHint, printSuccess } from "../logging/color.ts";
|
|
15
16
|
import { isGroupAddress, resolveGroupAddress } from "../mail/broadcast.ts";
|
|
16
17
|
import { createMailClient } from "../mail/client.ts";
|
|
17
18
|
import { createMailStore } from "../mail/store.ts";
|
|
@@ -36,7 +37,7 @@ function formatMessage(msg: MailMessage): string {
|
|
|
36
37
|
const readMarker = msg.read ? " " : "*";
|
|
37
38
|
const priorityTag = msg.priority !== "normal" ? ` [${msg.priority.toUpperCase()}]` : "";
|
|
38
39
|
const lines: string[] = [
|
|
39
|
-
`${readMarker} ${msg.id} From: ${msg.from} → To: ${msg.to}${priorityTag}`,
|
|
40
|
+
`${readMarker} ${accent(msg.id)} From: ${accent(msg.from)} → To: ${accent(msg.to)}${priorityTag}`,
|
|
40
41
|
` Subject: ${msg.subject} (${msg.type})`,
|
|
41
42
|
` ${msg.body}`,
|
|
42
43
|
];
|
|
@@ -369,9 +370,7 @@ async function handleSend(opts: SendOpts, cwd: string): Promise<void> {
|
|
|
369
370
|
|
|
370
371
|
// Output broadcast summary
|
|
371
372
|
if (opts.json) {
|
|
372
|
-
|
|
373
|
-
`${JSON.stringify({ messageIds, recipientCount: recipients.length })}\n`,
|
|
374
|
-
);
|
|
373
|
+
jsonOutput("mail send", { messageIds, recipientCount: recipients.length });
|
|
375
374
|
} else {
|
|
376
375
|
process.stdout.write(
|
|
377
376
|
`Broadcast sent to ${recipients.length} recipient${recipients.length === 1 ? "" : "s"} (${to})\n`,
|
|
@@ -379,7 +378,7 @@ async function handleSend(opts: SendOpts, cwd: string): Promise<void> {
|
|
|
379
378
|
for (let i = 0; i < recipients.length; i++) {
|
|
380
379
|
const recipient = recipients[i];
|
|
381
380
|
const msgId = messageIds[i];
|
|
382
|
-
process.stdout.write(` → ${recipient} (${msgId})\n`);
|
|
381
|
+
process.stdout.write(` → ${accent(recipient)} (${accent(msgId)})\n`);
|
|
383
382
|
}
|
|
384
383
|
}
|
|
385
384
|
|
|
@@ -428,7 +427,7 @@ async function handleSend(opts: SendOpts, cwd: string): Promise<void> {
|
|
|
428
427
|
}
|
|
429
428
|
|
|
430
429
|
if (opts.json) {
|
|
431
|
-
|
|
430
|
+
jsonOutput("mail send", { id });
|
|
432
431
|
} else {
|
|
433
432
|
printSuccess("Sent message", id);
|
|
434
433
|
}
|
|
@@ -557,7 +556,7 @@ async function handleCheck(opts: CheckOpts, cwd: string): Promise<void> {
|
|
|
557
556
|
const messages = client.check(agent);
|
|
558
557
|
|
|
559
558
|
if (json) {
|
|
560
|
-
|
|
559
|
+
jsonOutput("mail check", { messages });
|
|
561
560
|
} else if (messages.length === 0) {
|
|
562
561
|
printHint("No new messages");
|
|
563
562
|
} else {
|
|
@@ -592,7 +591,7 @@ function handleList(opts: ListOpts, cwd: string): void {
|
|
|
592
591
|
const messages = client.list({ from, to, unread });
|
|
593
592
|
|
|
594
593
|
if (json) {
|
|
595
|
-
|
|
594
|
+
jsonOutput("mail list", { messages });
|
|
596
595
|
} else if (messages.length === 0) {
|
|
597
596
|
printHint("No messages found");
|
|
598
597
|
} else {
|
|
@@ -614,7 +613,7 @@ function handleRead(id: string, cwd: string): void {
|
|
|
614
613
|
try {
|
|
615
614
|
const { alreadyRead } = client.markRead(id);
|
|
616
615
|
if (alreadyRead) {
|
|
617
|
-
printHint(`Message ${id} was already read`);
|
|
616
|
+
printHint(`Message ${accent(id)} was already read`);
|
|
618
617
|
} else {
|
|
619
618
|
printSuccess("Marked as read", id);
|
|
620
619
|
}
|
|
@@ -633,7 +632,7 @@ function handleReply(id: string, opts: ReplyOpts, cwd: string): void {
|
|
|
633
632
|
const replyId = client.reply(id, body, from);
|
|
634
633
|
|
|
635
634
|
if (opts.json) {
|
|
636
|
-
|
|
635
|
+
jsonOutput("mail reply", { id: replyId });
|
|
637
636
|
} else {
|
|
638
637
|
printSuccess("Reply sent", replyId);
|
|
639
638
|
}
|
|
@@ -673,7 +672,7 @@ function handlePurge(opts: PurgeOpts, cwd: string): void {
|
|
|
673
672
|
const purged = store.purge({ all, olderThanMs, agent });
|
|
674
673
|
|
|
675
674
|
if (json) {
|
|
676
|
-
|
|
675
|
+
jsonOutput("mail purge", { purged });
|
|
677
676
|
} else {
|
|
678
677
|
printSuccess(`Purged ${purged} message(s)`);
|
|
679
678
|
}
|