@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
|
@@ -4,7 +4,15 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { AgentError } from "../errors.ts";
|
|
6
6
|
import type { OverlayConfig, QualityGate } from "../types.ts";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
formatQualityGatesBash,
|
|
9
|
+
formatQualityGatesCapabilities,
|
|
10
|
+
formatQualityGatesInline,
|
|
11
|
+
formatQualityGatesSteps,
|
|
12
|
+
generateOverlay,
|
|
13
|
+
isCanonicalRoot,
|
|
14
|
+
writeOverlay,
|
|
15
|
+
} from "./overlay.ts";
|
|
8
16
|
|
|
9
17
|
const SAMPLE_BASE_DEFINITION = `# Builder Agent
|
|
10
18
|
|
|
@@ -674,3 +682,150 @@ describe("isCanonicalRoot", () => {
|
|
|
674
682
|
expect(isCanonicalRoot(worktreePath, canonicalRoot)).toBe(false);
|
|
675
683
|
});
|
|
676
684
|
});
|
|
685
|
+
|
|
686
|
+
describe("formatQualityGatesInline", () => {
|
|
687
|
+
test("formats default gates as inline backtick list", () => {
|
|
688
|
+
const result = formatQualityGatesInline(undefined);
|
|
689
|
+
expect(result).toBe("`bun test`, `bun run lint`, `bun run typecheck`");
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
test("formats custom gates as inline backtick list", () => {
|
|
693
|
+
const gates: QualityGate[] = [
|
|
694
|
+
{ name: "Test", command: "pytest", description: "all tests pass" },
|
|
695
|
+
{ name: "Lint", command: "ruff check .", description: "no lint errors" },
|
|
696
|
+
];
|
|
697
|
+
const result = formatQualityGatesInline(gates);
|
|
698
|
+
expect(result).toBe("`pytest`, `ruff check .`");
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
test("falls back to defaults for empty array", () => {
|
|
702
|
+
const result = formatQualityGatesInline([]);
|
|
703
|
+
expect(result).toContain("`bun test`");
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
describe("formatQualityGatesSteps", () => {
|
|
708
|
+
test("formats default gates as numbered steps", () => {
|
|
709
|
+
const result = formatQualityGatesSteps(undefined);
|
|
710
|
+
expect(result).toContain("1. Run `bun test`");
|
|
711
|
+
expect(result).toContain("2. Run `bun run lint`");
|
|
712
|
+
expect(result).toContain("3. Run `bun run typecheck`");
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
test("formats custom gates as numbered steps", () => {
|
|
716
|
+
const gates: QualityGate[] = [
|
|
717
|
+
{ name: "Build", command: "cargo build", description: "compilation succeeds" },
|
|
718
|
+
{ name: "Test", command: "cargo test", description: "all tests pass" },
|
|
719
|
+
];
|
|
720
|
+
const result = formatQualityGatesSteps(gates);
|
|
721
|
+
expect(result).toBe(
|
|
722
|
+
"1. Run `cargo build` -- compilation succeeds.\n2. Run `cargo test` -- all tests pass.",
|
|
723
|
+
);
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
describe("formatQualityGatesBash", () => {
|
|
728
|
+
test("formats as fenced bash block with aligned comments", () => {
|
|
729
|
+
const result = formatQualityGatesBash(undefined);
|
|
730
|
+
expect(result).toContain("```bash");
|
|
731
|
+
expect(result).toContain("bun test");
|
|
732
|
+
expect(result).toContain("bun run lint");
|
|
733
|
+
expect(result).toContain("bun run typecheck");
|
|
734
|
+
expect(result).toContain("```");
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
test("capitalizes first letter of description in comments", () => {
|
|
738
|
+
const gates: QualityGate[] = [
|
|
739
|
+
{ name: "Test", command: "pytest", description: "all tests pass" },
|
|
740
|
+
];
|
|
741
|
+
const result = formatQualityGatesBash(gates);
|
|
742
|
+
expect(result).toContain("# All tests pass");
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
test("custom gates produce correct bash block", () => {
|
|
746
|
+
const gates: QualityGate[] = [
|
|
747
|
+
{ name: "Test", command: "npm test", description: "tests pass" },
|
|
748
|
+
{ name: "Lint", command: "npm run lint", description: "lint clean" },
|
|
749
|
+
];
|
|
750
|
+
const result = formatQualityGatesBash(gates);
|
|
751
|
+
expect(result).toContain("npm test");
|
|
752
|
+
expect(result).toContain("npm run lint");
|
|
753
|
+
expect(result).not.toContain("bun");
|
|
754
|
+
});
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
describe("formatQualityGatesCapabilities", () => {
|
|
758
|
+
test("formats as indented bullet list", () => {
|
|
759
|
+
const result = formatQualityGatesCapabilities(undefined);
|
|
760
|
+
expect(result).toContain(" - `bun test`");
|
|
761
|
+
expect(result).toContain(" - `bun run lint`");
|
|
762
|
+
expect(result).toContain(" - `bun run typecheck`");
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
test("custom gates produce correct capability bullets", () => {
|
|
766
|
+
const gates: QualityGate[] = [
|
|
767
|
+
{ name: "Test", command: "pytest", description: "run tests" },
|
|
768
|
+
{ name: "Type", command: "mypy .", description: "type check" },
|
|
769
|
+
];
|
|
770
|
+
const result = formatQualityGatesCapabilities(gates);
|
|
771
|
+
expect(result).toBe(" - `pytest` (run tests)\n - `mypy .` (type check)");
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
describe("quality gate placeholders in base definitions", () => {
|
|
776
|
+
test("QUALITY_GATE_INLINE in base definition gets replaced", async () => {
|
|
777
|
+
const config = makeConfig({
|
|
778
|
+
baseDefinition: "Run {{QUALITY_GATE_INLINE}} before closing.",
|
|
779
|
+
});
|
|
780
|
+
const output = await generateOverlay(config);
|
|
781
|
+
expect(output).toContain("`bun test`, `bun run lint`, `bun run typecheck`");
|
|
782
|
+
expect(output).not.toContain("{{QUALITY_GATE_INLINE}}");
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
test("QUALITY_GATE_STEPS in base definition gets replaced", async () => {
|
|
786
|
+
const config = makeConfig({
|
|
787
|
+
baseDefinition: "## Steps\n{{QUALITY_GATE_STEPS}}",
|
|
788
|
+
});
|
|
789
|
+
const output = await generateOverlay(config);
|
|
790
|
+
expect(output).toContain("1. Run `bun test`");
|
|
791
|
+
expect(output).not.toContain("{{QUALITY_GATE_STEPS}}");
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
test("QUALITY_GATE_BASH in base definition gets replaced", async () => {
|
|
795
|
+
const config = makeConfig({
|
|
796
|
+
baseDefinition: "## Workflow\n{{QUALITY_GATE_BASH}}",
|
|
797
|
+
});
|
|
798
|
+
const output = await generateOverlay(config);
|
|
799
|
+
expect(output).toContain("```bash");
|
|
800
|
+
expect(output).toContain("bun test");
|
|
801
|
+
expect(output).not.toContain("{{QUALITY_GATE_BASH}}");
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
test("QUALITY_GATE_CAPABILITIES in base definition gets replaced", async () => {
|
|
805
|
+
const config = makeConfig({
|
|
806
|
+
baseDefinition: "## Caps\n{{QUALITY_GATE_CAPABILITIES}}",
|
|
807
|
+
});
|
|
808
|
+
const output = await generateOverlay(config);
|
|
809
|
+
expect(output).toContain(" - `bun test`");
|
|
810
|
+
expect(output).not.toContain("{{QUALITY_GATE_CAPABILITIES}}");
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
test("custom quality gates in base definition get custom commands", async () => {
|
|
814
|
+
const gates: QualityGate[] = [
|
|
815
|
+
{ name: "Test", command: "pytest", description: "all tests pass" },
|
|
816
|
+
{ name: "Lint", command: "ruff check .", description: "no lint errors" },
|
|
817
|
+
];
|
|
818
|
+
const config = makeConfig({
|
|
819
|
+
capability: "builder",
|
|
820
|
+
qualityGates: gates,
|
|
821
|
+
baseDefinition:
|
|
822
|
+
"Run {{QUALITY_GATE_INLINE}} before closing.\n{{QUALITY_GATE_BASH}}\n{{QUALITY_GATE_STEPS}}",
|
|
823
|
+
});
|
|
824
|
+
const output = await generateOverlay(config);
|
|
825
|
+
expect(output).toContain("`pytest`, `ruff check .`");
|
|
826
|
+
expect(output).toContain("pytest");
|
|
827
|
+
expect(output).toContain("ruff check .");
|
|
828
|
+
expect(output).not.toContain("bun test");
|
|
829
|
+
expect(output).not.toContain("{{QUALITY_GATE");
|
|
830
|
+
});
|
|
831
|
+
});
|
package/src/agents/overlay.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdir } from "node:fs/promises";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { DEFAULT_QUALITY_GATES } from "../config.ts";
|
|
3
4
|
import { AgentError } from "../errors.ts";
|
|
4
5
|
import type { OverlayConfig, QualityGate } from "../types.ts";
|
|
5
6
|
|
|
@@ -76,12 +77,65 @@ Your parent has already gathered the context you need.
|
|
|
76
77
|
* a lightweight section that only tells them to close the issue and report.
|
|
77
78
|
* Writable agents get the full quality gates (tests, lint, build, commit).
|
|
78
79
|
*/
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Resolve quality gates: use provided gates if non-empty, otherwise fall back to defaults.
|
|
82
|
+
*/
|
|
83
|
+
function resolveGates(gates: QualityGate[] | undefined): QualityGate[] {
|
|
84
|
+
return gates && gates.length > 0 ? gates : DEFAULT_QUALITY_GATES;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Format quality gates as inline backtick-delimited commands for prose sections.
|
|
89
|
+
* Example: `bun test`, `bun run lint`, `bun run typecheck`
|
|
90
|
+
*/
|
|
91
|
+
export function formatQualityGatesInline(gates: QualityGate[] | undefined): string {
|
|
92
|
+
return resolveGates(gates)
|
|
93
|
+
.map((g) => `\`${g.command}\``)
|
|
94
|
+
.join(", ");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Format quality gates as a numbered step list for completion-protocol sections.
|
|
99
|
+
* Example:
|
|
100
|
+
* 1. Run `bun test` -- all tests must pass.
|
|
101
|
+
* 2. Run `bun run lint` -- lint and formatting must be clean.
|
|
102
|
+
*/
|
|
103
|
+
export function formatQualityGatesSteps(gates: QualityGate[] | undefined): string {
|
|
104
|
+
return resolveGates(gates)
|
|
105
|
+
.map((g, i) => `${i + 1}. Run \`${g.command}\` -- ${g.description}.`)
|
|
106
|
+
.join("\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Format quality gates as a bash code block for workflow sections.
|
|
111
|
+
* Example:
|
|
112
|
+
* ```bash
|
|
113
|
+
* bun test # All tests must pass
|
|
114
|
+
* bun run lint # Lint and format must be clean
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
export function formatQualityGatesBash(gates: QualityGate[] | undefined): string {
|
|
118
|
+
const resolved = resolveGates(gates);
|
|
119
|
+
// Pad commands to align comments
|
|
120
|
+
const maxLen = Math.max(...resolved.map((g) => g.command.length));
|
|
121
|
+
const lines = resolved.map((g) => {
|
|
122
|
+
const padded = g.command.padEnd(maxLen + 2);
|
|
123
|
+
return `${padded}# ${g.description[0]?.toUpperCase() ?? ""}${g.description.slice(1)}`;
|
|
124
|
+
});
|
|
125
|
+
return ["```bash", ...lines, "```"].join("\n");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Format quality gates as a bullet list for capabilities sections.
|
|
130
|
+
* Example:
|
|
131
|
+
* - `bun test` (run tests)
|
|
132
|
+
* - `bun run lint` (lint and format check via biome)
|
|
133
|
+
*/
|
|
134
|
+
export function formatQualityGatesCapabilities(gates: QualityGate[] | undefined): string {
|
|
135
|
+
return resolveGates(gates)
|
|
136
|
+
.map((g) => ` - \`${g.command}\` (${g.description})`)
|
|
137
|
+
.join("\n");
|
|
138
|
+
}
|
|
85
139
|
|
|
86
140
|
function formatQualityGates(config: OverlayConfig): string {
|
|
87
141
|
if (READ_ONLY_CAPABILITIES.has(config.capability)) {
|
|
@@ -99,7 +153,9 @@ function formatQualityGates(config: OverlayConfig): string {
|
|
|
99
153
|
}
|
|
100
154
|
|
|
101
155
|
const gates =
|
|
102
|
-
config.qualityGates && config.qualityGates.length > 0
|
|
156
|
+
config.qualityGates && config.qualityGates.length > 0
|
|
157
|
+
? config.qualityGates
|
|
158
|
+
: DEFAULT_QUALITY_GATES;
|
|
103
159
|
|
|
104
160
|
const gateLines = gates.map(
|
|
105
161
|
(gate, i) => `${i + 1}. **${gate.name}:** \`${gate.command}\` — ${gate.description}`,
|
|
@@ -220,6 +276,10 @@ export async function generateOverlay(config: OverlayConfig): Promise<string> {
|
|
|
220
276
|
"{{SPEC_INSTRUCTION}}": specInstruction,
|
|
221
277
|
"{{SKIP_SCOUT}}": config.skipScout ? SKIP_SCOUT_SECTION : "",
|
|
222
278
|
"{{BASE_DEFINITION}}": config.baseDefinition,
|
|
279
|
+
"{{QUALITY_GATE_INLINE}}": formatQualityGatesInline(config.qualityGates),
|
|
280
|
+
"{{QUALITY_GATE_STEPS}}": formatQualityGatesSteps(config.qualityGates),
|
|
281
|
+
"{{QUALITY_GATE_BASH}}": formatQualityGatesBash(config.qualityGates),
|
|
282
|
+
"{{QUALITY_GATE_CAPABILITIES}}": formatQualityGatesCapabilities(config.qualityGates),
|
|
223
283
|
"{{TRACKER_CLI}}": config.trackerCli ?? "bd",
|
|
224
284
|
"{{TRACKER_NAME}}": config.trackerName ?? "beads",
|
|
225
285
|
};
|
package/src/commands/agents.ts
CHANGED
|
@@ -8,7 +8,8 @@ import { join } from "node:path";
|
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
import { loadConfig } from "../config.ts";
|
|
10
10
|
import { ValidationError } from "../errors.ts";
|
|
11
|
-
import {
|
|
11
|
+
import { jsonOutput } from "../json.ts";
|
|
12
|
+
import { accent, color } from "../logging/color.ts";
|
|
12
13
|
import { openSessionStore } from "../sessions/compat.ts";
|
|
13
14
|
import { type AgentSession, SUPPORTED_CAPABILITIES } from "../types.ts";
|
|
14
15
|
|
|
@@ -166,10 +167,12 @@ function printAgents(agents: DiscoveredAgent[]): void {
|
|
|
166
167
|
|
|
167
168
|
for (const agent of agents) {
|
|
168
169
|
const icon = getStateIcon(agent.state);
|
|
169
|
-
w(` ${icon} ${agent.agentName} [${agent.capability}]\n`);
|
|
170
|
-
w(` State: ${agent.state} | Task: ${agent.taskId}\n`);
|
|
171
|
-
w(` Branch: ${agent.branchName}\n`);
|
|
172
|
-
w(
|
|
170
|
+
w(` ${icon} ${accent(agent.agentName)} [${agent.capability}]\n`);
|
|
171
|
+
w(` State: ${agent.state} | Task: ${accent(agent.taskId)}\n`);
|
|
172
|
+
w(` Branch: ${accent(agent.branchName)}\n`);
|
|
173
|
+
w(
|
|
174
|
+
` Parent: ${agent.parentAgent ? accent(agent.parentAgent) : "none"} | Depth: ${agent.depth}\n`,
|
|
175
|
+
);
|
|
173
176
|
|
|
174
177
|
if (agent.fileScope.length === 0) {
|
|
175
178
|
w(" Files: (unrestricted)\n");
|
|
@@ -220,7 +223,7 @@ export function createAgentsCommand(): Command {
|
|
|
220
223
|
});
|
|
221
224
|
|
|
222
225
|
if (opts.json) {
|
|
223
|
-
|
|
226
|
+
jsonOutput("agents discover", { agents });
|
|
224
227
|
} else {
|
|
225
228
|
printAgents(agents);
|
|
226
229
|
}
|
package/src/commands/clean.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { join } from "node:path";
|
|
|
25
25
|
import { loadConfig } from "../config.ts";
|
|
26
26
|
import { ValidationError } from "../errors.ts";
|
|
27
27
|
import { createEventStore } from "../events/store.ts";
|
|
28
|
+
import { jsonOutput } from "../json.ts";
|
|
28
29
|
import { printHint, printSuccess } from "../logging/color.ts";
|
|
29
30
|
import { createMulchClient } from "../mulch/client.ts";
|
|
30
31
|
import { openSessionStore } from "../sessions/compat.ts";
|
|
@@ -518,7 +519,7 @@ export async function cleanCommand(opts: CleanOptions): Promise<void> {
|
|
|
518
519
|
|
|
519
520
|
// Output
|
|
520
521
|
if (json) {
|
|
521
|
-
|
|
522
|
+
jsonOutput("clean", { ...result });
|
|
522
523
|
return;
|
|
523
524
|
}
|
|
524
525
|
|
|
@@ -203,54 +203,42 @@ describe("completionsCommand", () => {
|
|
|
203
203
|
});
|
|
204
204
|
|
|
205
205
|
it("should exit with error for missing shell argument", () => {
|
|
206
|
-
const
|
|
206
|
+
const originalExitCode = process.exitCode;
|
|
207
207
|
const originalStderr = process.stderr.write;
|
|
208
|
-
let exitCode: number | undefined;
|
|
209
208
|
let stderrOutput = "";
|
|
210
209
|
|
|
211
|
-
process.exit = mock((code?: string | number | null | undefined) => {
|
|
212
|
-
exitCode = typeof code === "number" ? code : 1;
|
|
213
|
-
throw new Error("process.exit called");
|
|
214
|
-
}) as never;
|
|
215
|
-
|
|
216
210
|
process.stderr.write = mock((chunk: unknown) => {
|
|
217
211
|
stderrOutput += String(chunk);
|
|
218
212
|
return true;
|
|
219
213
|
});
|
|
220
214
|
|
|
221
215
|
try {
|
|
222
|
-
|
|
223
|
-
expect(exitCode).toBe(1);
|
|
216
|
+
completionsCommand([]);
|
|
217
|
+
expect(process.exitCode).toBe(1);
|
|
224
218
|
expect(stderrOutput).toContain("missing shell argument");
|
|
225
219
|
} finally {
|
|
226
|
-
process.
|
|
220
|
+
process.exitCode = originalExitCode;
|
|
227
221
|
process.stderr.write = originalStderr;
|
|
228
222
|
}
|
|
229
223
|
});
|
|
230
224
|
|
|
231
225
|
it("should exit with error for unknown shell", () => {
|
|
232
|
-
const
|
|
226
|
+
const originalExitCode = process.exitCode;
|
|
233
227
|
const originalStderr = process.stderr.write;
|
|
234
|
-
let exitCode: number | undefined;
|
|
235
228
|
let stderrOutput = "";
|
|
236
229
|
|
|
237
|
-
process.exit = mock((code?: string | number | null | undefined) => {
|
|
238
|
-
exitCode = typeof code === "number" ? code : 1;
|
|
239
|
-
throw new Error("process.exit called");
|
|
240
|
-
}) as never;
|
|
241
|
-
|
|
242
230
|
process.stderr.write = mock((chunk: unknown) => {
|
|
243
231
|
stderrOutput += String(chunk);
|
|
244
232
|
return true;
|
|
245
233
|
});
|
|
246
234
|
|
|
247
235
|
try {
|
|
248
|
-
|
|
249
|
-
expect(exitCode).toBe(1);
|
|
236
|
+
completionsCommand(["powershell"]);
|
|
237
|
+
expect(process.exitCode).toBe(1);
|
|
250
238
|
expect(stderrOutput).toContain("unknown shell");
|
|
251
239
|
expect(stderrOutput).toContain("powershell");
|
|
252
240
|
} finally {
|
|
253
|
-
process.
|
|
241
|
+
process.exitCode = originalExitCode;
|
|
254
242
|
process.stderr.write = originalStderr;
|
|
255
243
|
}
|
|
256
244
|
});
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Command } from "commander";
|
|
8
|
+
import { printError } from "../logging/color.ts";
|
|
8
9
|
|
|
9
10
|
interface FlagDef {
|
|
10
11
|
name: string;
|
|
@@ -872,9 +873,9 @@ export function completionsCommand(args: string[]): void {
|
|
|
872
873
|
const shell = args[0];
|
|
873
874
|
|
|
874
875
|
if (!shell) {
|
|
875
|
-
|
|
876
|
-
process.
|
|
877
|
-
|
|
876
|
+
printError("missing shell argument", "Usage: ov --completions <bash|zsh|fish>");
|
|
877
|
+
process.exitCode = 1;
|
|
878
|
+
return;
|
|
878
879
|
}
|
|
879
880
|
|
|
880
881
|
let script: string;
|
|
@@ -889,9 +890,9 @@ export function completionsCommand(args: string[]): void {
|
|
|
889
890
|
script = generateFish();
|
|
890
891
|
break;
|
|
891
892
|
default:
|
|
892
|
-
|
|
893
|
-
process.
|
|
894
|
-
|
|
893
|
+
printError(`unknown shell '${shell}'`, "Supported shells: bash, zsh, fish");
|
|
894
|
+
process.exitCode = 1;
|
|
895
|
+
return;
|
|
895
896
|
}
|
|
896
897
|
|
|
897
898
|
process.stdout.write(script);
|
|
@@ -592,6 +592,8 @@ describe("startCoordinator", () => {
|
|
|
592
592
|
}
|
|
593
593
|
|
|
594
594
|
const parsed = JSON.parse(output) as Record<string, unknown>;
|
|
595
|
+
expect(parsed.success).toBe(true);
|
|
596
|
+
expect(parsed.command).toBe("coordinator start");
|
|
595
597
|
expect(parsed.agentName).toBe("coordinator");
|
|
596
598
|
expect(parsed.capability).toBe("coordinator");
|
|
597
599
|
expect(parsed.tmuxSession).toBe("overstory-test-project-coordinator");
|
|
@@ -761,6 +763,8 @@ describe("stopCoordinator", () => {
|
|
|
761
763
|
|
|
762
764
|
const output = await captureStdout(() => coordinatorCommand(["stop", "--json"], deps));
|
|
763
765
|
const parsed = JSON.parse(output) as Record<string, unknown>;
|
|
766
|
+
expect(parsed.success).toBe(true);
|
|
767
|
+
expect(parsed.command).toBe("coordinator stop");
|
|
764
768
|
expect(parsed.stopped).toBe(true);
|
|
765
769
|
expect(parsed.sessionId).toBe(session.id);
|
|
766
770
|
});
|
|
@@ -930,6 +934,8 @@ describe("statusCoordinator", () => {
|
|
|
930
934
|
const { deps } = makeDeps();
|
|
931
935
|
const output = await captureStdout(() => coordinatorCommand(["status", "--json"], deps));
|
|
932
936
|
const parsed = JSON.parse(output) as Record<string, unknown>;
|
|
937
|
+
expect(parsed.success).toBe(true);
|
|
938
|
+
expect(parsed.command).toBe("coordinator status");
|
|
933
939
|
expect(parsed.running).toBe(false);
|
|
934
940
|
});
|
|
935
941
|
|
|
@@ -951,6 +957,8 @@ describe("statusCoordinator", () => {
|
|
|
951
957
|
|
|
952
958
|
const output = await captureStdout(() => coordinatorCommand(["status", "--json"], deps));
|
|
953
959
|
const parsed = JSON.parse(output) as Record<string, unknown>;
|
|
960
|
+
expect(parsed.success).toBe(true);
|
|
961
|
+
expect(parsed.command).toBe("coordinator status");
|
|
954
962
|
expect(parsed.running).toBe(true);
|
|
955
963
|
expect(parsed.sessionId).toBe(session.id);
|
|
956
964
|
expect(parsed.state).toBe("working");
|
|
@@ -20,6 +20,7 @@ import { createIdentity, loadIdentity } from "../agents/identity.ts";
|
|
|
20
20
|
import { createManifestLoader, resolveModel } from "../agents/manifest.ts";
|
|
21
21
|
import { loadConfig } from "../config.ts";
|
|
22
22
|
import { AgentError, ValidationError } from "../errors.ts";
|
|
23
|
+
import { jsonOutput } from "../json.ts";
|
|
23
24
|
import { printHint, printSuccess, printWarning } from "../logging/color.ts";
|
|
24
25
|
import { openSessionStore } from "../sessions/compat.ts";
|
|
25
26
|
import { createRunStore } from "../sessions/store.ts";
|
|
@@ -462,7 +463,7 @@ async function startCoordinator(
|
|
|
462
463
|
};
|
|
463
464
|
|
|
464
465
|
if (json) {
|
|
465
|
-
|
|
466
|
+
jsonOutput("coordinator start", output);
|
|
466
467
|
} else {
|
|
467
468
|
printSuccess("Coordinator started");
|
|
468
469
|
process.stdout.write(` Tmux: ${tmuxSession}\n`);
|
|
@@ -564,9 +565,13 @@ async function stopCoordinator(opts: { json: boolean }, deps: CoordinatorDeps =
|
|
|
564
565
|
}
|
|
565
566
|
|
|
566
567
|
if (json) {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
568
|
+
jsonOutput("coordinator stop", {
|
|
569
|
+
stopped: true,
|
|
570
|
+
sessionId: session.id,
|
|
571
|
+
watchdogStopped,
|
|
572
|
+
monitorStopped,
|
|
573
|
+
runCompleted,
|
|
574
|
+
});
|
|
570
575
|
} else {
|
|
571
576
|
printSuccess("Coordinator stopped", session.id);
|
|
572
577
|
if (watchdogStopped) {
|
|
@@ -629,9 +634,7 @@ async function statusCoordinator(
|
|
|
629
634
|
session.state === "zombie"
|
|
630
635
|
) {
|
|
631
636
|
if (json) {
|
|
632
|
-
|
|
633
|
-
`${JSON.stringify({ running: false, watchdogRunning, monitorRunning })}\n`,
|
|
634
|
-
);
|
|
637
|
+
jsonOutput("coordinator status", { running: false, watchdogRunning, monitorRunning });
|
|
635
638
|
} else {
|
|
636
639
|
printHint("Coordinator is not running");
|
|
637
640
|
if (watchdogRunning) {
|
|
@@ -668,7 +671,7 @@ async function statusCoordinator(
|
|
|
668
671
|
};
|
|
669
672
|
|
|
670
673
|
if (json) {
|
|
671
|
-
|
|
674
|
+
jsonOutput("coordinator status", status);
|
|
672
675
|
} else {
|
|
673
676
|
const stateLabel = alive ? "running" : session.state;
|
|
674
677
|
process.stdout.write(`Coordinator: ${stateLabel}\n`);
|