@plimeor/harness 0.1.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.
@@ -0,0 +1,9 @@
1
+ import './claude'
2
+ import './codex'
3
+ import './kiro'
4
+ import './pi'
5
+
6
+ export { claudeAdapter } from './claude'
7
+ export { codexAdapter } from './codex'
8
+ export { kiroAdapter } from './kiro'
9
+ export { piAdapter } from './pi'
@@ -0,0 +1,57 @@
1
+ import { harness } from '../registry'
2
+ import type { HarnessContext, RunOutputRequest, RunRequest, TextOutputRequest } from '../types'
3
+ import { configDirectory, createExtensionFacet } from './extensions'
4
+ import { createBuiltInAdapter, planTextCommand, unsupportedOutputMode } from './shared'
5
+
6
+ const HARNESS_ID = 'kiro'
7
+
8
+ export const kiroAdapter = createBuiltInAdapter({
9
+ commands: ['kiro-cli'],
10
+ id: HARNESS_ID,
11
+ identity: /kiro/i,
12
+ installHint: 'Install Kiro CLI and ensure `kiro-cli --version` is available on PATH.',
13
+ extensions(context: HarnessContext | undefined) {
14
+ const directory = context?.env?.KIRO_HOME ?? configDirectory(context?.home, '.kiro')
15
+ return createExtensionFacet({
16
+ configDirectory: directory,
17
+ context,
18
+ harnessId: HARNESS_ID,
19
+ mcp: { configFile: `${directory}/settings/mcp.json`, kind: 'kiro-cli' },
20
+ skillsDirectory: `${directory}/skills`,
21
+ hooks: {
22
+ hooksDirectory: `${directory}/hooks`,
23
+ kind: 'kiro-hook-files',
24
+ events: [
25
+ 'Manual',
26
+ 'PostFileCreate',
27
+ 'PostFileDelete',
28
+ 'PostFileSave',
29
+ 'PostTaskExec',
30
+ 'PostToolUse',
31
+ 'PreTaskExec',
32
+ 'PreToolUse',
33
+ 'SessionStart',
34
+ 'Stop',
35
+ 'UserPromptSubmit'
36
+ ]
37
+ }
38
+ })
39
+ },
40
+ plan(request: RunRequest<RunOutputRequest>, command: string, cwd: string) {
41
+ const output = request.output ?? ({ mode: 'text' } satisfies TextOutputRequest)
42
+ if (!output.mode || output.mode === 'text') {
43
+ return planTextCommand({
44
+ args: ['chat', '--no-interactive', '--wrap', 'never', request.prompt],
45
+ command,
46
+ cwd,
47
+ harnessId: HARNESS_ID,
48
+ output: { mode: 'text' },
49
+ request
50
+ })
51
+ }
52
+
53
+ return unsupportedOutputMode(HARNESS_ID, output)
54
+ }
55
+ })
56
+
57
+ harness.use(kiroAdapter)
@@ -0,0 +1,65 @@
1
+ import { harness } from '../registry'
2
+ import type { HarnessContext, JsonlOutputRequest, RunOutputRequest, RunRequest, TextOutputRequest } from '../types'
3
+ import { configDirectory, createExtensionFacet } from './extensions'
4
+ import { createBuiltInAdapter, planCommand, shellQuote, unsupportedOutputMode } from './shared'
5
+
6
+ const HARNESS_ID = 'pi'
7
+
8
+ export const piAdapter = createBuiltInAdapter({
9
+ commands: ['pi'],
10
+ id: HARNESS_ID,
11
+ identity: /^\d+\.\d+\.\d+/,
12
+ installHint: 'Install pi and ensure `pi --version` is available on PATH.',
13
+ extensions(context: HarnessContext | undefined) {
14
+ const directory = configDirectory(context?.home, '.pi/agent')
15
+ return createExtensionFacet({
16
+ configDirectory: directory,
17
+ context,
18
+ harnessId: HARNESS_ID,
19
+ skillsDirectory: `${directory}/skills`
20
+ })
21
+ },
22
+ plan(request: RunRequest<RunOutputRequest>, command: string, cwd: string) {
23
+ const output = request.output ?? ({ mode: 'text' } satisfies TextOutputRequest)
24
+ if (output.mode === 'jsonl') {
25
+ return planCommand({
26
+ args: ['-lc', piCommand(command, request.prompt, 'json')],
27
+ command: 'fish',
28
+ cwd,
29
+ harnessId: HARNESS_ID,
30
+ output: output satisfies JsonlOutputRequest,
31
+ request
32
+ })
33
+ }
34
+
35
+ if (!output.mode || output.mode === 'text') {
36
+ return planCommand({
37
+ args: ['-lc', piCommand(command, request.prompt, 'text')],
38
+ command: 'fish',
39
+ cwd,
40
+ harnessId: HARNESS_ID,
41
+ output: { mode: 'text' },
42
+ request
43
+ })
44
+ }
45
+
46
+ return unsupportedOutputMode(HARNESS_ID, output)
47
+ }
48
+ })
49
+
50
+ harness.use(piAdapter)
51
+
52
+ function piCommand(command: string, prompt: string, mode: 'json' | 'text'): string {
53
+ return [
54
+ shellQuote(command),
55
+ '-p',
56
+ '--no-session',
57
+ '--no-tools',
58
+ '--no-extensions',
59
+ '--no-skills',
60
+ '--no-context-files',
61
+ '--mode',
62
+ mode,
63
+ shellQuote(prompt)
64
+ ].join(' ')
65
+ }
@@ -0,0 +1,338 @@
1
+ import { access } from 'node:fs/promises'
2
+ import { delimiter, join } from 'node:path'
3
+
4
+ import { HarnessPlanError } from '../errors'
5
+ import { createProcessRunner } from '../process'
6
+ import type {
7
+ CommandPlan,
8
+ ExtensionFacet,
9
+ HarnessAdapter,
10
+ HarnessContext,
11
+ HarnessDetection,
12
+ HarnessHandle,
13
+ HarnessId,
14
+ HarnessRun,
15
+ HealthReport,
16
+ JsonlOutputRequest,
17
+ ProcessFacet,
18
+ RunOutputRequest,
19
+ RunRequest,
20
+ StructuredOutputRequest,
21
+ TextOutputRequest
22
+ } from '../types'
23
+
24
+ type BuiltInAdapterConfig = {
25
+ id: Extract<HarnessId, 'claude' | 'codex' | 'kiro' | 'pi'>
26
+ commands: string[]
27
+ identity: RegExp
28
+ identityArgs?: string[]
29
+ installHint: string
30
+ requiresGoogleAccessBeforeSmoke?: boolean
31
+ extensions(context: HarnessContext | undefined): ExtensionFacet
32
+ plan(request: RunRequest<RunOutputRequest>, command: string, cwd: string): CommandPlan<RunOutputRequest>
33
+ }
34
+
35
+ const HEALTH_PROMPT_TIMEOUT_MS = 30_000
36
+ const GOOGLE_ACCESS_TIMEOUT_MS = 5_000
37
+ const GOOGLE_ACCESS_URL = 'https://www.google.com/generate_204'
38
+
39
+ export function createBuiltInAdapter(config: BuiltInAdapterConfig): HarnessAdapter {
40
+ return {
41
+ id: config.id,
42
+ async detect(context?: HarnessContext): Promise<HarnessDetection> {
43
+ return detectCommand(config, context)
44
+ },
45
+
46
+ async open(context?: HarnessContext): Promise<HarnessHandle> {
47
+ const detection = await detectCommand(config, context)
48
+ const runner = createProcessRunner(config.id)
49
+ const process: ProcessFacet = {
50
+ async plan<Output extends RunOutputRequest = TextOutputRequest>(
51
+ request: RunRequest<Output>
52
+ ): Promise<CommandPlan<Output>> {
53
+ if (!detection.detected || !detection.binary) {
54
+ throw new HarnessPlanError({
55
+ harnessId: config.id,
56
+ kind: 'unsupported_operation',
57
+ message: `${config.id} CLI is not installed or was not recognized.`
58
+ })
59
+ }
60
+
61
+ return config.plan(request, detection.binary.command, resolveCwd(context)) as CommandPlan<Output>
62
+ },
63
+ async run<Output extends RunOutputRequest = TextOutputRequest>(
64
+ input: RunRequest<Output> | CommandPlan<Output>
65
+ ): Promise<HarnessRun<Output>> {
66
+ if (isCommandPlan(input)) {
67
+ return runner.run(input)
68
+ }
69
+
70
+ const plan = await process.plan(input)
71
+ return runner.run(plan)
72
+ }
73
+ }
74
+
75
+ return {
76
+ detection,
77
+ extensions: config.extensions(context),
78
+ health: {
79
+ async check(): Promise<HealthReport> {
80
+ if (!detection.detected || !detection.binary) {
81
+ return {
82
+ message: `${config.id} CLI is not installed or was not recognized. ${config.installHint}`,
83
+ success: false
84
+ }
85
+ }
86
+
87
+ if (config.requiresGoogleAccessBeforeSmoke) {
88
+ const googleAccess = await checkGoogleAccess(config.id)
89
+ if (!googleAccess.success) {
90
+ return googleAccess
91
+ }
92
+ }
93
+
94
+ const plan = config.plan(
95
+ {
96
+ output: { mode: 'text' },
97
+ prompt: 'Reply with OK.',
98
+ timeoutMs: HEALTH_PROMPT_TIMEOUT_MS
99
+ },
100
+ detection.binary.command,
101
+ resolveCwd(context)
102
+ )
103
+
104
+ try {
105
+ const run = await runner.run(plan)
106
+ const result = await run.result
107
+ if (result.finalText.trim().length > 0) {
108
+ return { success: true }
109
+ }
110
+
111
+ return {
112
+ message: `${config.id} CLI did not produce output for the health prompt within ${HEALTH_PROMPT_TIMEOUT_MS}ms.`,
113
+ success: false
114
+ }
115
+ } catch (error) {
116
+ return {
117
+ message: `${config.id} CLI did not respond to the health prompt within ${HEALTH_PROMPT_TIMEOUT_MS}ms: ${formatError(error)}`,
118
+ success: false
119
+ }
120
+ }
121
+ }
122
+ },
123
+ process
124
+ }
125
+ }
126
+ }
127
+ }
128
+
129
+ async function checkGoogleAccess(harnessId: HarnessId): Promise<HealthReport> {
130
+ const controller = new AbortController()
131
+ const timeout = setTimeout(() => {
132
+ controller.abort()
133
+ }, GOOGLE_ACCESS_TIMEOUT_MS)
134
+
135
+ try {
136
+ const response = await fetch(GOOGLE_ACCESS_URL, {
137
+ method: 'GET',
138
+ signal: controller.signal
139
+ })
140
+
141
+ if (response.ok) {
142
+ return { success: true }
143
+ }
144
+
145
+ return {
146
+ message: `${harnessId} CLI smoke prompt was skipped because google.com is not reachable. ${GOOGLE_ACCESS_URL} returned HTTP ${response.status}. Check local network access to google.com.`,
147
+ success: false
148
+ }
149
+ } catch (error) {
150
+ return {
151
+ message: `${harnessId} CLI smoke prompt was skipped because google.com is not reachable within ${GOOGLE_ACCESS_TIMEOUT_MS}ms. Check local network access to google.com: ${formatError(error)}`,
152
+ success: false
153
+ }
154
+ } finally {
155
+ clearTimeout(timeout)
156
+ }
157
+ }
158
+
159
+ function isCommandPlan<Output extends RunOutputRequest>(
160
+ input: RunRequest<Output> | CommandPlan<Output>
161
+ ): input is CommandPlan<Output> {
162
+ return 'args' in input && 'command' in input && 'harnessId' in input
163
+ }
164
+
165
+ export function unsupportedOutputMode(harnessId: HarnessId, output: RunOutputRequest): never {
166
+ throw new HarnessPlanError({
167
+ harnessId,
168
+ kind: 'unsupported_output_mode',
169
+ message: unsupportedOutputModeMessage(harnessId, output)
170
+ })
171
+ }
172
+
173
+ function unsupportedOutputModeMessage(harnessId: HarnessId, output: RunOutputRequest): string {
174
+ const mode = output.mode ?? 'text'
175
+ const prefix = `${harnessId} adapter does not support ${mode} output mode.`
176
+
177
+ if (mode === 'structured') {
178
+ return [
179
+ prefix,
180
+ 'The current model path does not support structured output for this harness.',
181
+ 'Use text output and put the required JSON shape, field names, constraints, and validation rules directly in the prompt.'
182
+ ].join(' ')
183
+ }
184
+
185
+ if (mode === 'jsonl') {
186
+ return [
187
+ prefix,
188
+ 'The current model path does not support native JSONL events for this harness.',
189
+ 'Use text output and ask the model to emit one valid JSON object per line, or choose a harness with native JSONL support.'
190
+ ].join(' ')
191
+ }
192
+
193
+ return prefix
194
+ }
195
+
196
+ export function planTextCommand<Output extends RunOutputRequest>(input: {
197
+ harnessId: HarnessId
198
+ request: RunRequest<Output>
199
+ command: string
200
+ args: string[]
201
+ cwd: string
202
+ output: TextOutputRequest | JsonlOutputRequest
203
+ }): CommandPlan<Output> {
204
+ return {
205
+ args: input.args,
206
+ command: input.command,
207
+ cwd: input.request.cwd ?? input.cwd,
208
+ env: input.request.env,
209
+ harnessId: input.harnessId,
210
+ output: input.output as Output,
211
+ stdin: input.request.stdin,
212
+ timeoutMs: input.request.timeoutMs
213
+ }
214
+ }
215
+
216
+ export function planCommand<Output extends RunOutputRequest>(input: {
217
+ harnessId: HarnessId
218
+ request: RunRequest<Output>
219
+ command: string
220
+ args: string[]
221
+ cwd: string
222
+ output: TextOutputRequest | JsonlOutputRequest | StructuredOutputRequest
223
+ env?: Record<string, string | undefined>
224
+ }): CommandPlan<Output> {
225
+ return {
226
+ args: input.args,
227
+ command: input.command,
228
+ cwd: input.request.cwd ?? input.cwd,
229
+ env: mergeEnv(input.env, input.request.env),
230
+ harnessId: input.harnessId,
231
+ output: input.output as Output,
232
+ stdin: input.request.stdin,
233
+ timeoutMs: input.request.timeoutMs
234
+ }
235
+ }
236
+
237
+ function mergeEnv(
238
+ base: Record<string, string | undefined> | undefined,
239
+ patch: Record<string, string | undefined> | undefined
240
+ ): Record<string, string | undefined> | undefined {
241
+ const env = { ...base, ...patch }
242
+ return Object.keys(env).length > 0 ? env : undefined
243
+ }
244
+
245
+ export function shellQuote(value: string): string {
246
+ return `'${value.replaceAll("'", "'\\''")}'`
247
+ }
248
+
249
+ async function detectCommand(
250
+ config: BuiltInAdapterConfig,
251
+ context: HarnessContext | undefined
252
+ ): Promise<HarnessDetection> {
253
+ for (const command of config.commands) {
254
+ const resolved = await resolveCommand(command, context?.env)
255
+ if (!resolved) {
256
+ continue
257
+ }
258
+
259
+ const identity = await readIdentity(resolved, context, config.identityArgs ?? ['--version'])
260
+ if (identity && config.identity.test(identity)) {
261
+ return {
262
+ binary: { command: resolved, identity },
263
+ detected: true,
264
+ id: config.id
265
+ }
266
+ }
267
+ }
268
+
269
+ return { detected: false, id: config.id }
270
+ }
271
+
272
+ async function resolveCommand(
273
+ command: string,
274
+ env: Record<string, string | undefined> | undefined
275
+ ): Promise<string | undefined> {
276
+ if (command.includes('/')) {
277
+ return (await isExecutable(command)) ? command : undefined
278
+ }
279
+
280
+ for (const directory of (env?.PATH ?? process.env.PATH ?? '').split(delimiter)) {
281
+ if (!directory) {
282
+ continue
283
+ }
284
+
285
+ const candidate = join(directory, command)
286
+ if (await isExecutable(candidate)) {
287
+ return candidate
288
+ }
289
+ }
290
+
291
+ return undefined
292
+ }
293
+
294
+ async function isExecutable(path: string): Promise<boolean> {
295
+ try {
296
+ await access(path, 0b001)
297
+ return true
298
+ } catch {
299
+ return false
300
+ }
301
+ }
302
+
303
+ async function readIdentity(
304
+ command: string,
305
+ context: HarnessContext | undefined,
306
+ args: string[]
307
+ ): Promise<string | undefined> {
308
+ try {
309
+ const subprocess = Bun.spawn({
310
+ cmd: [command, ...args],
311
+ cwd: resolveCwd(context),
312
+ env: Object.fromEntries(
313
+ Object.entries({ ...process.env, ...(context?.env ?? {}) }).filter((entry): entry is [string, string] => {
314
+ return typeof entry[1] === 'string'
315
+ })
316
+ ),
317
+ stderr: 'pipe',
318
+ stdout: 'pipe'
319
+ })
320
+ const [exitCode, stdout, stderr] = await Promise.all([
321
+ subprocess.exited,
322
+ new Response(subprocess.stdout).text(),
323
+ new Response(subprocess.stderr).text()
324
+ ])
325
+ const identity = `${stdout}\n${stderr}`.trim()
326
+ return exitCode === 0 && identity.length > 0 ? identity : undefined
327
+ } catch {
328
+ return undefined
329
+ }
330
+ }
331
+
332
+ function resolveCwd(context: HarnessContext | undefined): string {
333
+ return context?.cwd ?? process.cwd()
334
+ }
335
+
336
+ function formatError(error: unknown): string {
337
+ return error instanceof Error ? error.message : String(error)
338
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,43 @@
1
+ import type { HarnessId, RunOutputRequest } from './types'
2
+
3
+ export type OutputErrorKind = 'json_parse_failed' | 'structured_validation_failed'
4
+ export type OutputErrorMode = Exclude<RunOutputRequest['mode'], 'text' | undefined>
5
+
6
+ export class HarnessRunOutputError extends Error {
7
+ readonly kind: OutputErrorKind
8
+ readonly outputMode: OutputErrorMode
9
+ readonly finalText: string
10
+ readonly exitCode: number | null
11
+ readonly signal?: string
12
+ override readonly cause: unknown
13
+
14
+ constructor(input: {
15
+ kind: OutputErrorKind
16
+ outputMode: OutputErrorMode
17
+ finalText: string
18
+ exitCode: number | null
19
+ signal?: string
20
+ cause: unknown
21
+ }) {
22
+ super(`${input.outputMode} output failed: ${input.kind}`)
23
+ this.name = 'HarnessRunOutputError'
24
+ this.kind = input.kind
25
+ this.outputMode = input.outputMode
26
+ this.finalText = input.finalText
27
+ this.exitCode = input.exitCode
28
+ this.signal = input.signal
29
+ this.cause = input.cause
30
+ }
31
+ }
32
+
33
+ export class HarnessPlanError extends Error {
34
+ readonly harnessId: HarnessId
35
+ readonly kind: 'unsupported_output_mode' | 'unsupported_operation'
36
+
37
+ constructor(input: { harnessId: HarnessId; kind: HarnessPlanError['kind']; message: string }) {
38
+ super(input.message)
39
+ this.name = 'HarnessPlanError'
40
+ this.harnessId = input.harnessId
41
+ this.kind = input.kind
42
+ }
43
+ }
package/src/index.ts ADDED
@@ -0,0 +1,34 @@
1
+ import './adapters'
2
+
3
+ export { HarnessPlanError, HarnessRunOutputError } from './errors'
4
+ export { harness } from './registry'
5
+ export type {
6
+ CommandPlan,
7
+ ExtensionCheckResult,
8
+ ExtensionFacet,
9
+ ExtensionIssue,
10
+ ExtensionResourceKind,
11
+ ExtensionResources,
12
+ ExtensionResult,
13
+ HarnessAdapter,
14
+ HarnessContext,
15
+ HarnessDetection,
16
+ HarnessExtension,
17
+ HarnessHandle,
18
+ HarnessId,
19
+ HarnessRegistry,
20
+ HarnessRun,
21
+ HarnessRunEvent,
22
+ HarnessRunResult,
23
+ HealthFacet,
24
+ HealthReport,
25
+ HookResource,
26
+ JsonlOutputRequest,
27
+ McpServerResource,
28
+ ProcessFacet,
29
+ RunOutputRequest,
30
+ RunRequest,
31
+ StructuredOutputRequest,
32
+ StructuredRunResult,
33
+ TextOutputRequest
34
+ } from './types'