@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,126 +0,0 @@
1
- import type { HarnessDescriptor } from "../types.ts";
2
-
3
- export const codexDescriptor: HarnessDescriptor = {
4
- name: "codex",
5
- versionArgv: ["codex", "--version"],
6
- binName: "codex",
7
- binDirs: ["/opt/homebrew/bin", "~/.local/bin"],
8
- authStatusArgv: ["login", "status"],
9
-
10
- // `codex login status` prints "Logged in using …" (exit 0) or "Not logged in".
11
- interpretAuthStatus({ exitCode, stdout, stderr }) {
12
- const text = `${stdout}\n${stderr}`;
13
- if (/not\s+logged\s+in/i.test(text)) return false;
14
- if (exitCode === 0 && /logged\s+in/i.test(text)) return true;
15
- return null;
16
- },
17
-
18
- buildArgv(o) {
19
- return [
20
- "codex",
21
- "exec",
22
- o.prompt,
23
- "--json",
24
- "--dangerously-bypass-approvals-and-sandbox",
25
- "--skip-git-repo-check",
26
- "-C",
27
- o.cwd,
28
- ...(o.model ? ["-m", o.model] : []),
29
- ...(o.mcp
30
- ? [
31
- "-c",
32
- `mcp_servers.popio.url="${o.mcp.url}"`,
33
- "-c",
34
- 'mcp_servers.popio.bearer_token_env_var="POP_MCP_TOKEN"',
35
- ]
36
- : []),
37
- ];
38
- },
39
-
40
- buildEnv(o) {
41
- const env: Record<string, string> = {};
42
- if (o.mcp) {
43
- env.POP_MCP_TOKEN = o.mcp.token;
44
- }
45
- return { env };
46
- },
47
-
48
- parseEvents(lines) {
49
- return lines.map((raw) => {
50
- const obj = raw as Record<string, unknown>;
51
- const type = typeof obj.type === "string" ? obj.type : undefined;
52
- switch (type) {
53
- case "text":
54
- return { type: "text" as const, raw, text: String(obj.text ?? "") };
55
- case "tool_call":
56
- return {
57
- type: "tool_call" as const,
58
- raw,
59
- tool: typeof obj.tool === "string" ? obj.tool : undefined,
60
- };
61
- case "tool_result":
62
- return { type: "tool_result" as const, raw };
63
- case "system":
64
- return { type: "system" as const, raw };
65
- case "result":
66
- return { type: "result" as const, raw };
67
- default:
68
- return { type: "raw" as const, raw };
69
- }
70
- });
71
- },
72
-
73
- extractFinalMessage(events) {
74
- for (let i = events.length - 1; i >= 0; i--) {
75
- const e = events[i]!;
76
- if (e.type === "text" || e.type === "result") {
77
- if (e.text) return e.text;
78
- }
79
- }
80
- return "";
81
- },
82
-
83
- extractUsage(events) {
84
- for (let i = events.length - 1; i >= 0; i--) {
85
- const e = events[i]!;
86
- if (e.type === "result") {
87
- const raw = e.raw as Record<string, unknown>;
88
- const usage = raw.usage as Record<string, unknown> | undefined;
89
- if (usage && typeof usage.input_tokens === "number" && typeof usage.output_tokens === "number") {
90
- return {
91
- inputTokens: usage.input_tokens as number,
92
- outputTokens: usage.output_tokens as number,
93
- totalTokens: (usage.input_tokens as number) + (usage.output_tokens as number),
94
- };
95
- }
96
- }
97
- }
98
- return undefined;
99
- },
100
-
101
- extractCostUsd(events) {
102
- for (let i = events.length - 1; i >= 0; i--) {
103
- const e = events[i]!;
104
- if (e.type === "result") {
105
- const raw = e.raw as Record<string, unknown>;
106
- if (typeof raw.total_cost_usd === "number") {
107
- return raw.total_cost_usd as number;
108
- }
109
- }
110
- }
111
- return undefined;
112
- },
113
-
114
- extractSessionId(events) {
115
- for (let i = events.length - 1; i >= 0; i--) {
116
- const e = events[i]!;
117
- if (e.type === "result") {
118
- const raw = e.raw as Record<string, unknown>;
119
- if (typeof raw.session_id === "string") {
120
- return raw.session_id as string;
121
- }
122
- }
123
- }
124
- return undefined;
125
- },
126
- };
@@ -1,10 +0,0 @@
1
- import type { HarnessDescriptor, HarnessName } from "../types.ts";
2
- import { claudeDescriptor } from "./claude.ts";
3
- import { codexDescriptor } from "./codex.ts";
4
- import { opencodeDescriptor } from "./opencode.ts";
5
-
6
- export const DESCRIPTORS: Record<HarnessName, HarnessDescriptor> = {
7
- claude: claudeDescriptor,
8
- codex: codexDescriptor,
9
- opencode: opencodeDescriptor,
10
- };
@@ -1,147 +0,0 @@
1
- import type { HarnessDescriptor } from "../types.ts";
2
-
3
- const OPENCODE_PERMISSION = JSON.stringify([
4
- { permission: "*", pattern: "*", action: "allow" },
5
- ]);
6
-
7
- export const opencodeDescriptor: HarnessDescriptor = {
8
- name: "opencode",
9
- versionArgv: ["opencode", "--version"],
10
- binName: "opencode",
11
- binDirs: ["~/.opencode/bin", "~/.local/bin"],
12
- authStatusArgv: ["auth", "list"],
13
-
14
- // `opencode auth list` prints "N credentials" for configured providers.
15
- interpretAuthStatus({ exitCode, stdout }) {
16
- if (exitCode !== 0) return null;
17
- const text = stdout.replace(/\x1b\[[0-9;]*m/g, "");
18
- const match = text.match(/(\d+)\s+credentials?/i);
19
- if (match) return Number(match[1]) > 0;
20
- return /credential/i.test(text) ? true : null;
21
- },
22
-
23
- buildArgv(o) {
24
- return [
25
- "opencode",
26
- "run",
27
- o.prompt,
28
- "--format",
29
- "json",
30
- "--dangerously-skip-permissions",
31
- "--dir",
32
- o.cwd,
33
- ...(o.model ? ["--model", o.model] : []),
34
- ];
35
- },
36
-
37
- buildEnv(o) {
38
- const env: Record<string, string> = {
39
- OPENCODE_PERMISSION,
40
- };
41
- const tmpFiles: { path: string; content: string }[] = [];
42
-
43
- if (o.mcp) {
44
- const configDir = `/tmp/opencode-mcp-${Date.now()}`;
45
- const configPath = `${configDir}/opencode.json`;
46
- const config = {
47
- mcp: {
48
- popio: {
49
- type: "remote",
50
- url: o.mcp.url,
51
- headers: { Authorization: `Bearer ${o.mcp.token}` },
52
- enabled: true,
53
- },
54
- },
55
- };
56
- tmpFiles.push({ path: configPath, content: JSON.stringify(config) });
57
- env.OPENCODE_CONFIG_DIR = configDir;
58
- }
59
-
60
- return { env, tmpFiles: tmpFiles.length > 0 ? tmpFiles : undefined };
61
- },
62
-
63
- parseEvents(lines) {
64
- return lines.map((raw) => {
65
- const obj = raw as Record<string, unknown>;
66
- const type = typeof obj.type === "string" ? obj.type : undefined;
67
- switch (type) {
68
- case "text":
69
- return { type: "text" as const, raw, text: String(obj.text ?? "") };
70
- case "tool_call":
71
- return {
72
- type: "tool_call" as const,
73
- raw,
74
- tool: typeof obj.tool === "string" ? obj.tool : undefined,
75
- };
76
- case "tool_result":
77
- return { type: "tool_result" as const, raw };
78
- case "system":
79
- return { type: "system" as const, raw };
80
- case "result":
81
- return { type: "result" as const, raw };
82
- default:
83
- return { type: "raw" as const, raw };
84
- }
85
- });
86
- },
87
-
88
- extractFinalMessage(events) {
89
- for (let i = events.length - 1; i >= 0; i--) {
90
- const e = events[i]!;
91
- if (e.type === "text" || e.type === "result") {
92
- if (e.text) return e.text;
93
- }
94
- }
95
- return "";
96
- },
97
-
98
- extractUsage(events) {
99
- for (let i = events.length - 1; i >= 0; i--) {
100
- const e = events[i]!;
101
- if (e.type === "result") {
102
- const raw = e.raw as Record<string, unknown>;
103
- const info = raw.info as Record<string, unknown> | undefined;
104
- const tokens = info?.tokens as Record<string, unknown> | undefined;
105
- if (tokens && typeof tokens.input === "number" && typeof tokens.output === "number") {
106
- return {
107
- inputTokens: tokens.input as number,
108
- outputTokens: tokens.output as number,
109
- totalTokens: typeof tokens.total === "number" ? (tokens.total as number) : (tokens.input as number) + (tokens.output as number),
110
- };
111
- }
112
- }
113
- }
114
- return undefined;
115
- },
116
-
117
- extractCostUsd(events) {
118
- for (let i = events.length - 1; i >= 0; i--) {
119
- const e = events[i]!;
120
- if (e.type === "result") {
121
- const raw = e.raw as Record<string, unknown>;
122
- const info = raw.info as Record<string, unknown> | undefined;
123
- if (info && typeof info.costUsd === "number") {
124
- return info.costUsd as number;
125
- }
126
- }
127
- }
128
- return undefined;
129
- },
130
-
131
- extractSessionId(events) {
132
- for (let i = events.length - 1; i >= 0; i--) {
133
- const e = events[i]!;
134
- if (e.type === "result") {
135
- const raw = e.raw as Record<string, unknown>;
136
- if (typeof raw.session_id === "string") {
137
- return raw.session_id as string;
138
- }
139
- const info = raw.info as Record<string, unknown> | undefined;
140
- if (info && typeof info.sessionId === "string") {
141
- return info.sessionId as string;
142
- }
143
- }
144
- }
145
- return undefined;
146
- },
147
- };
@@ -1,128 +0,0 @@
1
- import type { HarnessDescriptor } from "./types.ts";
2
- import { runJsonlSubprocess } from "./subprocess.ts";
3
- import { binEnvVar, harnessBinName, healedPath, resolveHarnessBinary } from "./resolve.ts";
4
- import type {
5
- HarnessEvent,
6
- HarnessName,
7
- HarnessRunOptions,
8
- HarnessRunResult,
9
- } from "./types.ts";
10
-
11
- const DEFAULT_TIMEOUT = 300_000;
12
-
13
- export type DescriptorMap = Record<HarnessName, HarnessDescriptor>;
14
-
15
- /** True when a spawn failure looks like the executable could not be found. */
16
- function isMissingBinaryError(err: unknown): boolean {
17
- if (err && typeof err === "object") {
18
- if ((err as { code?: unknown }).code === "ENOENT") return true;
19
- const message = (err as { message?: unknown }).message;
20
- if (typeof message === "string" && message.includes("ENOENT")) return true;
21
- }
22
- return false;
23
- }
24
-
25
- export async function runHarnessTask(
26
- options: HarnessRunOptions,
27
- deps?: {
28
- runJsonlSubprocess?: typeof runJsonlSubprocess;
29
- descriptors?: DescriptorMap;
30
- resolveBinary?: typeof resolveHarnessBinary;
31
- },
32
- ): Promise<HarnessRunResult> {
33
- const descriptors = deps?.descriptors ?? (await import("./descriptors/index.ts")).DESCRIPTORS;
34
- const descriptor = descriptors[options.harness];
35
- const resolveBinary = deps?.resolveBinary ?? resolveHarnessBinary;
36
-
37
- // Resolve the CLI to an absolute path so it runs regardless of how POP was launched.
38
- // When resolution fails, keep the bare command and let the healed PATH below find it.
39
- const binPath = resolveBinary(descriptor);
40
- const builtArgv = descriptor.buildArgv(options);
41
- const argv = binPath ? [binPath, ...builtArgv.slice(1)] : builtArgv;
42
-
43
- const built = descriptor.buildEnv(options);
44
- const tmpFiles = built.tmpFiles;
45
- const env = { ...built.env, PATH: healedPath(descriptor.binDirs ?? []) };
46
-
47
- const writtenPaths: string[] = [];
48
- try {
49
- if (tmpFiles) {
50
- for (const tmp of tmpFiles) {
51
- await Bun.write(tmp.path, tmp.content);
52
- writtenPaths.push(tmp.path);
53
- }
54
- }
55
-
56
- const subprocess = deps?.runJsonlSubprocess ?? runJsonlSubprocess;
57
- let result;
58
- try {
59
- result = await subprocess({
60
- argv,
61
- env,
62
- cwd: options.cwd,
63
- timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT,
64
- signal: options.signal,
65
- });
66
- } catch (err) {
67
- if (binPath === null && isMissingBinaryError(err)) {
68
- const searched = (descriptor.binDirs ?? []).join(", ") || "no extra install dirs";
69
- throw new Error(
70
- `Harness "${options.harness}" CLI "${harnessBinName(descriptor)}" not found. ` +
71
- `Searched PATH and ${searched}. ` +
72
- `Install it or set ${binEnvVar(options.harness)} to its absolute path.`,
73
- );
74
- }
75
- throw err;
76
- }
77
-
78
- if (result.timedOut) {
79
- throw new Error(
80
- `Harness "${options.harness}" timed out after ${options.timeoutMs ?? DEFAULT_TIMEOUT}ms${result.stderr ? `: ${result.stderr}` : ""}`,
81
- );
82
- }
83
-
84
- if (result.exitCode !== 0) {
85
- throw new Error(
86
- `Harness "${options.harness}" exited with code ${result.exitCode}${result.stderr ? `: ${result.stderr}` : ""}`,
87
- );
88
- }
89
-
90
- const events = descriptor.parseEvents(result.events);
91
-
92
- if (options.onEvent) {
93
- for (const event of events) {
94
- options.onEvent(event);
95
- }
96
- }
97
-
98
- const finalMessage = descriptor.extractFinalMessage(events);
99
- const usage = descriptor.extractUsage(events);
100
- const costUsd = descriptor.extractCostUsd(events);
101
- const sessionId = descriptor.extractSessionId(events);
102
-
103
- return {
104
- finalMessage,
105
- sessionId,
106
- usage,
107
- costUsd,
108
- events,
109
- exitCode: result.exitCode,
110
- };
111
- } finally {
112
- for (const path of writtenPaths) {
113
- await Bun.file(path).delete().catch(() => {});
114
- }
115
- }
116
- }
117
-
118
- export async function isHarnessAvailable(harness: HarnessName, descriptors?: DescriptorMap): Promise<boolean> {
119
- const desc = descriptors ?? (await import("./descriptors/index.ts")).DESCRIPTORS;
120
- const descriptor = desc[harness];
121
- const result = Bun.spawnSync([...descriptor.versionArgv], {
122
- timeout: 5000,
123
- stdout: "ignore",
124
- stderr: "ignore",
125
- env: { ...process.env, PATH: healedPath(descriptor.binDirs ?? []) },
126
- });
127
- return result.exitCode === 0;
128
- }
@@ -1,176 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
- import type {
5
- AuthStatusResult,
6
- HarnessDescriptor,
7
- HarnessName,
8
- HarnessProbe,
9
- } from "./types.ts";
10
-
11
- /** How long to wait for a CLI's auth-status command before giving up (advisory only). */
12
- const AUTH_PROBE_TIMEOUT_MS = 10_000;
13
-
14
- type EnvLike = Record<string, string | undefined>;
15
-
16
- /** Expand a leading "~/" to the user's home directory. */
17
- function expandTilde(dir: string): string {
18
- if (dir === "~") return homedir();
19
- if (dir.startsWith("~/")) return join(homedir(), dir.slice(2));
20
- return dir;
21
- }
22
-
23
- /**
24
- * Standard user/local bin dirs that interactive shells add (via .zshrc etc.) but
25
- * GUI/launchd/cron launches usually miss. Prepended to PATH so the harness — and
26
- * the agent's own child tools (git, rg, node) — resolve regardless of how POP started.
27
- */
28
- export function standardBinDirs(): string[] {
29
- const home = homedir();
30
- return [
31
- join(home, ".opencode/bin"),
32
- join(home, ".local/bin"),
33
- join(home, ".bun/bin"),
34
- join(home, ".cargo/bin"),
35
- join(home, ".npm-global/bin"),
36
- join(home, ".deno/bin"),
37
- "/opt/homebrew/bin",
38
- "/usr/local/bin",
39
- ];
40
- }
41
-
42
- /**
43
- * Build a PATH with known bin dirs prepended — descriptor-specific dirs first, then
44
- * the standard set — keeping only dirs that exist and are not already present.
45
- */
46
- export function healedPath(extraDirs: readonly string[] = [], basePath: string = process.env.PATH ?? ""): string {
47
- const current = basePath.split(":").filter(Boolean);
48
- const seen = new Set(current);
49
- const prepend: string[] = [];
50
- for (const raw of [...extraDirs, ...standardBinDirs()]) {
51
- const dir = expandTilde(raw);
52
- if (dir && !seen.has(dir) && existsSync(dir)) {
53
- prepend.push(dir);
54
- seen.add(dir);
55
- }
56
- }
57
- return [...prepend, ...current].join(":");
58
- }
59
-
60
- /** Env var that pins a harness CLI to an absolute path, e.g. "opencode" -> POP_OPENCODE_BIN. */
61
- export function binEnvVar(name: HarnessName): string {
62
- return `POP_${name.toUpperCase()}_BIN`;
63
- }
64
-
65
- /** The command used to launch a harness (descriptor override, else the harness name). */
66
- export function harnessBinName(descriptor: HarnessDescriptor): string {
67
- return descriptor.binName ?? descriptor.name;
68
- }
69
-
70
- /**
71
- * Resolve a harness CLI to an absolute path: explicit override env var → PATH search
72
- * (with known install dirs healed in) → null when it cannot be found.
73
- */
74
- export function resolveHarnessBinary(
75
- descriptor: HarnessDescriptor,
76
- env: EnvLike = process.env,
77
- ): string | null {
78
- const override = env[binEnvVar(descriptor.name)];
79
- if (override && existsSync(override)) return override;
80
-
81
- const path = healedPath(descriptor.binDirs ?? [], env.PATH ?? "");
82
- return Bun.which(harnessBinName(descriptor), { PATH: path });
83
- }
84
-
85
- /**
86
- * Run the harness CLI's own auth-status command and interpret the result.
87
- * Best-effort and advisory: returns null on any timeout/spawn error, missing
88
- * config, or undeterminable output. Never throws.
89
- */
90
- export async function probeHarnessAuth(
91
- descriptor: HarnessDescriptor,
92
- binPath: string,
93
- env: EnvLike = process.env,
94
- ): Promise<boolean | null> {
95
- if (!descriptor.authStatusArgv || !descriptor.interpretAuthStatus) return null;
96
-
97
- try {
98
- const proc = Bun.spawn([binPath, ...descriptor.authStatusArgv], {
99
- stdin: "ignore",
100
- stdout: "pipe",
101
- stderr: "pipe",
102
- env: { ...env, PATH: healedPath(descriptor.binDirs ?? [], env.PATH ?? "") } as Record<string, string>,
103
- });
104
- const timer = setTimeout(() => proc.kill(), AUTH_PROBE_TIMEOUT_MS);
105
- const stdout = await new Response(proc.stdout).text();
106
- const stderr = await new Response(proc.stderr).text();
107
- await proc.exited;
108
- clearTimeout(timer);
109
-
110
- const result: AuthStatusResult = { exitCode: proc.exitCode ?? -1, stdout, stderr };
111
- return descriptor.interpretAuthStatus(result);
112
- } catch {
113
- return null;
114
- }
115
- }
116
-
117
- /** Probe every harness once: resolve its CLI and (if found) check auth. */
118
- export async function discoverHarnesses(
119
- descriptors?: Record<HarnessName, HarnessDescriptor>,
120
- env: EnvLike = process.env,
121
- ): Promise<Record<HarnessName, HarnessProbe>> {
122
- const map = descriptors ?? (await import("./descriptors/index.ts")).DESCRIPTORS;
123
- const out = {} as Record<HarnessName, HarnessProbe>;
124
-
125
- await Promise.all(
126
- (Object.keys(map) as HarnessName[]).map(async (name) => {
127
- const descriptor = map[name];
128
- const binPath = resolveHarnessBinary(descriptor, env);
129
- const authenticated = binPath ? await probeHarnessAuth(descriptor, binPath, env) : null;
130
- out[name] = { name, binPath, available: binPath !== null, authenticated };
131
- }),
132
- );
133
-
134
- return out;
135
- }
136
-
137
- export interface PreflightMessage {
138
- level: "info" | "warn";
139
- message: string;
140
- }
141
-
142
- /**
143
- * Apply discovery results to an env object (heal PATH, cache resolved absolute paths
144
- * as POP_<HARNESS>_BIN) and return human-readable status/warning messages. Pure aside
145
- * from the passed `env` object — never blocks, only warns.
146
- */
147
- export function applyHarnessDiscovery(
148
- probes: Record<HarnessName, HarnessProbe>,
149
- env: EnvLike,
150
- ): PreflightMessage[] {
151
- env.PATH = healedPath([], env.PATH ?? "");
152
-
153
- const messages: PreflightMessage[] = [];
154
- for (const probe of Object.values(probes)) {
155
- if (probe.binPath) env[binEnvVar(probe.name)] = probe.binPath;
156
-
157
- if (!probe.available) {
158
- messages.push({
159
- level: "warn",
160
- message: `harness "${probe.name}": CLI not found on PATH or known install dirs; agent steps using it will fail until it is installed`,
161
- });
162
- } else if (probe.authenticated === false) {
163
- messages.push({
164
- level: "warn",
165
- message: `harness "${probe.name}": found at ${probe.binPath} but not authenticated; runs may fail until you log in`,
166
- });
167
- } else {
168
- const auth = probe.authenticated === true ? "authenticated" : "auth status unknown";
169
- messages.push({
170
- level: "info",
171
- message: `harness "${probe.name}": ${probe.binPath} (${auth})`,
172
- });
173
- }
174
- }
175
- return messages;
176
- }
@@ -1,100 +0,0 @@
1
- export type HarnessName = "claude" | "codex" | "opencode";
2
-
3
- export interface McpServerConnection {
4
- url: string;
5
- token: string;
6
- }
7
-
8
- export interface HarnessUsage {
9
- inputTokens: number;
10
- outputTokens: number;
11
- totalTokens: number;
12
- }
13
-
14
- export interface HarnessEvent {
15
- type: "text" | "tool_call" | "tool_result" | "system" | "result" | "raw";
16
- raw: unknown;
17
- text?: string;
18
- tool?: string;
19
- }
20
-
21
- export interface HarnessRunOptions {
22
- harness: HarnessName;
23
- prompt: string;
24
- cwd: string;
25
- model?: string;
26
- mcp?: McpServerConnection;
27
- timeoutMs?: number;
28
- signal?: AbortSignal;
29
- onEvent?: (event: HarnessEvent) => void;
30
- }
31
-
32
- export interface HarnessRunResult {
33
- finalMessage: string;
34
- sessionId?: string;
35
- usage?: HarnessUsage;
36
- costUsd?: number;
37
- events: HarnessEvent[];
38
- exitCode: number;
39
- }
40
-
41
- /** Raw result of running a harness CLI's auth-status command, fed to {@link HarnessDescriptor.interpretAuthStatus}. */
42
- export interface AuthStatusResult {
43
- exitCode: number;
44
- stdout: string;
45
- stderr: string;
46
- }
47
-
48
- export interface HarnessDescriptor {
49
- name: HarnessName;
50
- versionArgv: readonly string[];
51
- /** The CLI command used to launch this harness. Defaults to {@link name} when omitted. */
52
- binName?: string;
53
- /** Extra install dirs (beyond PATH) to search for the CLI — e.g. ["~/.opencode/bin"]. */
54
- binDirs?: readonly string[];
55
- /** Args (after the binary) for the CLI's own auth-status check, e.g. ["auth", "status"]. */
56
- authStatusArgv?: readonly string[];
57
- /** Pure interpreter mapping the auth-status command result to true/false/null (null = can't tell). */
58
- interpretAuthStatus?(result: AuthStatusResult): boolean | null;
59
- buildArgv(o: HarnessRunOptions): string[];
60
- buildEnv(o: HarnessRunOptions): {
61
- env: Record<string, string>;
62
- tmpFiles?: { path: string; content: string }[];
63
- };
64
- parseEvents(lines: unknown[]): HarnessEvent[];
65
- extractFinalMessage(events: HarnessEvent[]): string;
66
- extractUsage(events: HarnessEvent[]): HarnessUsage | undefined;
67
- extractCostUsd(events: HarnessEvent[]): number | undefined;
68
- extractSessionId(events: HarnessEvent[]): string | undefined;
69
- }
70
-
71
- /** Startup discovery result for one harness CLI. */
72
- export interface HarnessProbe {
73
- name: HarnessName;
74
- /** Absolute path to the resolved CLI, or null when it could not be found. */
75
- binPath: string | null;
76
- available: boolean;
77
- /** true/false from the CLI's own auth check; null when unavailable or undeterminable. Advisory only. */
78
- authenticated: boolean | null;
79
- }
80
-
81
- export interface AgentEntryConfig {
82
- harness: HarnessName;
83
- model?: string;
84
- prompt?: string;
85
- promptFrom?: string;
86
- cwd?: string;
87
- io?: boolean;
88
- timeoutMs?: number;
89
- captureDiff?: boolean;
90
- }
91
-
92
- export interface AgentStepResult {
93
- ok: boolean;
94
- finalMessage: string;
95
- artifactsWritten: string[];
96
- usage?: HarnessUsage;
97
- costUsd?: number;
98
- sessionId?: string;
99
- error?: string;
100
- }