Skip to content

Commit 263b842

Browse files
committed
feat(v1): add harness provider registry
1 parent 4d5bf56 commit 263b842

10 files changed

Lines changed: 283 additions & 0 deletions

File tree

docs/plans/2026-05-10-v1-decision-log.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,22 @@ manager, harness SDK, provider adapters, and simple prompt assembly.
3636

3737
Consequences: Implementation should delete stale architecture where it conflicts
3838
with the new model.
39+
40+
## 2026-05-09 19:47 PDT
41+
42+
Decision: Add harness provider registry with placeholder adapters before porting
43+
Claude and Codex.
44+
45+
Context: The new operation/process layers need a stable provider-neutral
46+
registry and capability model before real provider ports are implemented.
47+
48+
Alternatives:
49+
- Port Claude immediately while defining the registry.
50+
- Reuse `src/agent/providers/*` directly.
51+
52+
Why: Placeholder adapters let the project compile and test the new boundary
53+
without mixing old `RunAgentOptions` semantics into `AgentRunSpec`.
54+
55+
Consequences: Any command using the new harness before provider ports land will
56+
fail clearly with "not implemented yet." Real Claude/Codex behavior lands in the
57+
provider adapter phase.

docs/plans/2026-05-10-v1-implementation-log.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,23 @@ This log tracks implementation checkpoints for the V1 harness/process refactor.
5656
- `npm run lint`
5757
- Result: 1 test file passed, 5 tests passed; TypeScript lint passed.
5858
- Next: Phase 2.2 harness provider registry.
59+
60+
## 2026-05-09 19:47 PDT
61+
62+
- Built: Phase 2.2 harness provider registry with explicit provider metadata and placeholder adapters.
63+
- Files changed:
64+
- `src/harness/providers/index.ts`
65+
- `src/harness/providers/metadata.ts`
66+
- `src/harness/providers/not-implemented.ts`
67+
- `src/harness/providers/claude.ts`
68+
- `src/harness/providers/codex.ts`
69+
- `src/harness/providers/cursor.ts`
70+
- `src/harness/index.ts`
71+
- `test/harness-provider-registry.test.ts`
72+
- `docs/plans/2026-05-10-v1-implementation-log.md`
73+
- `docs/plans/2026-05-10-v1-decision-log.md`
74+
- Tests run:
75+
- `npm test -- test/harness-types.test.ts test/harness-provider-registry.test.ts`
76+
- `npm run lint`
77+
- Result: 2 test files passed, 9 tests passed; TypeScript lint passed.
78+
- Next: Phase 3.1 process run records and ids.

src/harness/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,8 @@ export type {
1717
} from "./events.js";
1818
export type { ShellPolicy, ToolId, ToolRequest } from "./tools.js";
1919
export { isToolId, uniqueToolRequests } from "./tools.js";
20+
export {
21+
getHarnessProvider,
22+
HARNESS_PROVIDER_METADATA,
23+
listHarnessProviders,
24+
} from "./providers/index.js";

src/harness/providers/claude.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { HarnessProvider } from "../types.js";
2+
import { HARNESS_PROVIDER_METADATA } from "./metadata.js";
3+
import { createNotImplementedProvider } from "./not-implemented.js";
4+
5+
export const claudeHarnessProvider: HarnessProvider = createNotImplementedProvider(
6+
HARNESS_PROVIDER_METADATA.claude,
7+
);

src/harness/providers/codex.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { HarnessProvider } from "../types.js";
2+
import { HARNESS_PROVIDER_METADATA } from "./metadata.js";
3+
import { createNotImplementedProvider } from "./not-implemented.js";
4+
5+
export const codexHarnessProvider: HarnessProvider = createNotImplementedProvider(
6+
HARNESS_PROVIDER_METADATA.codex,
7+
);

src/harness/providers/cursor.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { HarnessProvider } from "../types.js";
2+
import { HARNESS_PROVIDER_METADATA } from "./metadata.js";
3+
import { createNotImplementedProvider } from "./not-implemented.js";
4+
5+
export const cursorHarnessProvider: HarnessProvider = createNotImplementedProvider(
6+
HARNESS_PROVIDER_METADATA.cursor,
7+
);

src/harness/providers/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { HarnessProvider, HarnessProviderId } from "../types.js";
2+
import { claudeHarnessProvider } from "./claude.js";
3+
import { codexHarnessProvider } from "./codex.js";
4+
import { cursorHarnessProvider } from "./cursor.js";
5+
import { HARNESS_PROVIDER_METADATA } from "./metadata.js";
6+
7+
const HARNESS_PROVIDERS = {
8+
claude: claudeHarnessProvider,
9+
codex: codexHarnessProvider,
10+
cursor: cursorHarnessProvider,
11+
} satisfies Record<HarnessProviderId, HarnessProvider>;
12+
13+
export function getHarnessProvider(id: HarnessProviderId): HarnessProvider {
14+
return HARNESS_PROVIDERS[id];
15+
}
16+
17+
export function listHarnessProviders(): HarnessProvider[] {
18+
return Object.values(HARNESS_PROVIDERS);
19+
}
20+
21+
export { HARNESS_PROVIDER_METADATA };

