@os-eco/overstory-cli 0.7.4 → 0.7.6
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 +10 -8
- package/package.json +1 -1
- package/src/commands/agents.ts +21 -3
- package/src/commands/completions.ts +7 -1
- package/src/commands/coordinator.test.ts +3 -1
- package/src/commands/coordinator.ts +6 -3
- package/src/commands/costs.test.ts +45 -2
- package/src/commands/costs.ts +42 -13
- package/src/commands/doctor.ts +3 -1
- package/src/commands/init.test.ts +366 -27
- package/src/commands/init.ts +194 -2
- package/src/commands/monitor.ts +4 -3
- package/src/commands/supervisor.ts +4 -3
- package/src/doctor/providers.test.ts +373 -0
- package/src/doctor/providers.ts +250 -0
- package/src/doctor/types.ts +2 -1
- package/src/e2e/init-sling-lifecycle.test.ts +12 -7
- package/src/index.ts +11 -2
- package/src/metrics/pricing.ts +57 -2
- package/src/metrics/store.test.ts +38 -0
- package/src/metrics/store.ts +10 -0
- package/src/metrics/transcript.test.ts +84 -2
- package/src/metrics/transcript.ts +1 -1
- package/src/runtimes/claude.test.ts +40 -0
- package/src/runtimes/claude.ts +8 -1
- package/src/runtimes/copilot.test.ts +507 -0
- package/src/runtimes/copilot.ts +226 -0
- package/src/runtimes/pi.test.ts +28 -0
- package/src/runtimes/pi.ts +5 -1
- package/src/runtimes/registry.test.ts +20 -0
- package/src/runtimes/registry.ts +2 -0
- package/src/runtimes/types.ts +2 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// GitHub Copilot runtime adapter for overstory's AgentRuntime interface.
|
|
2
|
+
// Implements the AgentRuntime contract for the `copilot` CLI (GitHub Copilot).
|
|
3
|
+
|
|
4
|
+
import { mkdir } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import type { ResolvedModel } from "../types.ts";
|
|
7
|
+
import type {
|
|
8
|
+
AgentRuntime,
|
|
9
|
+
HooksDef,
|
|
10
|
+
OverlayContent,
|
|
11
|
+
ReadyState,
|
|
12
|
+
SpawnOpts,
|
|
13
|
+
TranscriptSummary,
|
|
14
|
+
} from "./types.ts";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* GitHub Copilot runtime adapter.
|
|
18
|
+
*
|
|
19
|
+
* Implements AgentRuntime for the `copilot` CLI (GitHub Copilot coding agent).
|
|
20
|
+
* Key differences from Claude Code:
|
|
21
|
+
* - Uses `--allow-all-tools` instead of `--permission-mode bypassPermissions`
|
|
22
|
+
* - No `--append-system-prompt` flag (ignored when provided)
|
|
23
|
+
* - Instruction file lives at `.github/copilot-instructions.md`
|
|
24
|
+
* - No hooks deployment (hooks param unused in deployConfig)
|
|
25
|
+
* - Transcript parser handles both Claude-style and Pi-style formats
|
|
26
|
+
*/
|
|
27
|
+
export class CopilotRuntime implements AgentRuntime {
|
|
28
|
+
/** Unique identifier for this runtime. */
|
|
29
|
+
readonly id = "copilot";
|
|
30
|
+
|
|
31
|
+
/** Relative path to the instruction file within a worktree. */
|
|
32
|
+
readonly instructionPath = ".github/copilot-instructions.md";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build the shell command string to spawn an interactive Copilot agent.
|
|
36
|
+
*
|
|
37
|
+
* Maps SpawnOpts to `copilot` CLI flags:
|
|
38
|
+
* - `model` → `--model <model>`
|
|
39
|
+
* - `permissionMode === "bypass"` → `--allow-all-tools`
|
|
40
|
+
* - `permissionMode === "ask"` → no permission flag added
|
|
41
|
+
* - `appendSystemPrompt` and `appendSystemPromptFile` are IGNORED —
|
|
42
|
+
* the `copilot` CLI has no equivalent flag.
|
|
43
|
+
*
|
|
44
|
+
* The `cwd` and `env` fields of SpawnOpts are handled by the tmux session
|
|
45
|
+
* creator, not embedded in the command string.
|
|
46
|
+
*
|
|
47
|
+
* @param opts - Spawn options (model, permissionMode; appendSystemPrompt ignored)
|
|
48
|
+
* @returns Shell command string suitable for tmux new-session -c
|
|
49
|
+
*/
|
|
50
|
+
buildSpawnCommand(opts: SpawnOpts): string {
|
|
51
|
+
let cmd = `copilot --model ${opts.model}`;
|
|
52
|
+
|
|
53
|
+
if (opts.permissionMode === "bypass") {
|
|
54
|
+
cmd += " --allow-all-tools";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// appendSystemPrompt and appendSystemPromptFile are intentionally ignored.
|
|
58
|
+
// The copilot CLI has no --append-system-prompt equivalent.
|
|
59
|
+
|
|
60
|
+
return cmd;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build the argv array for a headless one-shot Copilot invocation.
|
|
65
|
+
*
|
|
66
|
+
* Returns an argv array suitable for `Bun.spawn()`. The `-p` flag passes
|
|
67
|
+
* the prompt and `--allow-all-tools` grants full tool access. An optional
|
|
68
|
+
* `--model` flag can override the default model.
|
|
69
|
+
*
|
|
70
|
+
* Used by merge/resolver.ts and watchdog/triage.ts for AI-assisted operations.
|
|
71
|
+
*
|
|
72
|
+
* @param prompt - The prompt to pass via `-p`
|
|
73
|
+
* @param model - Optional model override
|
|
74
|
+
* @returns Argv array for Bun.spawn
|
|
75
|
+
*/
|
|
76
|
+
buildPrintCommand(prompt: string, model?: string): string[] {
|
|
77
|
+
const cmd = ["copilot", "-p", prompt, "--allow-all-tools"];
|
|
78
|
+
if (model !== undefined) {
|
|
79
|
+
cmd.push("--model", model);
|
|
80
|
+
}
|
|
81
|
+
return cmd;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Deploy per-agent instructions to a worktree.
|
|
86
|
+
*
|
|
87
|
+
* For Copilot this writes only the instruction file:
|
|
88
|
+
* - `.github/copilot-instructions.md` — the agent's task-specific overlay.
|
|
89
|
+
* Skipped when overlay is undefined.
|
|
90
|
+
*
|
|
91
|
+
* The `hooks` parameter is unused — Copilot does not support Claude Code's
|
|
92
|
+
* hook mechanism, so no settings file is deployed.
|
|
93
|
+
*
|
|
94
|
+
* @param worktreePath - Absolute path to the agent's git worktree
|
|
95
|
+
* @param overlay - Overlay content to write as copilot-instructions.md, or undefined to skip
|
|
96
|
+
* @param _hooks - Unused for Copilot runtime
|
|
97
|
+
*/
|
|
98
|
+
async deployConfig(
|
|
99
|
+
worktreePath: string,
|
|
100
|
+
overlay: OverlayContent | undefined,
|
|
101
|
+
_hooks: HooksDef,
|
|
102
|
+
): Promise<void> {
|
|
103
|
+
if (overlay) {
|
|
104
|
+
const githubDir = join(worktreePath, ".github");
|
|
105
|
+
await mkdir(githubDir, { recursive: true });
|
|
106
|
+
await Bun.write(join(githubDir, "copilot-instructions.md"), overlay.content);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// No hook deployment for Copilot — the runtime has no hook mechanism.
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Detect Copilot TUI readiness from a tmux pane content snapshot.
|
|
114
|
+
*
|
|
115
|
+
* Detection requires both a prompt indicator AND a status bar indicator
|
|
116
|
+
* (matched case-insensitively). No trust dialog phase exists for Copilot.
|
|
117
|
+
*
|
|
118
|
+
* - Prompt: U+276F (❯) or "copilot" in pane content (case-insensitive)
|
|
119
|
+
* - Status bar: "shift+tab" or "esc" in pane content (case-insensitive)
|
|
120
|
+
*
|
|
121
|
+
* @param paneContent - Captured tmux pane content to analyze
|
|
122
|
+
* @returns Current readiness phase (never "dialog" for Copilot)
|
|
123
|
+
*/
|
|
124
|
+
detectReady(paneContent: string): ReadyState {
|
|
125
|
+
const lower = paneContent.toLowerCase();
|
|
126
|
+
|
|
127
|
+
// Prompt indicator: ❯ character or "copilot" keyword in pane.
|
|
128
|
+
const hasPrompt = paneContent.includes("\u276f") || lower.includes("copilot");
|
|
129
|
+
|
|
130
|
+
// Status bar indicator: keyboard shortcut hints visible when TUI is ready.
|
|
131
|
+
const hasStatusBar = lower.includes("shift+tab") || lower.includes("esc");
|
|
132
|
+
|
|
133
|
+
if (hasPrompt && hasStatusBar) {
|
|
134
|
+
return { phase: "ready" };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { phase: "loading" };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Parse a Copilot transcript JSONL file into normalized token usage.
|
|
142
|
+
*
|
|
143
|
+
* Handles two transcript formats:
|
|
144
|
+
* - Claude-style: `type === "assistant"` entries with `message.usage.input_tokens`
|
|
145
|
+
* and `message.usage.output_tokens`; model from `message.model`
|
|
146
|
+
* - Pi-style: `type === "message_end"` entries with top-level `inputTokens`
|
|
147
|
+
* and `outputTokens`
|
|
148
|
+
*
|
|
149
|
+
* Also checks the top-level `model` field on any entry for model identification.
|
|
150
|
+
* Returns null if the file does not exist or cannot be parsed.
|
|
151
|
+
*
|
|
152
|
+
* @param path - Absolute path to the transcript JSONL file
|
|
153
|
+
* @returns Aggregated token usage, or null if unavailable
|
|
154
|
+
*/
|
|
155
|
+
async parseTranscript(path: string): Promise<TranscriptSummary | null> {
|
|
156
|
+
const file = Bun.file(path);
|
|
157
|
+
if (!(await file.exists())) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const text = await file.text();
|
|
163
|
+
const lines = text.split("\n").filter((l) => l.trim().length > 0);
|
|
164
|
+
|
|
165
|
+
let inputTokens = 0;
|
|
166
|
+
let outputTokens = 0;
|
|
167
|
+
let model = "";
|
|
168
|
+
|
|
169
|
+
for (const line of lines) {
|
|
170
|
+
let entry: Record<string, unknown>;
|
|
171
|
+
try {
|
|
172
|
+
entry = JSON.parse(line) as Record<string, unknown>;
|
|
173
|
+
} catch {
|
|
174
|
+
// Skip malformed lines — transcripts may have partial writes.
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Check top-level model field (Pi model_change and other events).
|
|
179
|
+
if (typeof entry.model === "string" && entry.model) {
|
|
180
|
+
model = entry.model;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (entry.type === "assistant") {
|
|
184
|
+
// Claude-style: message.usage.input_tokens / output_tokens.
|
|
185
|
+
const message = entry.message as Record<string, unknown> | undefined;
|
|
186
|
+
const usage = message?.usage as Record<string, unknown> | undefined;
|
|
187
|
+
if (typeof usage?.input_tokens === "number") {
|
|
188
|
+
inputTokens += usage.input_tokens;
|
|
189
|
+
}
|
|
190
|
+
if (typeof usage?.output_tokens === "number") {
|
|
191
|
+
outputTokens += usage.output_tokens;
|
|
192
|
+
}
|
|
193
|
+
// Model may also live inside message object.
|
|
194
|
+
if (typeof message?.model === "string" && message.model) {
|
|
195
|
+
model = message.model;
|
|
196
|
+
}
|
|
197
|
+
} else if (entry.type === "message_end") {
|
|
198
|
+
// Pi-style: top-level inputTokens / outputTokens.
|
|
199
|
+
if (typeof entry.inputTokens === "number") {
|
|
200
|
+
inputTokens += entry.inputTokens;
|
|
201
|
+
}
|
|
202
|
+
if (typeof entry.outputTokens === "number") {
|
|
203
|
+
outputTokens += entry.outputTokens;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return { inputTokens, outputTokens, model };
|
|
209
|
+
} catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Build runtime-specific environment variables for model/provider routing.
|
|
216
|
+
*
|
|
217
|
+
* Returns the provider environment variables from the resolved model, or an
|
|
218
|
+
* empty object if none are set.
|
|
219
|
+
*
|
|
220
|
+
* @param model - Resolved model with optional provider env vars
|
|
221
|
+
* @returns Environment variable map (may be empty)
|
|
222
|
+
*/
|
|
223
|
+
buildEnv(model: ResolvedModel): Record<string, string> {
|
|
224
|
+
return model.env ?? {};
|
|
225
|
+
}
|
|
226
|
+
}
|
package/src/runtimes/pi.test.ts
CHANGED
|
@@ -126,6 +126,34 @@ describe("PiRuntime", () => {
|
|
|
126
126
|
);
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
+
test("with appendSystemPromptFile uses $(cat ...) expansion", () => {
|
|
130
|
+
const opts: SpawnOpts = {
|
|
131
|
+
model: "opus",
|
|
132
|
+
permissionMode: "bypass",
|
|
133
|
+
cwd: "/project",
|
|
134
|
+
env: {},
|
|
135
|
+
appendSystemPromptFile: "/project/.overstory/agent-defs/coordinator.md",
|
|
136
|
+
};
|
|
137
|
+
const cmd = runtime.buildSpawnCommand(opts);
|
|
138
|
+
expect(cmd).toBe(
|
|
139
|
+
`pi --model anthropic/claude-opus-4-6 --append-system-prompt "$(cat '/project/.overstory/agent-defs/coordinator.md')"`,
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("appendSystemPromptFile takes precedence over appendSystemPrompt", () => {
|
|
144
|
+
const opts: SpawnOpts = {
|
|
145
|
+
model: "opus",
|
|
146
|
+
permissionMode: "bypass",
|
|
147
|
+
cwd: "/project",
|
|
148
|
+
env: {},
|
|
149
|
+
appendSystemPromptFile: "/project/.overstory/agent-defs/coordinator.md",
|
|
150
|
+
appendSystemPrompt: "This inline content should be ignored",
|
|
151
|
+
};
|
|
152
|
+
const cmd = runtime.buildSpawnCommand(opts);
|
|
153
|
+
expect(cmd).toContain("$(cat ");
|
|
154
|
+
expect(cmd).not.toContain("This inline content should be ignored");
|
|
155
|
+
});
|
|
156
|
+
|
|
129
157
|
test("without appendSystemPrompt omits the flag", () => {
|
|
130
158
|
const opts: SpawnOpts = {
|
|
131
159
|
model: "haiku",
|
package/src/runtimes/pi.ts
CHANGED
|
@@ -75,7 +75,11 @@ export class PiRuntime implements AgentRuntime {
|
|
|
75
75
|
buildSpawnCommand(opts: SpawnOpts): string {
|
|
76
76
|
let cmd = `pi --model ${this.expandModel(opts.model)}`;
|
|
77
77
|
|
|
78
|
-
if (opts.
|
|
78
|
+
if (opts.appendSystemPromptFile) {
|
|
79
|
+
// Read from file at shell expansion time — avoids tmux command length limits.
|
|
80
|
+
const escaped = opts.appendSystemPromptFile.replace(/'/g, "'\\''");
|
|
81
|
+
cmd += ` --append-system-prompt "$(cat '${escaped}')"`;
|
|
82
|
+
} else if (opts.appendSystemPrompt) {
|
|
79
83
|
// POSIX single-quote escape: end quote, backslash-quote, start quote.
|
|
80
84
|
const escaped = opts.appendSystemPrompt.replace(/'/g, "'\\''");
|
|
81
85
|
cmd += ` --append-system-prompt '${escaped}'`;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import type { OverstoryConfig } from "../types.ts";
|
|
3
3
|
import { ClaudeRuntime } from "./claude.ts";
|
|
4
|
+
import { CopilotRuntime } from "./copilot.ts";
|
|
4
5
|
import { PiRuntime } from "./pi.ts";
|
|
5
6
|
import { getRuntime } from "./registry.ts";
|
|
6
7
|
|
|
@@ -83,4 +84,23 @@ describe("getRuntime", () => {
|
|
|
83
84
|
// Should use default anthropic mappings
|
|
84
85
|
expect(runtime.expandModel("sonnet")).toBe("anthropic/claude-sonnet-4-6");
|
|
85
86
|
});
|
|
87
|
+
|
|
88
|
+
it("returns CopilotRuntime when name is 'copilot'", () => {
|
|
89
|
+
const runtime = getRuntime("copilot");
|
|
90
|
+
expect(runtime).toBeInstanceOf(CopilotRuntime);
|
|
91
|
+
expect(runtime.id).toBe("copilot");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("uses config.runtime.default 'copilot' when name is omitted", () => {
|
|
95
|
+
const config = { runtime: { default: "copilot" } } as OverstoryConfig;
|
|
96
|
+
const runtime = getRuntime(undefined, config);
|
|
97
|
+
expect(runtime).toBeInstanceOf(CopilotRuntime);
|
|
98
|
+
expect(runtime.id).toBe("copilot");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("copilot runtime returns a new instance on each call", () => {
|
|
102
|
+
const a = getRuntime("copilot");
|
|
103
|
+
const b = getRuntime("copilot");
|
|
104
|
+
expect(a).not.toBe(b);
|
|
105
|
+
});
|
|
86
106
|
});
|
package/src/runtimes/registry.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import type { OverstoryConfig } from "../types.ts";
|
|
5
5
|
import { ClaudeRuntime } from "./claude.ts";
|
|
6
|
+
import { CopilotRuntime } from "./copilot.ts";
|
|
6
7
|
import { PiRuntime } from "./pi.ts";
|
|
7
8
|
import type { AgentRuntime } from "./types.ts";
|
|
8
9
|
|
|
@@ -10,6 +11,7 @@ import type { AgentRuntime } from "./types.ts";
|
|
|
10
11
|
const runtimes = new Map<string, () => AgentRuntime>([
|
|
11
12
|
["claude", () => new ClaudeRuntime()],
|
|
12
13
|
["pi", () => new PiRuntime()],
|
|
14
|
+
["copilot", () => new CopilotRuntime()],
|
|
13
15
|
]);
|
|
14
16
|
|
|
15
17
|
/**
|
package/src/runtimes/types.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface SpawnOpts {
|
|
|
15
15
|
systemPrompt?: string;
|
|
16
16
|
/** Optional system prompt suffix appended after the base instructions. */
|
|
17
17
|
appendSystemPrompt?: string;
|
|
18
|
+
/** Path to a file whose contents are appended as system prompt (avoids tmux command length limits). */
|
|
19
|
+
appendSystemPromptFile?: string;
|
|
18
20
|
/** Working directory for the spawned process. */
|
|
19
21
|
cwd: string;
|
|
20
22
|
/** Additional environment variables to pass to the spawned process. */
|