@ryanfw/prompt-orchestration-pipeline 1.3.0 → 1.3.2

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.
Files changed (50) hide show
  1. package/README.md +1 -0
  2. package/docs/pop-task-guide.md +45 -0
  3. package/package.json +3 -3
  4. package/src/core/__tests__/agent-step.test.ts +402 -35
  5. package/src/core/__tests__/task-runner.test.ts +104 -0
  6. package/src/core/agent-step.ts +295 -41
  7. package/src/core/agent-types.ts +61 -0
  8. package/src/core/file-io.ts +8 -74
  9. package/src/core/orchestrator.ts +2 -1
  10. package/src/core/pipeline-definition.ts +1 -1
  11. package/src/core/pipeline-runner.ts +54 -3
  12. package/src/core/status-writer.ts +44 -26
  13. package/src/core/task-runner.ts +44 -1
  14. package/src/core/validation.ts +1 -1
  15. package/src/harness/__tests__/discovery.test.ts +235 -0
  16. package/src/harness/discovery.ts +109 -0
  17. package/src/harness/index.ts +22 -0
  18. package/src/harness/mcp-io-server.ts +1 -1
  19. package/src/ui/client/hooks/useJobListWithUpdates.ts +16 -1
  20. package/src/ui/dist/assets/{index-D7hzshSS.js → index--RH3sAt3.js} +129 -1
  21. package/src/ui/dist/assets/index--RH3sAt3.js.map +1 -0
  22. package/src/ui/dist/assets/style-CSSKuMOe.css +2 -0
  23. package/src/ui/dist/index.html +2 -2
  24. package/src/ui/embedded-assets.js +6 -6
  25. package/src/ui/pages/Code.tsx +135 -0
  26. package/src/ui/server/__tests__/gate-endpoints.test.ts +90 -0
  27. package/src/ui/server/__tests__/job-control-endpoints.test.ts +188 -0
  28. package/src/ui/server/__tests__/path-containment.test.ts +54 -0
  29. package/src/ui/server/__tests__/status-corruption.test.ts +55 -0
  30. package/src/ui/server/config-bridge.ts +1 -0
  31. package/src/ui/server/endpoints/__tests__/upload-endpoints.test.ts +77 -0
  32. package/src/ui/server/endpoints/gate-endpoints.ts +17 -1
  33. package/src/ui/server/endpoints/job-control-endpoints.ts +36 -2
  34. package/src/ui/server/endpoints/upload-endpoints.ts +13 -3
  35. package/src/ui/server/utils/http-utils.ts +6 -1
  36. package/src/ui/server/utils/path-containment.ts +18 -0
  37. package/src/ui/server/utils/status-corruption.ts +27 -0
  38. package/src/harness/__tests__/descriptors.test.ts +0 -378
  39. package/src/harness/__tests__/executor.test.ts +0 -193
  40. package/src/harness/__tests__/resolve.test.ts +0 -200
  41. package/src/harness/__tests__/types.test.ts +0 -297
  42. package/src/harness/descriptors/claude.ts +0 -132
  43. package/src/harness/descriptors/codex.ts +0 -126
  44. package/src/harness/descriptors/index.ts +0 -10
  45. package/src/harness/descriptors/opencode.ts +0 -147
  46. package/src/harness/executor.ts +0 -128
  47. package/src/harness/resolve.ts +0 -176
  48. package/src/harness/types.ts +0 -100
  49. package/src/ui/dist/assets/index-D7hzshSS.js.map +0 -1
  50. package/src/ui/dist/assets/style-BUFg3Sth.css +0 -2