src/harness/providers/metadata.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import type {
2+
HarnessCapabilities,
3+
HarnessProviderId,
4+
ProviderMetadata,
5+
} from "../types.js";
6+
7+
const BASE_CAPABILITIES: Omit<
8+
HarnessCapabilities,
9+
| "reasoningEffort"
10+
| "sessionPersistence"
11+
| "threadResume"
12+
| "interrupt"
13+
| "mcp"
14+
| "skills"
15+
| "usage"
16+
| "cost"
17+
| "contextUsage"
18+
| "structuredOutput"
19+
| "subagents"
20+
| "policy"
21+
> = {
22+
nonInteractive: true,
23+
streaming: true,
24+
modelOverride: true,
25+
modelOptions: false,
26+
fileRead: true,
27+
fileWrite: true,
28+
shell: true,
29+
};
30+
31+
export const HARNESS_PROVIDER_METADATA: Record<HarnessProviderId, ProviderMetadata> = {
32+
claude: {
33+
id: "claude",
34+
displayName: "Claude",
35+
defaultModel: "claude-sonnet-4-6",
36+
capabilities: {
37+
...BASE_CAPABILITIES,
38+
reasoningEffort: false,
39+
sessionPersistence: true,
40+
threadResume: true,
41+
interrupt: true,
42+
mcp: true,
43+
skills: true,
44+
usage: true,
45+
cost: true,
46+
contextUsage: false,
47+
structuredOutput: true,
48+
subagents: {
49+
supported: true,
50+
programmaticPerRun: true,
51+
enforcedToolScopes: true,
52+
},
53+
policy: {
54+
sandbox: true,
55+
strictToolAllowlist: true,
56+
commandApproval: true,
57+
toolHook: true,
58+
},
59+
},
60+
},
61+
codex: {
62+
id: "codex",
63+
displayName: "Codex",
64+
defaultModel: null,
65+
capabilities: {
66+
...BASE_CAPABILITIES,
67+
modelOptions: true,
68+
reasoningEffort: true,
69+
sessionPersistence: true,
70+
threadResume: true,
71+
interrupt: true,
72+
mcp: true,
73+
skills: true,
74+
usage: true,
75+
cost: false,
76+
contextUsage: true,
77+
structuredOutput: true,
78+
subagents: {
79+
supported: true,
80+
programmaticPerRun: false,
81+
enforcedToolScopes: false,
82+
},
83+
policy: {
84+
sandbox: true,
85+
strictToolAllowlist: false,
86+
commandApproval: true,
87+
toolHook: false,
88+
},
89+
},
90+
},
91+
cursor: {
92+
id: "cursor",
93+
displayName: "Cursor",
94+
defaultModel: null,
95+
capabilities: {
96+
...BASE_CAPABILITIES,
97+
reasoningEffort: false,
98+
sessionPersistence: true,
99+
threadResume: true,
100+
interrupt: true,
101+
mcp: true,
102+
skills: false,
103+
usage: true,
104+
cost: false,
105+
contextUsage: false,
106+
structuredOutput: false,
107+
subagents: {
108+
supported: false,
109+
programmaticPerRun: false,
110+
enforcedToolScopes: false,
111+
},
112+
policy: {
113+
sandbox: false,
114+
strictToolAllowlist: false,
115+
commandApproval: true,
116+
toolHook: false,
117+
},
118+
},
119+
},
120+
};
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type {
2+
AgentRunSpec,
3+
HarnessProvider,
4+
ProviderMetadata,
5+
ProviderStatus,
6+
} from "../types.js";
7+
8+
export function createNotImplementedProvider(
9+
metadata: ProviderMetadata,
10+
): HarnessProvider {
11+
return {
12+
metadata,
13+
checkStatus: async (): Promise<ProviderStatus> => ({
14+
id: metadata.id,
15+
installed: false,
16+
authenticated: false,
17+
detail: `${metadata.displayName} harness adapter is not implemented yet`,
18+
}),
19+
run: async (spec: AgentRunSpec) => ({
20+
success: false,
21+
result: "",
22+
error:
23+
`${metadata.displayName} harness adapter is not implemented yet ` +
24+
`for ${spec.metadata?.operation ?? "unknown"} runs`,
25+
}),
26+
};
27+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import {
4+
getHarnessProvider,
5+
HARNESS_PROVIDER_METADATA,
6+
listHarnessProviders,
7+
} from "../src/harness/index.js";
8+
9+
describe("harness provider registry", () => {
10+
it("lists the supported provider ids", () => {
11+
expect(listHarnessProviders().map((provider) => provider.metadata.id)).toEqual([
12+
"claude",
13+
"codex",
14+
"cursor",
15+
]);
16+
});
17+
18+
it("returns provider metadata by id", () => {
19+
expect(getHarnessProvider("claude").metadata).toBe(
20+
HARNESS_PROVIDER_METADATA.claude,
21+
);
22+
expect(getHarnessProvider("codex").metadata.displayName).toBe("Codex");
23+
expect(getHarnessProvider("cursor").metadata.displayName).toBe("Cursor");
24+
});
25+
26+
it("keeps capability differences explicit", () => {
27+
expect(
28+
HARNESS_PROVIDER_METADATA.claude.capabilities.subagents.programmaticPerRun,
29+
).toBe(true);
30+
expect(
31+
HARNESS_PROVIDER_METADATA.codex.capabilities.subagents.programmaticPerRun,
32+
).toBe(false);
33+
expect(HARNESS_PROVIDER_METADATA.codex.capabilities.contextUsage).toBe(true);
34+
expect(HARNESS_PROVIDER_METADATA.claude.capabilities.contextUsage).toBe(false);
35+
});
36+
37+
it("placeholder adapters fail clearly until provider ports land", async () => {
38+
await expect(
39+
getHarnessProvider("claude").run({
40+
provider: { id: "claude" },
41+
cwd: "/repo",
42+
prompt: "hello",
43+
metadata: { operation: "build" },
44+
}),
45+
).resolves.toMatchObject({
46+
success: false,
47+
error: expect.stringContaining("not implemented yet"),
48+
});
49+
});
50+
});

0 commit comments

Comments
 (0)