harnery 0.9.0 → 0.10.0
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 +1 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +6 -0
- package/dist/commands/agents.d.ts +24 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +244 -13
- package/dist/commands/backup.d.ts +5 -4
- package/dist/commands/backup.d.ts.map +1 -1
- package/dist/commands/backup.js +15 -14
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +27 -1
- package/dist/commands/claude-desktop.d.ts +19 -0
- package/dist/commands/claude-desktop.d.ts.map +1 -0
- package/dist/commands/claude-desktop.js +168 -0
- package/dist/commands/context.d.ts.map +1 -1
- package/dist/commands/context.js +149 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +2 -3
- package/dist/commands/eml.d.ts +32 -0
- package/dist/commands/eml.d.ts.map +1 -1
- package/dist/commands/eml.js +16 -3
- package/dist/commands/harness.d.ts +11 -0
- package/dist/commands/harness.d.ts.map +1 -0
- package/dist/commands/harness.js +118 -0
- package/dist/commands/presence.d.ts +9 -4
- package/dist/commands/presence.d.ts.map +1 -1
- package/dist/commands/presence.js +88 -5
- package/dist/commands/relay.d.ts +9 -0
- package/dist/commands/relay.d.ts.map +1 -0
- package/dist/commands/relay.js +143 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +5 -0
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +8 -15
- package/dist/core/agents/canonical-emit.d.ts +15 -0
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +41 -3
- package/dist/core/agents/cli.js +4 -2
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +44 -3
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +26 -1
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +4 -37
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +16 -43
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-projector.js +11 -2
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +3 -3
- package/dist/core/agents/state/names.d.ts +38 -3
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +46 -7
- package/dist/core/agents/state/pidmap.d.ts +5 -1
- package/dist/core/agents/state/pidmap.d.ts.map +1 -1
- package/dist/core/agents/state/pidmap.js +58 -2
- package/dist/core/agents/state/stale-sweep.d.ts +3 -2
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +6 -6
- package/dist/core/config.d.ts +68 -11
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +191 -25
- package/dist/core/context/index.d.ts +144 -0
- package/dist/core/context/index.d.ts.map +1 -0
- package/dist/core/context/index.js +380 -0
- package/dist/core/harnesses/bench.d.ts +29 -0
- package/dist/core/harnesses/bench.d.ts.map +1 -0
- package/dist/core/harnesses/bench.js +151 -0
- package/dist/core/harnesses/index.d.ts +9 -0
- package/dist/core/harnesses/index.d.ts.map +1 -0
- package/dist/core/harnesses/index.js +4 -0
- package/dist/core/harnesses/profiles.d.ts +62 -0
- package/dist/core/harnesses/profiles.d.ts.map +1 -0
- package/dist/core/harnesses/profiles.js +115 -0
- package/dist/core/harnesses/registry.d.ts +15 -0
- package/dist/core/harnesses/registry.d.ts.map +1 -0
- package/dist/core/harnesses/registry.js +113 -0
- package/dist/core/harnesses/types.d.ts +64 -0
- package/dist/core/harnesses/types.d.ts.map +1 -0
- package/dist/core/harnesses/types.js +20 -0
- package/dist/core/hooks/cli.js +280 -63
- package/dist/core/hooks/events/schema.d.ts +38 -1
- package/dist/core/hooks/events/schema.d.ts.map +1 -1
- package/dist/core/hooks/harness/events.d.ts.map +1 -1
- package/dist/core/hooks/harness/events.js +3 -0
- package/dist/core/hooks/harness/parse.d.ts +1 -1
- package/dist/core/hooks/harness/parse.d.ts.map +1 -1
- package/dist/core/hooks/harness/parse.js +4 -0
- package/dist/core/presence/blob.d.ts +40 -0
- package/dist/core/presence/blob.d.ts.map +1 -0
- package/dist/core/presence/blob.js +91 -0
- package/dist/core/presence/git.d.ts +68 -0
- package/dist/core/presence/git.d.ts.map +1 -0
- package/dist/core/presence/git.js +173 -0
- package/dist/core/presence/index.d.ts +74 -0
- package/dist/core/presence/index.d.ts.map +1 -0
- package/dist/core/presence/index.js +224 -0
- package/dist/core/presence/relay-client.d.ts +39 -0
- package/dist/core/presence/relay-client.d.ts.map +1 -0
- package/dist/core/presence/relay-client.js +295 -0
- package/dist/core/presence/relay-protocol.d.ts +94 -0
- package/dist/core/presence/relay-protocol.d.ts.map +1 -0
- package/dist/core/presence/relay-protocol.js +169 -0
- package/dist/core/workflow/billing.d.ts +1 -1
- package/dist/core/workflow/billing.d.ts.map +1 -1
- package/dist/core/workflow/billing.js +7 -1
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +11 -2
- package/dist/core/workflow/harnesses.d.ts +3 -3
- package/dist/core/workflow/harnesses.d.ts.map +1 -1
- package/dist/core/workflow/harnesses.js +8 -17
- package/dist/core/workflow/spawn-claude.d.ts +4 -1
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +39 -18
- package/dist/core/workflow/spawn-codex.d.ts +4 -1
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +44 -22
- package/dist/core/workflow/spawn-cursor.d.ts +4 -1
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +35 -16
- package/dist/core/workflow/types.d.ts +9 -2
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/lib/browser/client.d.ts +16 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +22 -0
- package/dist/lib/browser/index.d.ts +1 -0
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +1 -0
- package/dist/lib/browser/launch-args.d.ts +21 -0
- package/dist/lib/browser/launch-args.d.ts.map +1 -0
- package/dist/lib/browser/launch-args.js +32 -0
- package/dist/lib/claude-desktop.d.ts +105 -0
- package/dist/lib/claude-desktop.d.ts.map +1 -0
- package/dist/lib/claude-desktop.js +217 -0
- package/dist/lib/docs-lint.d.ts.map +1 -1
- package/dist/lib/docs-lint.js +5 -2
- package/dist/lib/identities/assume.d.ts +51 -0
- package/dist/lib/identities/assume.d.ts.map +1 -0
- package/dist/lib/identities/assume.js +274 -0
- package/dist/lib/identities/index.d.ts +7 -7
- package/dist/lib/identities/index.d.ts.map +1 -1
- package/dist/lib/identities/index.js +24 -24
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +15 -2
- package/package.json +12 -1
- package/schemas/config.schema.json +88 -19
- package/src/commander.ts +6 -0
- package/src/commands/agents.ts +280 -14
- package/src/commands/backup.ts +17 -16
- package/src/commands/browse.ts +32 -0
- package/src/commands/claude-desktop.ts +215 -0
- package/src/commands/context.ts +174 -1
- package/src/commands/doctor.ts +2 -3
- package/src/commands/eml.ts +22 -4
- package/src/commands/harness.ts +147 -0
- package/src/commands/presence.ts +111 -5
- package/src/commands/relay.ts +160 -0
- package/src/commands/sync.ts +4 -0
- package/src/commands/workflow.ts +9 -18
- package/src/core/agents/canonical-emit.ts +41 -3
- package/src/core/agents/cli.ts +4 -2
- package/src/core/agents/render/prompt-context.ts +48 -3
- package/src/core/agents/render/session-context.ts +27 -1
- package/src/core/agents/rules/commit-conflict.ts +4 -33
- package/src/core/agents/session-events.ts +15 -39
- package/src/core/agents/state/heartbeat-projector.ts +10 -1
- package/src/core/agents/state/heartbeat-writer.ts +3 -2
- package/src/core/agents/state/names.ts +68 -9
- package/src/core/agents/state/pidmap.ts +59 -2
- package/src/core/agents/state/stale-sweep.ts +6 -11
- package/src/core/config.ts +248 -24
- package/src/core/context/index.ts +575 -0
- package/src/core/harnesses/bench.ts +214 -0
- package/src/core/harnesses/index.ts +31 -0
- package/src/core/harnesses/profiles.ts +141 -0
- package/src/core/harnesses/registry.ts +140 -0
- package/src/core/harnesses/types.ts +90 -0
- package/src/core/hooks/cli.ts +331 -62
- package/src/core/hooks/events/schema.ts +68 -0
- package/src/core/hooks/harness/events.ts +3 -0
- package/src/core/hooks/harness/parse.ts +7 -1
- package/src/core/presence/blob.ts +125 -0
- package/src/core/presence/git.ts +191 -0
- package/src/core/presence/index.ts +274 -0
- package/src/core/presence/relay-client.ts +322 -0
- package/src/core/presence/relay-protocol.ts +246 -0
- package/src/core/workflow/billing.ts +8 -2
- package/src/core/workflow/engine.ts +11 -2
- package/src/core/workflow/harnesses.ts +14 -19
- package/src/core/workflow/spawn-claude.ts +39 -20
- package/src/core/workflow/spawn-codex.ts +47 -28
- package/src/core/workflow/spawn-cursor.ts +37 -18
- package/src/core/workflow/types.ts +9 -2
- package/src/lib/browser/client.ts +37 -0
- package/src/lib/browser/index.ts +1 -0
- package/src/lib/browser/launch-args.ts +33 -0
- package/src/lib/claude-desktop.ts +293 -0
- package/src/lib/docs-lint.ts +5 -2
- package/src/lib/identities/assume.ts +363 -0
- package/src/lib/identities/index.ts +28 -24
- package/src/lib/instructions/templates.ts +16 -2
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { HARNESS_SPECS } from "../hooks/harness/events.ts";
|
|
3
|
+
import type { SpawnRequest, SpawnResult } from "../workflow/types.ts";
|
|
4
|
+
import type { HarnessRegistry } from "./registry.ts";
|
|
5
|
+
import type {
|
|
6
|
+
CapabilitySupport,
|
|
7
|
+
HarnessAdapter,
|
|
8
|
+
HarnessCapabilityDimension,
|
|
9
|
+
HarnessId,
|
|
10
|
+
} from "./types.ts";
|
|
11
|
+
import { HARNESS_CAPABILITY_DIMENSIONS } from "./types.ts";
|
|
12
|
+
|
|
13
|
+
export type BenchVerdict =
|
|
14
|
+
| "supported"
|
|
15
|
+
| "partial"
|
|
16
|
+
| "unsupported"
|
|
17
|
+
| "not_applicable"
|
|
18
|
+
| "unknown"
|
|
19
|
+
| "skipped"
|
|
20
|
+
| "drift";
|
|
21
|
+
|
|
22
|
+
export type BenchDimension = "registration" | "binary" | HarnessCapabilityDimension;
|
|
23
|
+
|
|
24
|
+
export interface BenchResult {
|
|
25
|
+
harness: HarnessId;
|
|
26
|
+
dimension: BenchDimension;
|
|
27
|
+
declared: CapabilitySupport | "not_applicable";
|
|
28
|
+
observed: BenchVerdict;
|
|
29
|
+
verdict: BenchVerdict;
|
|
30
|
+
note?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface HarnessBenchReport {
|
|
34
|
+
generatedAt: string;
|
|
35
|
+
mode: "offline";
|
|
36
|
+
harnesses: HarnessId[];
|
|
37
|
+
results: BenchResult[];
|
|
38
|
+
summary: Record<BenchVerdict, number>;
|
|
39
|
+
drift: boolean;
|
|
40
|
+
skipped: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface HarnessBenchOptions {
|
|
44
|
+
harnesses?: readonly string[];
|
|
45
|
+
dimensions?: readonly HarnessCapabilityDimension[];
|
|
46
|
+
/** Test seam and alternate host probe. A null version means unavailable. */
|
|
47
|
+
versionProbe?: (binary: string) => string | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const EMPTY_SUMMARY: Record<BenchVerdict, number> = {
|
|
51
|
+
supported: 0,
|
|
52
|
+
partial: 0,
|
|
53
|
+
unsupported: 0,
|
|
54
|
+
not_applicable: 0,
|
|
55
|
+
unknown: 0,
|
|
56
|
+
skipped: 0,
|
|
57
|
+
drift: 0,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export function runHarnessBench(
|
|
61
|
+
registry: HarnessRegistry,
|
|
62
|
+
opts: HarnessBenchOptions = {},
|
|
63
|
+
): HarnessBenchReport {
|
|
64
|
+
const ids = opts.harnesses?.length ? [...opts.harnesses] : registry.ids();
|
|
65
|
+
const dimensions = opts.dimensions?.length
|
|
66
|
+
? [...new Set(opts.dimensions)]
|
|
67
|
+
: [...HARNESS_CAPABILITY_DIMENSIONS];
|
|
68
|
+
const versionProbe = opts.versionProbe ?? probeVersion;
|
|
69
|
+
const results: BenchResult[] = [];
|
|
70
|
+
|
|
71
|
+
for (const id of ids) {
|
|
72
|
+
const adapter = registry.require(id);
|
|
73
|
+
results.push({
|
|
74
|
+
harness: id,
|
|
75
|
+
dimension: "registration",
|
|
76
|
+
declared: "supported",
|
|
77
|
+
observed: "supported",
|
|
78
|
+
verdict: "supported",
|
|
79
|
+
note: `${adapter.profile.integrationMode}; ${adapter.profile.authModel}`,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const version = versionProbe(adapter.profile.binary);
|
|
83
|
+
results.push({
|
|
84
|
+
harness: id,
|
|
85
|
+
dimension: "binary",
|
|
86
|
+
declared: "supported",
|
|
87
|
+
observed: version ? "supported" : "skipped",
|
|
88
|
+
verdict: version ? "supported" : "skipped",
|
|
89
|
+
note: version ?? `${adapter.profile.binary} not found on PATH`,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const observations = observeAdapter(adapter);
|
|
93
|
+
for (const dimension of dimensions) {
|
|
94
|
+
const claim = adapter.profile.capabilities[dimension];
|
|
95
|
+
const observed = observations[dimension];
|
|
96
|
+
results.push({
|
|
97
|
+
harness: id,
|
|
98
|
+
dimension,
|
|
99
|
+
declared: claim.support,
|
|
100
|
+
observed,
|
|
101
|
+
verdict: reconcile(claim.support, observed),
|
|
102
|
+
note: claim.note,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const summary = { ...EMPTY_SUMMARY };
|
|
108
|
+
for (const result of results) summary[result.verdict]++;
|
|
109
|
+
return {
|
|
110
|
+
generatedAt: new Date().toISOString(),
|
|
111
|
+
mode: "offline",
|
|
112
|
+
harnesses: ids,
|
|
113
|
+
results,
|
|
114
|
+
summary,
|
|
115
|
+
drift: summary.drift > 0,
|
|
116
|
+
skipped: summary.skipped > 0,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function observeAdapter(adapter: HarnessAdapter): Record<HarnessCapabilityDimension, BenchVerdict> {
|
|
121
|
+
const profile = adapter.profile;
|
|
122
|
+
const effort = profile.effortValues[0];
|
|
123
|
+
const request: SpawnRequest = {
|
|
124
|
+
prompt: "HARNERY_BENCH_PROMPT",
|
|
125
|
+
model: "harnery-bench-model",
|
|
126
|
+
effort,
|
|
127
|
+
timeoutMs: 1_000,
|
|
128
|
+
maxTurns: 7,
|
|
129
|
+
cwd: "/harnery-bench",
|
|
130
|
+
};
|
|
131
|
+
let argv: string[] = [];
|
|
132
|
+
let planningFailed = false;
|
|
133
|
+
try {
|
|
134
|
+
argv = adapter.buildInvocation(request, "/harnery-bench/final.txt").argv;
|
|
135
|
+
} catch {
|
|
136
|
+
planningFailed = true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let normalized: SpawnResult | null = null;
|
|
140
|
+
try {
|
|
141
|
+
normalized = adapter.normalizeResult(adapter.fixture.raw);
|
|
142
|
+
} catch {
|
|
143
|
+
normalized = null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const fixture = adapter.fixture.expected;
|
|
147
|
+
const finalMatches = normalized?.ok === fixture.ok && normalized?.text === fixture.text;
|
|
148
|
+
const sessionObserved = normalized?.sessionId !== undefined ? "supported" : "unsupported";
|
|
149
|
+
const costObserved = normalized?.costUsd !== undefined ? "supported" : "unsupported";
|
|
150
|
+
const hookSpec =
|
|
151
|
+
adapter.profile.id in HARNESS_SPECS
|
|
152
|
+
? HARNESS_SPECS[adapter.profile.id as keyof typeof HARNESS_SPECS]
|
|
153
|
+
: undefined;
|
|
154
|
+
const hookSubcommands = new Set(hookSpec?.events.map((event) => event.subcommand) ?? []);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
invocation:
|
|
158
|
+
!planningFailed && argv[0] === profile.binary && argv.includes(request.prompt)
|
|
159
|
+
? "supported"
|
|
160
|
+
: "unsupported",
|
|
161
|
+
modelSelection:
|
|
162
|
+
!planningFailed && argv.includes(request.model ?? "") ? "supported" : "unsupported",
|
|
163
|
+
effortSelection:
|
|
164
|
+
effort === undefined
|
|
165
|
+
? "unsupported"
|
|
166
|
+
: !planningFailed && argv.some((arg) => arg.includes(effort))
|
|
167
|
+
? "supported"
|
|
168
|
+
: "unsupported",
|
|
169
|
+
maxTurns:
|
|
170
|
+
!planningFailed && argv.includes(String(request.maxTurns)) ? "supported" : "unsupported",
|
|
171
|
+
finalResult: finalMatches ? "supported" : "unsupported",
|
|
172
|
+
sessionId:
|
|
173
|
+
sessionObserved === "supported" && normalized?.sessionId !== fixture.sessionId
|
|
174
|
+
? "unsupported"
|
|
175
|
+
: sessionObserved,
|
|
176
|
+
cost:
|
|
177
|
+
costObserved === "supported" && normalized?.costUsd !== fixture.costUsd
|
|
178
|
+
? "unsupported"
|
|
179
|
+
: costObserved,
|
|
180
|
+
toolEvidence: normalized && "toolEvidence" in normalized ? "supported" : "unsupported",
|
|
181
|
+
policyMapping: "unknown",
|
|
182
|
+
interruption: "unknown",
|
|
183
|
+
streaming: "unknown",
|
|
184
|
+
steering: "unknown",
|
|
185
|
+
resume: "unknown",
|
|
186
|
+
images: "unknown",
|
|
187
|
+
contextTelemetry: "unknown",
|
|
188
|
+
preCompactionSignal: hookSpec
|
|
189
|
+
? hookSubcommands.has("pre-compact")
|
|
190
|
+
? "supported"
|
|
191
|
+
: "unsupported"
|
|
192
|
+
: "unknown",
|
|
193
|
+
postCompactionSignal: hookSpec
|
|
194
|
+
? hookSubcommands.has("post-compact") ||
|
|
195
|
+
(adapter.profile.id === "claude-code" && hookSubcommands.has("session-start"))
|
|
196
|
+
? "supported"
|
|
197
|
+
: "unsupported"
|
|
198
|
+
: "unknown",
|
|
199
|
+
compaction: "unknown",
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function reconcile(declared: CapabilitySupport, observed: BenchVerdict): BenchVerdict {
|
|
204
|
+
if (observed === "unknown" || observed === "skipped" || observed === "not_applicable") {
|
|
205
|
+
return observed;
|
|
206
|
+
}
|
|
207
|
+
return declared === observed ? observed : "drift";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function probeVersion(binary: string): string | null {
|
|
211
|
+
const result = spawnSync(binary, ["--version"], { encoding: "utf8", timeout: 5_000 });
|
|
212
|
+
if (result.error || result.status !== 0) return null;
|
|
213
|
+
return (result.stdout || result.stderr).trim().split("\n")[0] || "installed";
|
|
214
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type { Spawner, SpawnRequest, SpawnResult } from "../workflow/types.ts";
|
|
2
|
+
export type {
|
|
3
|
+
BenchDimension,
|
|
4
|
+
BenchResult,
|
|
5
|
+
BenchVerdict,
|
|
6
|
+
HarnessBenchOptions,
|
|
7
|
+
HarnessBenchReport,
|
|
8
|
+
} from "./bench.ts";
|
|
9
|
+
export { runHarnessBench } from "./bench.ts";
|
|
10
|
+
export type { BuiltinHarnessId } from "./profiles.ts";
|
|
11
|
+
export {
|
|
12
|
+
BUILTIN_HARNESS_IDS,
|
|
13
|
+
BUILTIN_HARNESS_PROFILES,
|
|
14
|
+
builtinHarnessProfile,
|
|
15
|
+
isBuiltinHarness,
|
|
16
|
+
validateHarnessEffort,
|
|
17
|
+
} from "./profiles.ts";
|
|
18
|
+
export { createBuiltinHarnessRegistry, HarnessRegistry } from "./registry.ts";
|
|
19
|
+
export type {
|
|
20
|
+
CapabilityClaim,
|
|
21
|
+
CapabilitySupport,
|
|
22
|
+
HarnessAdapter,
|
|
23
|
+
HarnessBenchFixture,
|
|
24
|
+
HarnessCapabilities,
|
|
25
|
+
HarnessCapabilityDimension,
|
|
26
|
+
HarnessId,
|
|
27
|
+
HarnessInvocation,
|
|
28
|
+
HarnessProfile,
|
|
29
|
+
HarnessRawResult,
|
|
30
|
+
} from "./types.ts";
|
|
31
|
+
export { HARNESS_CAPABILITY_DIMENSIONS } from "./types.ts";
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { CapabilityClaim, HarnessCapabilities, HarnessProfile } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
const supported = (note?: string): CapabilityClaim => ({ support: "supported", note });
|
|
4
|
+
const unsupported = (note?: string): CapabilityClaim => ({ support: "unsupported", note });
|
|
5
|
+
const partial = (note: string): CapabilityClaim => ({ support: "partial", note });
|
|
6
|
+
const unknown = (note: string): CapabilityClaim => ({ support: "unknown", note });
|
|
7
|
+
|
|
8
|
+
function capabilities(overrides: Partial<HarnessCapabilities>): HarnessCapabilities {
|
|
9
|
+
return {
|
|
10
|
+
invocation: supported("Headless CLI subprocess."),
|
|
11
|
+
modelSelection: supported("Explicit model flag is mapped by the adapter."),
|
|
12
|
+
effortSelection: unsupported(),
|
|
13
|
+
maxTurns: unsupported(),
|
|
14
|
+
finalResult: supported("Normalized to SpawnResult.text."),
|
|
15
|
+
sessionId: unsupported(),
|
|
16
|
+
cost: unsupported(),
|
|
17
|
+
toolEvidence: unsupported("The final-result adapter does not retain tool events."),
|
|
18
|
+
policyMapping: unsupported("No ALLOW/DENY/ASK translation at the workflow boundary."),
|
|
19
|
+
interruption: partial("Timeout kills the subprocess; no caller-driven interrupt handle."),
|
|
20
|
+
streaming: unsupported("Workflow children return one normalized final result."),
|
|
21
|
+
steering: unsupported("One prompt is fixed at subprocess launch."),
|
|
22
|
+
resume: unsupported("Workflow children always start a new vendor session."),
|
|
23
|
+
images: unsupported("SpawnRequest currently carries text only."),
|
|
24
|
+
contextTelemetry: unknown("The workflow adapter does not expose live context usage."),
|
|
25
|
+
preCompactionSignal: unknown("No compaction lifecycle probe has certified this adapter."),
|
|
26
|
+
postCompactionSignal: unknown("No compaction lifecycle probe has certified this adapter."),
|
|
27
|
+
compaction: unsupported("Harnery does not initiate native harness compaction."),
|
|
28
|
+
...overrides,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The one built-in profile catalog. Adding a fourth harness starts here; CLI
|
|
33
|
+
* choices, doctor metadata, workflow dispatch, and the bench derive from it. */
|
|
34
|
+
export const BUILTIN_HARNESS_PROFILES = {
|
|
35
|
+
"claude-code": {
|
|
36
|
+
id: "claude-code",
|
|
37
|
+
displayName: "Claude Code",
|
|
38
|
+
binary: "claude",
|
|
39
|
+
installHint: "npm install -g @anthropic-ai/claude-code",
|
|
40
|
+
loginHint: "run `claude` and use /login",
|
|
41
|
+
apiKeyEnv: "ANTHROPIC_API_KEY",
|
|
42
|
+
integrationMode: "cli-subprocess",
|
|
43
|
+
authModel: "own-auth",
|
|
44
|
+
modelFamily: "claude",
|
|
45
|
+
effortValues: ["low", "medium", "high", "xhigh", "max"],
|
|
46
|
+
verified: { date: "2026-07-20", version: "current CLI contract" },
|
|
47
|
+
capabilities: capabilities({
|
|
48
|
+
effortSelection: supported("Mapped to `--effort <level>`."),
|
|
49
|
+
maxTurns: supported("Mapped to `--max-turns <n>`."),
|
|
50
|
+
sessionId: supported("Read from the JSON result envelope."),
|
|
51
|
+
cost: supported("Read from total_cost_usd in the JSON result envelope."),
|
|
52
|
+
contextTelemetry: partial(
|
|
53
|
+
"Normalized when a hook payload reports context_window; no dedicated statusline bridge.",
|
|
54
|
+
),
|
|
55
|
+
preCompactionSignal: supported("Claude Code PreCompact is wired to a durable checkpoint."),
|
|
56
|
+
postCompactionSignal: supported(
|
|
57
|
+
"SessionStart source=compact completes recovery and injects a verified briefing.",
|
|
58
|
+
),
|
|
59
|
+
}),
|
|
60
|
+
},
|
|
61
|
+
codex: {
|
|
62
|
+
id: "codex",
|
|
63
|
+
displayName: "Codex",
|
|
64
|
+
binary: "codex",
|
|
65
|
+
installHint: "npm install -g @openai/codex",
|
|
66
|
+
loginHint: "codex login",
|
|
67
|
+
apiKeyEnv: "OPENAI_API_KEY",
|
|
68
|
+
integrationMode: "cli-subprocess",
|
|
69
|
+
authModel: "own-auth",
|
|
70
|
+
modelFamily: "gpt",
|
|
71
|
+
effortValues: ["none", "minimal", "low", "medium", "high", "xhigh"],
|
|
72
|
+
verified: { date: "2026-07-21", version: "codex-cli 0.145.0-alpha.18" },
|
|
73
|
+
capabilities: capabilities({
|
|
74
|
+
effortSelection: supported('Mapped to `-c model_reasoning_effort="<level>"`.'),
|
|
75
|
+
maxTurns: unsupported("codex exec exposes no turn-ceiling flag."),
|
|
76
|
+
sessionId: unsupported("--output-last-message carries no session id."),
|
|
77
|
+
cost: unsupported("The final-message path carries no usage or cost."),
|
|
78
|
+
contextTelemetry: partial(
|
|
79
|
+
"Normalized when native hook payloads report context_window; unavailable otherwise.",
|
|
80
|
+
),
|
|
81
|
+
preCompactionSignal: supported("Codex PreCompact is wired to a durable checkpoint."),
|
|
82
|
+
postCompactionSignal: supported(
|
|
83
|
+
"Codex PostCompact is observed; recovery is injected on the next submitted prompt.",
|
|
84
|
+
),
|
|
85
|
+
}),
|
|
86
|
+
},
|
|
87
|
+
cursor: {
|
|
88
|
+
id: "cursor",
|
|
89
|
+
displayName: "Cursor Agent",
|
|
90
|
+
binary: "cursor-agent",
|
|
91
|
+
installHint: "curl https://cursor.com/install -fsS | bash",
|
|
92
|
+
loginHint: "cursor-agent login",
|
|
93
|
+
apiKeyEnv: "CURSOR_API_KEY",
|
|
94
|
+
integrationMode: "cli-subprocess",
|
|
95
|
+
authModel: "own-auth",
|
|
96
|
+
modelFamily: "multi",
|
|
97
|
+
effortValues: [],
|
|
98
|
+
verified: { date: "2026-07-21", version: "2026.07.16-899851b" },
|
|
99
|
+
capabilities: capabilities({
|
|
100
|
+
effortSelection: unsupported(
|
|
101
|
+
"Cursor embeds effort in some parameterized model ids; Harnery does not rewrite model ids.",
|
|
102
|
+
),
|
|
103
|
+
maxTurns: unsupported("cursor-agent exposes no turn-ceiling flag."),
|
|
104
|
+
sessionId: supported("Read from the JSON result envelope."),
|
|
105
|
+
cost: unsupported("The JSON result envelope carries no cost."),
|
|
106
|
+
contextTelemetry: unknown("No stable Cursor context-usage payload has been certified."),
|
|
107
|
+
preCompactionSignal: unsupported(
|
|
108
|
+
"Harnery does not wire an unverified Cursor preCompact hook.",
|
|
109
|
+
),
|
|
110
|
+
postCompactionSignal: unsupported(
|
|
111
|
+
"Harnery does not wire an unverified Cursor postCompact hook.",
|
|
112
|
+
),
|
|
113
|
+
}),
|
|
114
|
+
},
|
|
115
|
+
} as const satisfies Record<string, HarnessProfile>;
|
|
116
|
+
|
|
117
|
+
export type BuiltinHarnessId = keyof typeof BUILTIN_HARNESS_PROFILES;
|
|
118
|
+
|
|
119
|
+
export const BUILTIN_HARNESS_IDS = Object.freeze(
|
|
120
|
+
Object.keys(BUILTIN_HARNESS_PROFILES) as BuiltinHarnessId[],
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
export function isBuiltinHarness(id: string): id is BuiltinHarnessId {
|
|
124
|
+
return Object.hasOwn(BUILTIN_HARNESS_PROFILES, id);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function builtinHarnessProfile(id: string): HarnessProfile | undefined {
|
|
128
|
+
return isBuiltinHarness(id) ? BUILTIN_HARNESS_PROFILES[id] : undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function validateHarnessEffort(id: string, effort: string | undefined): void {
|
|
132
|
+
if (!effort) return;
|
|
133
|
+
const profile = builtinHarnessProfile(id);
|
|
134
|
+
if (!profile) throw new Error(`unknown harness ${JSON.stringify(id)}`);
|
|
135
|
+
if (!profile.effortValues.includes(effort)) {
|
|
136
|
+
const supportedValues = profile.effortValues.length ? profile.effortValues.join(", ") : "none";
|
|
137
|
+
throw new Error(
|
|
138
|
+
`effort ${JSON.stringify(effort)} is not supported by ${id}; supported values: ${supportedValues}`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildClaudeInvocation,
|
|
3
|
+
claudeCodeSpawner,
|
|
4
|
+
normalizeClaudeResult,
|
|
5
|
+
} from "../workflow/spawn-claude.ts";
|
|
6
|
+
import {
|
|
7
|
+
buildCodexInvocation,
|
|
8
|
+
codexSpawner,
|
|
9
|
+
normalizeCodexResult,
|
|
10
|
+
} from "../workflow/spawn-codex.ts";
|
|
11
|
+
import {
|
|
12
|
+
buildCursorInvocation,
|
|
13
|
+
cursorSpawner,
|
|
14
|
+
normalizeCursorResult,
|
|
15
|
+
} from "../workflow/spawn-cursor.ts";
|
|
16
|
+
import {
|
|
17
|
+
BUILTIN_HARNESS_IDS,
|
|
18
|
+
BUILTIN_HARNESS_PROFILES,
|
|
19
|
+
type BuiltinHarnessId,
|
|
20
|
+
} from "./profiles.ts";
|
|
21
|
+
import type { HarnessAdapter, HarnessId } from "./types.ts";
|
|
22
|
+
|
|
23
|
+
const BUILTIN_ADAPTERS: Record<BuiltinHarnessId, HarnessAdapter> = {
|
|
24
|
+
"claude-code": {
|
|
25
|
+
profile: BUILTIN_HARNESS_PROFILES["claude-code"],
|
|
26
|
+
spawn: claudeCodeSpawner,
|
|
27
|
+
buildInvocation: buildClaudeInvocation,
|
|
28
|
+
normalizeResult: normalizeClaudeResult,
|
|
29
|
+
fixture: {
|
|
30
|
+
raw: {
|
|
31
|
+
stdout: JSON.stringify({
|
|
32
|
+
type: "result",
|
|
33
|
+
result: "HARNERY_BENCH_OK",
|
|
34
|
+
session_id: "claude-bench-session",
|
|
35
|
+
total_cost_usd: 0.0012,
|
|
36
|
+
}),
|
|
37
|
+
stderr: "",
|
|
38
|
+
exitCode: 0,
|
|
39
|
+
durationMs: 12,
|
|
40
|
+
},
|
|
41
|
+
expected: {
|
|
42
|
+
ok: true,
|
|
43
|
+
text: "HARNERY_BENCH_OK",
|
|
44
|
+
sessionId: "claude-bench-session",
|
|
45
|
+
costUsd: 0.0012,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
codex: {
|
|
50
|
+
profile: BUILTIN_HARNESS_PROFILES.codex,
|
|
51
|
+
spawn: codexSpawner,
|
|
52
|
+
buildInvocation: buildCodexInvocation,
|
|
53
|
+
normalizeResult: normalizeCodexResult,
|
|
54
|
+
fixture: {
|
|
55
|
+
raw: {
|
|
56
|
+
stdout: "event stream is not the final answer",
|
|
57
|
+
stderr: "",
|
|
58
|
+
exitCode: 0,
|
|
59
|
+
durationMs: 14,
|
|
60
|
+
resultFileText: "HARNERY_BENCH_OK\n",
|
|
61
|
+
},
|
|
62
|
+
expected: { ok: true, text: "HARNERY_BENCH_OK" },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
cursor: {
|
|
66
|
+
profile: BUILTIN_HARNESS_PROFILES.cursor,
|
|
67
|
+
spawn: cursorSpawner,
|
|
68
|
+
buildInvocation: buildCursorInvocation,
|
|
69
|
+
normalizeResult: normalizeCursorResult,
|
|
70
|
+
fixture: {
|
|
71
|
+
raw: {
|
|
72
|
+
stdout: JSON.stringify({
|
|
73
|
+
type: "result",
|
|
74
|
+
result: "HARNERY_BENCH_OK",
|
|
75
|
+
session_id: "cursor-bench-session",
|
|
76
|
+
}),
|
|
77
|
+
stderr: "",
|
|
78
|
+
exitCode: 0,
|
|
79
|
+
durationMs: 9,
|
|
80
|
+
},
|
|
81
|
+
expected: {
|
|
82
|
+
ok: true,
|
|
83
|
+
text: "HARNERY_BENCH_OK",
|
|
84
|
+
sessionId: "cursor-bench-session",
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** Mutable registry for embedders, initialized empty unless adapters are
|
|
91
|
+
* provided. Duplicate ids fail loud instead of silently replacing behavior. */
|
|
92
|
+
export class HarnessRegistry {
|
|
93
|
+
readonly #adapters = new Map<HarnessId, HarnessAdapter>();
|
|
94
|
+
|
|
95
|
+
constructor(adapters: Iterable<HarnessAdapter> = []) {
|
|
96
|
+
for (const adapter of adapters) this.register(adapter);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
register(adapter: HarnessAdapter): this {
|
|
100
|
+
const id = adapter.profile.id.trim();
|
|
101
|
+
if (!id) throw new Error("harness adapter id cannot be empty");
|
|
102
|
+
if (this.#adapters.has(id))
|
|
103
|
+
throw new Error(`harness adapter ${JSON.stringify(id)} is already registered`);
|
|
104
|
+
if (adapter.profile.binary.trim() === "") {
|
|
105
|
+
throw new Error(`harness adapter ${JSON.stringify(id)} has no binary`);
|
|
106
|
+
}
|
|
107
|
+
this.#adapters.set(id, adapter);
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
get(id: HarnessId): HarnessAdapter | undefined {
|
|
112
|
+
return this.#adapters.get(id);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
require(id: HarnessId): HarnessAdapter {
|
|
116
|
+
const adapter = this.get(id);
|
|
117
|
+
if (!adapter) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`unknown harness ${JSON.stringify(id)} (registered: ${this.ids().join(", ") || "none"})`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return adapter;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
list(): HarnessAdapter[] {
|
|
126
|
+
return Array.from(this.#adapters.values());
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
ids(): HarnessId[] {
|
|
130
|
+
return Array.from(this.#adapters.keys());
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
spawners(): Record<HarnessId, HarnessAdapter["spawn"]> {
|
|
134
|
+
return Object.fromEntries(this.list().map((adapter) => [adapter.profile.id, adapter.spawn]));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function createBuiltinHarnessRegistry(): HarnessRegistry {
|
|
139
|
+
return new HarnessRegistry(BUILTIN_HARNESS_IDS.map((id) => BUILTIN_ADAPTERS[id]));
|
|
140
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Spawner, SpawnRequest, SpawnResult } from "../workflow/types.ts";
|
|
2
|
+
|
|
3
|
+
/** Open harness identifier. Built-ins are registered at runtime rather than
|
|
4
|
+
* repeated through closed unions in every consumer. */
|
|
5
|
+
export type HarnessId = string;
|
|
6
|
+
|
|
7
|
+
export type CapabilitySupport = "supported" | "partial" | "unsupported" | "unknown";
|
|
8
|
+
|
|
9
|
+
export const HARNESS_CAPABILITY_DIMENSIONS = [
|
|
10
|
+
"invocation",
|
|
11
|
+
"modelSelection",
|
|
12
|
+
"effortSelection",
|
|
13
|
+
"maxTurns",
|
|
14
|
+
"finalResult",
|
|
15
|
+
"sessionId",
|
|
16
|
+
"cost",
|
|
17
|
+
"toolEvidence",
|
|
18
|
+
"policyMapping",
|
|
19
|
+
"interruption",
|
|
20
|
+
"streaming",
|
|
21
|
+
"steering",
|
|
22
|
+
"resume",
|
|
23
|
+
"images",
|
|
24
|
+
"contextTelemetry",
|
|
25
|
+
"preCompactionSignal",
|
|
26
|
+
"postCompactionSignal",
|
|
27
|
+
"compaction",
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
export type HarnessCapabilityDimension = (typeof HARNESS_CAPABILITY_DIMENSIONS)[number];
|
|
31
|
+
|
|
32
|
+
export interface CapabilityClaim {
|
|
33
|
+
support: CapabilitySupport;
|
|
34
|
+
/** Operational qualification. Required for partial/unknown claims and useful
|
|
35
|
+
* whenever "supported" has a narrower meaning than the vendor CLI itself. */
|
|
36
|
+
note?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type HarnessCapabilities = Record<HarnessCapabilityDimension, CapabilityClaim>;
|
|
40
|
+
|
|
41
|
+
export interface HarnessProfile {
|
|
42
|
+
id: HarnessId;
|
|
43
|
+
displayName: string;
|
|
44
|
+
binary: string;
|
|
45
|
+
installHint: string;
|
|
46
|
+
loginHint: string;
|
|
47
|
+
apiKeyEnv: string;
|
|
48
|
+
integrationMode: "cli-subprocess";
|
|
49
|
+
authModel: "own-auth";
|
|
50
|
+
modelFamily: "claude" | "gpt" | "multi";
|
|
51
|
+
effortValues: readonly string[];
|
|
52
|
+
capabilities: HarnessCapabilities;
|
|
53
|
+
/** The last real vendor CLI contract used to validate this declaration. */
|
|
54
|
+
verified?: { date: string; version: string };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Fully planned child invocation. `resultFile` is used by adapters such as
|
|
58
|
+
* Codex that deliver the authoritative final answer out-of-band. */
|
|
59
|
+
export interface HarnessInvocation {
|
|
60
|
+
argv: string[];
|
|
61
|
+
resultFile?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Vendor subprocess material passed through an adapter's normalizer. */
|
|
65
|
+
export interface HarnessRawResult {
|
|
66
|
+
stdout: string;
|
|
67
|
+
stderr: string;
|
|
68
|
+
exitCode: number;
|
|
69
|
+
durationMs: number;
|
|
70
|
+
resultFileText?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface HarnessBenchFixture {
|
|
74
|
+
raw: HarnessRawResult;
|
|
75
|
+
expected: Pick<SpawnResult, "ok" | "text"> & {
|
|
76
|
+
sessionId?: string;
|
|
77
|
+
costUsd?: number;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** One registered executable adapter. The same planner and normalizer power
|
|
82
|
+
* production workflow runs and the offline conformance bench, preventing the
|
|
83
|
+
* bench from testing a parallel mock implementation. */
|
|
84
|
+
export interface HarnessAdapter {
|
|
85
|
+
profile: HarnessProfile;
|
|
86
|
+
spawn: Spawner;
|
|
87
|
+
buildInvocation: (request: SpawnRequest, resultFile?: string) => HarnessInvocation;
|
|
88
|
+
normalizeResult: (raw: HarnessRawResult) => SpawnResult;
|
|
89
|
+
fixture: HarnessBenchFixture;
|
|
90
|
+
}
|