@@ -1,200 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from "bun:test";
2
- import { mkdtempSync, writeFileSync, chmodSync, rmSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import {
6
- healedPath,
7
- binEnvVar,
8
- harnessBinName,
9
- resolveHarnessBinary,
10
- discoverHarnesses,
11
- applyHarnessDiscovery,
12
- } from "../resolve.ts";
13
- import { DESCRIPTORS } from "../descriptors/index.ts";
14
- import type { HarnessDescriptor, HarnessEvent, HarnessName, HarnessProbe } from "../types.ts";
15
-
16
- function stubDescriptor(overrides: Partial<HarnessDescriptor> & { name: HarnessName }): HarnessDescriptor {
17
- return {
18
- versionArgv: [overrides.binName ?? overrides.name, "--version"],
19
- buildArgv: () => [overrides.binName ?? overrides.name],
20
- buildEnv: () => ({ env: {} }),
21
- parseEvents: () => [] as HarnessEvent[],
22
- extractFinalMessage: () => "",
23
- extractUsage: () => undefined,
24
- extractCostUsd: () => undefined,
25
- extractSessionId: () => undefined,
26
- ...overrides,
27
- };
28
- }
29
-
30
- /** Create a temp dir holding an executable that prints `output` and exits with `code`. */
31
- function makeFakeCli(name: string, output: string, code = 0): { dir: string; bin: string } {
32
- const dir = mkdtempSync(join(tmpdir(), "resolve-test-"));
33
- const bin = join(dir, name);
34
- writeFileSync(bin, `#!/bin/sh\nprintf '%s' '${output.replace(/'/g, "")}'\nexit ${code}\n`);
35
- chmodSync(bin, 0o755);
36
- return { dir, bin };
37
- }
38
-
39
- describe("healedPath", () => {
40
- let existingDir: string;
41
-
42
- beforeEach(() => {
43
- existingDir = mkdtempSync(join(tmpdir(), "healed-"));
44
- });
45
- afterEach(() => {
46
- rmSync(existingDir, { recursive: true, force: true });
47
- });
48
-
49
- it("prepends an existing extra dir and preserves the base PATH", () => {
50
- const result = healedPath([existingDir], "/usr/bin:/bin");
51
- expect(result.split(":")[0]).toBe(existingDir);
52
- expect(result.endsWith("/usr/bin:/bin")).toBe(true);
53
- });
54
-
55
- it("skips a dir that does not exist", () => {
56
- const result = healedPath(["/no/such/dir/xyz"], "/usr/bin");
57
- expect(result.includes("/no/such/dir/xyz")).toBe(false);
58
- });
59
-
60
- it("does not duplicate a dir already on the base PATH", () => {
61
- const result = healedPath([existingDir], `${existingDir}:/bin`);
62
- expect(result.split(":").filter((d) => d === existingDir).length).toBe(1);
63
- });
64
- });
65
-
66
- describe("binEnvVar", () => {
67
- it("maps a harness name to its uppercase override variable", () => {
68
- expect(binEnvVar("opencode")).toBe("POP_OPENCODE_BIN");
69
- expect(binEnvVar("claude")).toBe("POP_CLAUDE_BIN");
70
- expect(binEnvVar("codex")).toBe("POP_CODEX_BIN");
71
- });
72
- });
73
-
74
- describe("harnessBinName", () => {
75
- it("uses binName when set, else the harness name", () => {
76
- expect(harnessBinName(stubDescriptor({ name: "opencode", binName: "opencode" }))).toBe("opencode");
77
- expect(harnessBinName(stubDescriptor({ name: "claude" }))).toBe("claude");
78
- });
79
- });
80
-
81
- describe("resolveHarnessBinary", () => {
82
- let cli: { dir: string; bin: string };
83
-
84
- beforeEach(() => {
85
- cli = makeFakeCli("faketool", "ok");
86
- });
87
- afterEach(() => {
88
- rmSync(cli.dir, { recursive: true, force: true });
89
- });
90
-
91
- it("returns the override env var when it points at an existing file", () => {
92
- const descriptor = stubDescriptor({ name: "opencode", binName: "faketool" });
93
- const resolved = resolveHarnessBinary(descriptor, { POP_OPENCODE_BIN: cli.bin, PATH: "" });
94
- expect(resolved).toBe(cli.bin);
95
- });
96
-
97
- it("resolves via a known install dir when not on PATH", () => {
98
- const descriptor = stubDescriptor({ name: "opencode", binName: "faketool", binDirs: [cli.dir] });
99
- const resolved = resolveHarnessBinary(descriptor, { PATH: "" });
100
- expect(resolved).toBe(cli.bin);
101
- });
102
-
103
- it("returns null when the CLI cannot be found", () => {
104
- const descriptor = stubDescriptor({ name: "opencode", binName: "definitely-not-a-real-cli-xyz" });
105
- expect(resolveHarnessBinary(descriptor, { PATH: "" })).toBeNull();
106
- });
107
- });
108
-
109
- describe("descriptor auth interpreters", () => {
110
- it("claude reads loggedIn from JSON, true and false", () => {
111
- const i = DESCRIPTORS.claude.interpretAuthStatus!;
112
- expect(i({ exitCode: 0, stdout: '{"loggedIn": true, "email": "x"}', stderr: "" })).toBe(true);
113
- expect(i({ exitCode: 0, stdout: '{"loggedIn": false}', stderr: "" })).toBe(false);
114
- });
115
-
116
- it("claude returns null on unparseable, undeterminable output", () => {
117
- const i = DESCRIPTORS.claude.interpretAuthStatus!;
118
- expect(i({ exitCode: 0, stdout: "weird non-json", stderr: "" })).toBeNull();
119
- });
120
-
121
- it("codex distinguishes logged-in from not-logged-in", () => {
122
- const i = DESCRIPTORS.codex.interpretAuthStatus!;
123
- expect(i({ exitCode: 0, stdout: "Logged in using ChatGPT", stderr: "" })).toBe(true);
124
- expect(i({ exitCode: 0, stdout: "Not logged in", stderr: "" })).toBe(false);
125
- expect(i({ exitCode: 1, stdout: "", stderr: "" })).toBeNull();
126
- });
127
-
128
- it("opencode counts credentials, ignoring ANSI codes", () => {
129
- const i = DESCRIPTORS.opencode.interpretAuthStatus!;
130
- expect(i({ exitCode: 0, stdout: "└ 5 credentials", stderr: "" })).toBe(true);
131
- expect(i({ exitCode: 0, stdout: "0 credentials", stderr: "" })).toBe(false);
132
- expect(i({ exitCode: 1, stdout: "", stderr: "" })).toBeNull();
133
- });
134
- });
135
-
136
- describe("discoverHarnesses", () => {
137
- let cli: { dir: string; bin: string };
138
-
139
- beforeEach(() => {
140
- cli = makeFakeCli("faketool", "AUTH_OK");
141
- });
142
- afterEach(() => {
143
- rmSync(cli.dir, { recursive: true, force: true });
144
- });
145
-
146
- it("reports resolved path and auth result for a found CLI", async () => {
147
- const descriptor = stubDescriptor({
148
- name: "opencode",
149
- binName: "faketool",
150
- binDirs: [cli.dir],
151
- authStatusArgv: ["status"],
152
- interpretAuthStatus: ({ stdout }) => (stdout.includes("AUTH_OK") ? true : null),
153
- });
154
- const descriptors = { opencode: descriptor, claude: descriptor, codex: descriptor } as Record<HarnessName, HarnessDescriptor>;
155
-
156
- const probes = await discoverHarnesses(descriptors, { PATH: "" });
157
-
158
- expect(probes.opencode.binPath).toBe(cli.bin);
159
- expect(probes.opencode.available).toBe(true);
160
- expect(probes.opencode.authenticated).toBe(true);
161
- });
162
-
163
- it("marks a missing CLI unavailable with null auth", async () => {
164
- const descriptor = stubDescriptor({ name: "codex", binName: "missing-cli-xyz" });
165
- const descriptors = { opencode: descriptor, claude: descriptor, codex: descriptor } as Record<HarnessName, HarnessDescriptor>;
166
-
167
- const probes = await discoverHarnesses(descriptors, { PATH: "" });
168
-
169
- expect(probes.codex.available).toBe(false);
170
- expect(probes.codex.binPath).toBeNull();
171
- expect(probes.codex.authenticated).toBeNull();
172
- });
173
- });
174
-
175
- describe("applyHarnessDiscovery", () => {
176
- function probe(name: HarnessName, p: Partial<HarnessProbe>): HarnessProbe {
177
- return { name, binPath: null, available: false, authenticated: null, ...p };
178
- }
179
-
180
- it("caches resolved bins, heals PATH, and warns without blocking", () => {
181
- const env: Record<string, string | undefined> = { PATH: "/bin" };
182
- const probes: Record<HarnessName, HarnessProbe> = {
183
- opencode: probe("opencode", { binPath: "/x/opencode", available: true, authenticated: true }),
184
- claude: probe("claude", { available: false }),
185
- codex: probe("codex", { binPath: "/y/codex", available: true, authenticated: false }),
186
- };
187
-
188
- const messages = applyHarnessDiscovery(probes, env);
189
-
190
- expect(env.POP_OPENCODE_BIN).toBe("/x/opencode");
191
- expect(env.POP_CODEX_BIN).toBe("/y/codex");
192
- expect(env.POP_CLAUDE_BIN).toBeUndefined();
193
- expect(env.PATH!.endsWith("/bin")).toBe(true);
194
-
195
- const byName = (n: string) => messages.find((m) => m.message.includes(`"${n}"`))!;
196
- expect(byName("opencode").level).toBe("info");
197
- expect(byName("claude").level).toBe("warn");
198
- expect(byName("codex").level).toBe("warn");
199
- });
200
- });
@@ -1,297 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import type {
3
- HarnessName,
4
- McpServerConnection,
5
- HarnessUsage,
6
- HarnessEvent,
7
- HarnessRunOptions,
8
- HarnessRunResult,
9
- HarnessDescriptor,
10
- AgentEntryConfig,
11
- AgentStepResult,
12
- } from "../types.ts";
13
-
14
- describe("HarnessName", () => {
15
- it("accepts all three valid harness names", () => {
16
- const names: HarnessName[] = ["claude", "codex", "opencode"];
17
- expect(names).toHaveLength(3);
18
- });
19
- });
20
-
21
- describe("McpServerConnection", () => {
22
- it("constructs with url and token", () => {
23
- const conn: McpServerConnection = {
24
- url: "http://127.0.0.1:9000/mcp",
25
- token: "ephemeral-secret",
26
- };
27
- expect(conn.url).toBe("http://127.0.0.1:9000/mcp");
28
- expect(conn.token).toBe("ephemeral-secret");
29
- });
30
- });
31
-
32
- describe("HarnessUsage", () => {
33
- it("constructs with all token counts", () => {
34
- const usage: HarnessUsage = {
35
- inputTokens: 100,
36
- outputTokens: 200,
37
- totalTokens: 300,
38
- };
39
- expect(usage.inputTokens).toBe(100);
40
- expect(usage.outputTokens).toBe(200);
41
- expect(usage.totalTokens).toBe(300);
42
- });
43
- });
44
-
45
- describe("HarnessEvent", () => {
46
- it("constructs a text event", () => {
47
- const event: HarnessEvent = { type: "text", raw: {}, text: "hello" };
48
- expect(event.type).toBe("text");
49
- expect(event.text).toBe("hello");
50
- });
51
-
52
- it("constructs a tool_call event", () => {
53
- const event: HarnessEvent = {
54
- type: "tool_call",
55
- raw: {},
56
- tool: "read_file",
57
- };
58
- expect(event.type).toBe("tool_call");
59
- expect(event.tool).toBe("read_file");
60
- });
61
-
62
- it("constructs a tool_result event", () => {
63
- const event: HarnessEvent = { type: "tool_result", raw: {} };
64
- expect(event.type).toBe("tool_result");
65
- });
66
-
67
- it("constructs a system event", () => {
68
- const event: HarnessEvent = { type: "system", raw: {} };
69
- expect(event.type).toBe("system");
70
- });
71
-
72
- it("constructs a result event", () => {
73
- const event: HarnessEvent = { type: "result", raw: {} };
74
- expect(event.type).toBe("result");
75
- });
76
-
77
- it("constructs a raw event", () => {
78
- const event: HarnessEvent = { type: "raw", raw: { data: 1 } };
79
- expect(event.type).toBe("raw");
80
- expect(event.raw).toEqual({ data: 1 });
81
- });
82
- });
83
-
84
- describe("HarnessRunOptions", () => {
85
- it("constructs with required fields only", () => {
86
- const opts: HarnessRunOptions = {
87
- harness: "claude",
88
- prompt: "do something",
89
- cwd: "/tmp/work",
90
- };
91
- expect(opts.harness).toBe("claude");
92
- expect(opts.prompt).toBe("do something");
93
- expect(opts.cwd).toBe("/tmp/work");
94
- expect(opts.model).toBeUndefined();
95
- expect(opts.mcp).toBeUndefined();
96
- expect(opts.timeoutMs).toBeUndefined();
97
- expect(opts.signal).toBeUndefined();
98
- expect(opts.onEvent).toBeUndefined();
99
- });
100
-
101
- it("constructs with all optional fields", () => {
102
- const controller = new AbortController();
103
- const opts: HarnessRunOptions = {
104
- harness: "opencode",
105
- prompt: "test",
106
- cwd: "/tmp",
107
- model: "gpt-4",
108
- mcp: { url: "http://127.0.0.1:3000/mcp", token: "tok" },
109
- timeoutMs: 30_000,
110
- signal: controller.signal,
111
- onEvent: (e) => {
112
- void e;
113
- },
114
- };
115
- expect(opts.harness).toBe("opencode");
116
- expect(opts.model).toBe("gpt-4");
117
- expect(opts.mcp?.url).toBe("http://127.0.0.1:3000/mcp");
118
- expect(opts.timeoutMs).toBe(30_000);
119
- expect(opts.signal).toBe(controller.signal);
120
- expect(typeof opts.onEvent).toBe("function");
121
- });
122
- });
123
-
124
- describe("HarnessRunResult", () => {
125
- it("constructs with required fields only", () => {
126
- const result: HarnessRunResult = {
127
- finalMessage: "done",
128
- events: [],
129
- exitCode: 0,
130
- };
131
- expect(result.finalMessage).toBe("done");
132
- expect(result.events).toHaveLength(0);
133
- expect(result.exitCode).toBe(0);
134
- expect(result.sessionId).toBeUndefined();
135
- expect(result.usage).toBeUndefined();
136
- expect(result.costUsd).toBeUndefined();
137
- });
138
-
139
- it("constructs with all optional fields", () => {
140
- const result: HarnessRunResult = {
141
- finalMessage: "completed",
142
- sessionId: "sess-123",
143
- usage: { inputTokens: 50, outputTokens: 100, totalTokens: 150 },
144
- costUsd: 0.005,
145
- events: [{ type: "text", raw: {}, text: "hi" }],
146
- exitCode: 0,
147
- };
148
- expect(result.sessionId).toBe("sess-123");
149
- expect(result.usage?.totalTokens).toBe(150);
150
- expect(result.costUsd).toBe(0.005);
151
- expect(result.events).toHaveLength(1);
152
- });
153
- });
154
-
155
- describe("HarnessDescriptor", () => {
156
- it("constructs a minimal descriptor with correct method signatures", () => {
157
- const descriptor: HarnessDescriptor = {
158
- name: "claude",
159
- versionArgv: ["claude", "--version"],
160
- buildArgv: (o) => ["claude", "-p", o.prompt],
161
- buildEnv: () => ({ env: {} }),
162
- parseEvents: (lines) =>
163
- lines.map((raw) => ({ type: "raw" as const, raw })),
164
- extractFinalMessage: (events) =>
165
- events.find((e) => e.type === "text")?.text ?? "",
166
- extractUsage: () => undefined,
167
- extractCostUsd: () => undefined,
168
- extractSessionId: () => undefined,
169
- };
170
- expect(descriptor.name).toBe("claude");
171
- expect(descriptor.versionArgv).toEqual(["claude", "--version"]);
172
- expect(descriptor.buildArgv({ harness: "claude", prompt: "hi", cwd: "/tmp" })).toEqual([
173
- "claude", "-p", "hi",
174
- ]);
175
- expect(descriptor.buildEnv({ harness: "claude", prompt: "hi", cwd: "/tmp" })).toEqual({
176
- env: {},
177
- });
178
- expect(descriptor.parseEvents([{ foo: 1 }])).toEqual([
179
- { type: "raw", raw: { foo: 1 } },
180
- ]);
181
- expect(descriptor.extractFinalMessage([])).toBe("");
182
- expect(descriptor.extractUsage([])).toBeUndefined();
183
- expect(descriptor.extractCostUsd([])).toBeUndefined();
184
- expect(descriptor.extractSessionId([])).toBeUndefined();
185
- });
186
-
187
- it("supports tmpFiles in buildEnv", () => {
188
- const descriptor: HarnessDescriptor = {
189
- name: "opencode",
190
- versionArgv: ["opencode", "--version"],
191
- buildArgv: () => [],
192
- buildEnv: () => ({
193
- env: { OPENCODE_PERMISSION: "allow" },
194
- tmpFiles: [{ path: "/tmp/config.json", content: "{}" }],
195
- }),
196
- parseEvents: () => [],
197
- extractFinalMessage: () => "",
198
- extractUsage: () => undefined,
199
- extractCostUsd: () => undefined,
200
- extractSessionId: () => undefined,
201
- };
202
- const envResult = descriptor.buildEnv({
203
- harness: "opencode",
204
- prompt: "",
205
- cwd: "/tmp",
206
- });
207
- expect(envResult.env.OPENCODE_PERMISSION).toBe("allow");
208
- expect(envResult.tmpFiles).toHaveLength(1);
209
- expect(envResult.tmpFiles?.[0]?.path).toBe("/tmp/config.json");
210
- });
211
- });
212
-
213
- describe("AgentEntryConfig", () => {
214
- it("constructs with required fields only", () => {
215
- const config: AgentEntryConfig = { harness: "codex" };
216
- expect(config.harness).toBe("codex");
217
- expect(config.model).toBeUndefined();
218
- expect(config.prompt).toBeUndefined();
219
- expect(config.promptFrom).toBeUndefined();
220
- expect(config.cwd).toBeUndefined();
221
- expect(config.io).toBeUndefined();
222
- expect(config.timeoutMs).toBeUndefined();
223
- expect(config.captureDiff).toBeUndefined();
224
- });
225
-
226
- it("constructs with all optional fields", () => {
227
- const config: AgentEntryConfig = {
228
- harness: "claude",
229
- model: "sonnet",
230
- prompt: "do it",
231
- cwd: "/work",
232
- io: true,
233
- timeoutMs: 60_000,
234
- captureDiff: true,
235
- };
236
- expect(config.harness).toBe("claude");
237
- expect(config.model).toBe("sonnet");
238
- expect(config.prompt).toBe("do it");
239
- expect(config.cwd).toBe("/work");
240
- expect(config.io).toBe(true);
241
- expect(config.timeoutMs).toBe(60_000);
242
- expect(config.captureDiff).toBe(true);
243
- });
244
-
245
- it("supports promptFrom instead of prompt", () => {
246
- const config: AgentEntryConfig = {
247
- harness: "opencode",
248
- promptFrom: "my-prompt-artifact",
249
- };
250
- expect(config.prompt).toBeUndefined();
251
- expect(config.promptFrom).toBe("my-prompt-artifact");
252
- });
253
- });
254
-
255
- describe("AgentStepResult", () => {
256
- it("constructs a success result", () => {
257
- const result: AgentStepResult = {
258
- ok: true,
259
- finalMessage: "all done",
260
- artifactsWritten: ["output.md"],
261
- };
262
- expect(result.ok).toBe(true);
263
- expect(result.finalMessage).toBe("all done");
264
- expect(result.artifactsWritten).toEqual(["output.md"]);
265
- expect(result.usage).toBeUndefined();
266
- expect(result.costUsd).toBeUndefined();
267
- expect(result.sessionId).toBeUndefined();
268
- expect(result.error).toBeUndefined();
269
- });
270
-
271
- it("constructs a failure result", () => {
272
- const result: AgentStepResult = {
273
- ok: false,
274
- finalMessage: "",
275
- artifactsWritten: [],
276
- error: "harness crashed",
277
- };
278
- expect(result.ok).toBe(false);
279
- expect(result.finalMessage).toBe("");
280
- expect(result.error).toBe("harness crashed");
281
- });
282
-
283
- it("constructs a result with all optional fields", () => {
284
- const result: AgentStepResult = {
285
- ok: true,
286
- finalMessage: "done",
287
- artifactsWritten: ["a.md", "b.patch"],
288
- usage: { inputTokens: 100, outputTokens: 200, totalTokens: 300 },
289
- costUsd: 0.01,
290
- sessionId: "sess-456",
291
- };
292
- expect(result.usage?.inputTokens).toBe(100);
293
- expect(result.costUsd).toBe(0.01);
294
- expect(result.sessionId).toBe("sess-456");
295
- expect(result.artifactsWritten).toHaveLength(2);
296
- });
297
- });
@@ -1,132 +0,0 @@
1
- import type { HarnessDescriptor } from "../types.ts";
2
-
3
- export const claudeDescriptor: HarnessDescriptor = {
4
- name: "claude",
5
- versionArgv: ["claude", "--version"],
6
- binName: "claude",
7
- binDirs: ["~/.local/bin"],
8
- authStatusArgv: ["auth", "status"],
9
-
10
- // `claude auth status` returns JSON, e.g. {"loggedIn": true, ...}.
11
- interpretAuthStatus({ stdout }) {
12
- try {
13
- const parsed = JSON.parse(stdout) as { loggedIn?: unknown };
14
- if (typeof parsed.loggedIn === "boolean") return parsed.loggedIn;
15
- } catch {
16
- // fall through to a text heuristic
17
- }
18
- if (/not\s+logged\s+in|not\s+authenticated/i.test(stdout)) return false;
19
- if (/logged\s+in|authenticated/i.test(stdout)) return true;
20
- return null;
21
- },
22
-
23
- buildArgv(o) {
24
- return [
25
- "claude",
26
- "-p",
27
- o.prompt,
28
- "--output-format",
29
- "stream-json",
30
- "--verbose",
31
- "--dangerously-skip-permissions",
32
- ...(o.model ? ["--model", o.model] : []),
33
- ...(o.mcp
34
- ? [
35
- "--mcp-config",
36
- JSON.stringify({
37
- mcpServers: {
38
- popio: {
39
- type: "http",
40
- url: o.mcp.url,
41
- headers: { Authorization: `Bearer ${o.mcp.token}` },
42
- },
43
- },
44
- }),
45
- ]
46
- : []),
47
- ];
48
- },
49
-
50
- buildEnv() {
51
- return { env: {} };
52
- },
53
-
54
- parseEvents(lines) {
55
- return lines.map((raw) => {
56
- const obj = raw as Record<string, unknown>;
57
- const type = typeof obj.type === "string" ? obj.type : undefined;
58
- switch (type) {
59
- case "text":
60
- return { type: "text" as const, raw, text: String(obj.text ?? "") };
61
- case "tool_call":
62
- return {
63
- type: "tool_call" as const,
64
- raw,
65
- tool: typeof obj.tool === "string" ? obj.tool : undefined,
66
- };
67
- case "tool_result":
68
- return { type: "tool_result" as const, raw };
69
- case "system":
70
- return { type: "system" as const, raw };
71
- case "result":
72
- return { type: "result" as const, raw };
73
- default:
74
- return { type: "raw" as const, raw };
75
- }
76
- });
77
- },
78
-
79
- extractFinalMessage(events) {
80
- for (let i = events.length - 1; i >= 0; i--) {
81
- const e = events[i]!;
82
- if (e.type === "text" || e.type === "result") {
83
- if (e.text) return e.text;
84
- }
85
- }
86
- return "";
87
- },
88
-
89
- extractUsage(events) {
90
- for (let i = events.length - 1; i >= 0; i--) {
91
- const e = events[i]!;
92
- if (e.type === "result") {
93
- const raw = e.raw as Record<string, unknown>;
94
- const usage = raw.usage as Record<string, unknown> | undefined;
95
- if (usage && typeof usage.input_tokens === "number" && typeof usage.output_tokens === "number") {
96
- return {
97
- inputTokens: usage.input_tokens as number,
98
- outputTokens: usage.output_tokens as number,
99
- totalTokens: (usage.input_tokens as number) + (usage.output_tokens as number),
100
- };
101
- }
102
- }
103
- }
104
- return undefined;
105
- },
106
-
107
- extractCostUsd(events) {
108
- for (let i = events.length - 1; i >= 0; i--) {
109
- const e = events[i]!;
110
- if (e.type === "result") {
111
- const raw = e.raw as Record<string, unknown>;
112
- if (typeof raw.total_cost_usd === "number") {
113
- return raw.total_cost_usd as number;
114
- }
115
- }
116
- }
117
- return undefined;
118
- },
119
-
120
- extractSessionId(events) {
121
- for (let i = events.length - 1; i >= 0; i--) {
122
- const e = events[i]!;
123
- if (e.type === "result") {
124
- const raw = e.raw as Record<string, unknown>;
125
- if (typeof raw.session_id === "string") {
126
- return raw.session_id as string;
127
- }
128
- }
129
- }
130
- return undefined;
131
- },
132
- };