@a5c-ai/adapters-harness-mock 5.1.1-staging.00ceebd28cf2

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 (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +227 -0
  3. package/dist/bin/adapters-harness-mock.d.ts +32 -0
  4. package/dist/bin/adapters-harness-mock.d.ts.map +1 -0
  5. package/dist/bin/adapters-harness-mock.js +155 -0
  6. package/dist/bin/adapters-harness-mock.js.map +1 -0
  7. package/dist/bin/mock-harness.d.ts +2 -0
  8. package/dist/bin/mock-harness.d.ts.map +1 -0
  9. package/dist/bin/mock-harness.js +25 -0
  10. package/dist/bin/mock-harness.js.map +1 -0
  11. package/dist/http-mock.d.ts +39 -0
  12. package/dist/http-mock.d.ts.map +1 -0
  13. package/dist/http-mock.js +206 -0
  14. package/dist/http-mock.js.map +1 -0
  15. package/dist/index.d.ts +9 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +10 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/mock-process.d.ts +57 -0
  20. package/dist/mock-process.d.ts.map +1 -0
  21. package/dist/mock-process.js +388 -0
  22. package/dist/mock-process.js.map +1 -0
  23. package/dist/mocks/index.d.ts +53 -0
  24. package/dist/mocks/index.d.ts.map +1 -0
  25. package/dist/mocks/index.js +140 -0
  26. package/dist/mocks/index.js.map +1 -0
  27. package/dist/mocks/mock-types.d.ts +170 -0
  28. package/dist/mocks/mock-types.d.ts.map +1 -0
  29. package/dist/mocks/mock-types.js +2 -0
  30. package/dist/mocks/mock-types.js.map +1 -0
  31. package/dist/mocks/programmatic-mocks.d.ts +40 -0
  32. package/dist/mocks/programmatic-mocks.d.ts.map +1 -0
  33. package/dist/mocks/programmatic-mocks.js +229 -0
  34. package/dist/mocks/programmatic-mocks.js.map +1 -0
  35. package/dist/mocks/remote-mocks.d.ts +43 -0
  36. package/dist/mocks/remote-mocks.d.ts.map +1 -0
  37. package/dist/mocks/remote-mocks.js +332 -0
  38. package/dist/mocks/remote-mocks.js.map +1 -0
  39. package/dist/mocks/scriptable-transport.d.ts +23 -0
  40. package/dist/mocks/scriptable-transport.d.ts.map +1 -0
  41. package/dist/mocks/scriptable-transport.js +83 -0
  42. package/dist/mocks/scriptable-transport.js.map +1 -0
  43. package/dist/multi-execution.d.ts +1 -0
  44. package/dist/multi-execution.d.ts.map +1 -0
  45. package/dist/multi-execution.js +2 -0
  46. package/dist/multi-execution.js.map +1 -0
  47. package/dist/probe.d.ts +113 -0
  48. package/dist/probe.d.ts.map +1 -0
  49. package/dist/probe.js +722 -0
  50. package/dist/probe.js.map +1 -0
  51. package/dist/scenarios/errors.d.ts +21 -0
  52. package/dist/scenarios/errors.d.ts.map +1 -0
  53. package/dist/scenarios/errors.js +73 -0
  54. package/dist/scenarios/errors.js.map +1 -0
  55. package/dist/scenarios/hooks.d.ts +17 -0
  56. package/dist/scenarios/hooks.d.ts.map +1 -0
  57. package/dist/scenarios/hooks.js +129 -0
  58. package/dist/scenarios/hooks.js.map +1 -0
  59. package/dist/scenarios/index.d.ts +22 -0
  60. package/dist/scenarios/index.d.ts.map +1 -0
  61. package/dist/scenarios/index.js +65 -0
  62. package/dist/scenarios/index.js.map +1 -0
  63. package/dist/scenarios/interactive.d.ts +17 -0
  64. package/dist/scenarios/interactive.d.ts.map +1 -0
  65. package/dist/scenarios/interactive.js +65 -0
  66. package/dist/scenarios/interactive.js.map +1 -0
  67. package/dist/scenarios/per-agent.d.ts +17 -0
  68. package/dist/scenarios/per-agent.d.ts.map +1 -0
  69. package/dist/scenarios/per-agent.js +597 -0
  70. package/dist/scenarios/per-agent.js.map +1 -0
  71. package/dist/scenarios/wire-format.d.ts +56 -0
  72. package/dist/scenarios/wire-format.d.ts.map +1 -0
  73. package/dist/scenarios/wire-format.js +261 -0
  74. package/dist/scenarios/wire-format.js.map +1 -0
  75. package/dist/scenarios.d.ts +29 -0
  76. package/dist/scenarios.d.ts.map +1 -0
  77. package/dist/scenarios.js +141 -0
  78. package/dist/scenarios.js.map +1 -0
  79. package/dist/types.d.ts +463 -0
  80. package/dist/types.d.ts.map +1 -0
  81. package/dist/types.js +9 -0
  82. package/dist/types.js.map +1 -0
  83. package/dist/websocket-mock.d.ts +58 -0
  84. package/dist/websocket-mock.d.ts.map +1 -0
  85. package/dist/websocket-mock.js +367 -0
  86. package/dist/websocket-mock.js.map +1 -0
  87. package/dist/workspace.d.ts +52 -0
  88. package/dist/workspace.d.ts.map +1 -0
  89. package/dist/workspace.js +157 -0
  90. package/dist/workspace.js.map +1 -0
  91. package/package.json +71 -0
package/dist/probe.js ADDED
@@ -0,0 +1,722 @@
1
+ /**
2
+ * HarnessProbe — probes real harness installations to capture behavior profiles.
3
+ *
4
+ * Used by the CI pipeline to periodically compare mock fidelity against
5
+ * real harness behavior. Runs the actual CLI tools with controlled inputs
6
+ * and records output format, timing, exit codes, etc.
7
+ */
8
+ import { execFile } from 'node:child_process';
9
+ import * as fs from 'node:fs';
10
+ import * as os from 'node:os';
11
+ import * as path from 'node:path';
12
+ function createSubprocessProbe(harness, command, metadata) {
13
+ return {
14
+ harness,
15
+ executionType: 'subprocess',
16
+ command,
17
+ ...metadata,
18
+ };
19
+ }
20
+ function createOfflineContractProbe(harness, executionType, metadata) {
21
+ return {
22
+ harness,
23
+ executionType,
24
+ command: process.execPath,
25
+ availability: 'offline-only',
26
+ ...metadata,
27
+ };
28
+ }
29
+ /** Pre-configured probe configurations for built-in harness and transport targets. */
30
+ export const PROBE_CONFIGS = {
31
+ 'claude-code': createSubprocessProbe('claude-code', 'claude', {
32
+ versionArgs: ['--version'],
33
+ helpArgs: ['--help'],
34
+ args: ['-p', 'Say hello in one word', '--output-format', 'json'],
35
+ availability: 'local-manual',
36
+ timeoutMs: 30000,
37
+ stdinSignals: ['(y/N)', '(y/n)', 'permission', 'approval'],
38
+ fileOperationPatterns: ['Read', 'Write', 'Edit', 'MultiEdit'],
39
+ environmentVariables: ['ANTHROPIC_API_KEY'],
40
+ cliPatterns: {
41
+ command: 'claude',
42
+ args: '-p "Say hello in one word" --output-format json',
43
+ versionArgs: '--version',
44
+ helpArgs: '--help',
45
+ },
46
+ outputFormat: 'jsonl',
47
+ outputFormatTraits: ['jsonl', 'newline-delimited', 'session-envelopes', 'tool-events', 'cost-events'],
48
+ probeNotes: ['Interactive/authenticated prompt probes are local-manual because Claude may require login or tool approval.'],
49
+ scenarios: [
50
+ { name: 'error', args: ['--definitely-invalid-flag'] },
51
+ ],
52
+ }),
53
+ 'codex': createSubprocessProbe('codex', 'codex', {
54
+ versionArgs: ['--version'],
55
+ helpArgs: ['--help'],
56
+ args: ['exec', '--json', 'Say hello in one word'],
57
+ availability: 'local-manual',
58
+ timeoutMs: 30000,
59
+ stdinSignals: ['approval', 'sandbox', 'Enter'],
60
+ fileOperationPatterns: ['apply_patch', 'write_file', 'function_call'],
61
+ environmentVariables: ['OPENAI_API_KEY'],
62
+ cliPatterns: {
63
+ command: 'codex',
64
+ args: 'exec --json "Say hello in one word"',
65
+ versionArgs: '--version',
66
+ helpArgs: '--help',
67
+ },
68
+ outputFormat: 'jsonl',
69
+ outputFormatTraits: ['jsonl', 'newline-delimited', 'session-envelopes', 'tool-events', 'function-call-events'],
70
+ probeNotes: ['The success scenario is local-manual because exec mode can require auth and version-dependent flags.'],
71
+ scenarios: [
72
+ { name: 'error', args: ['--definitely-invalid-flag'] },
73
+ ],
74
+ }),
75
+ 'gemini': createSubprocessProbe('gemini', 'gemini', {
76
+ versionArgs: ['--version'],
77
+ helpArgs: ['--help'],
78
+ args: ['--prompt', 'Say hello in one word'],
79
+ availability: 'local-manual',
80
+ timeoutMs: 30000,
81
+ stdinSignals: ['login', 'OAuth', 'interactive'],
82
+ fileOperationPatterns: ['tool_call', 'tool_result'],
83
+ environmentVariables: ['GOOGLE_API_KEY', 'GEMINI_API_KEY', 'GEMINI_CLI', 'GEMINI_SESSION_ID'],
84
+ cliPatterns: {
85
+ command: 'gemini',
86
+ args: '--prompt "Say hello in one word"',
87
+ versionArgs: '--version',
88
+ helpArgs: '--help',
89
+ },
90
+ outputFormat: 'jsonl',
91
+ outputFormatTraits: ['jsonl', 'thinking-events', 'tool-events'],
92
+ probeNotes: ['Gemini live prompt probes are expected to run locally with configured auth.'],
93
+ scenarios: [
94
+ { name: 'error', args: ['--definitely-invalid-flag'] },
95
+ ],
96
+ }),
97
+ 'copilot': createSubprocessProbe('copilot', 'gh', {
98
+ versionArgs: ['copilot', '--version'],
99
+ helpArgs: ['copilot', '--help'],
100
+ availability: 'local-manual',
101
+ timeoutMs: 30000,
102
+ stdinSignals: ['Install GitHub Copilot CLI?', 'Authenticate with GitHub'],
103
+ fileOperationPatterns: ['plain-text suggestions'],
104
+ environmentVariables: ['GITHUB_TOKEN', 'COPILOT_CLI_SESSION', 'GH_COPILOT_SESSION'],
105
+ cliPatterns: {
106
+ command: 'gh',
107
+ transportCommand: 'gh copilot',
108
+ versionArgs: 'copilot --version',
109
+ helpArgs: 'copilot --help',
110
+ },
111
+ outputFormat: 'text',
112
+ outputFormatTraits: ['plain-text', 'stderr-install-prompts'],
113
+ probeNotes: ['Copilot probes are local-manual because the CLI may prompt to install the extension first.'],
114
+ scenarios: [
115
+ { name: 'error', args: ['copilot', '--definitely-invalid-flag'] },
116
+ ],
117
+ }),
118
+ 'cursor': createSubprocessProbe('cursor', 'cursor', {
119
+ versionArgs: ['--version'],
120
+ helpArgs: ['--help'],
121
+ availability: 'local-manual',
122
+ timeoutMs: 30000,
123
+ stdinSignals: ['login', 'approval', 'interactive'],
124
+ fileOperationPatterns: ['edit_file', 'tool_call'],
125
+ environmentVariables: ['CURSOR_API_KEY', 'CURSOR_SESSION', 'CURSOR_AGENT_SESSION'],
126
+ cliPatterns: {
127
+ command: 'cursor',
128
+ versionArgs: '--version',
129
+ helpArgs: '--help',
130
+ },
131
+ outputFormat: 'jsonl',
132
+ outputFormatTraits: ['jsonl', 'session-envelopes', 'tool-events'],
133
+ probeNotes: ['Cursor probes are local-manual because install and auth are user-specific.'],
134
+ scenarios: [
135
+ { name: 'error', args: ['--definitely-invalid-flag'] },
136
+ ],
137
+ }),
138
+ 'opencode': createSubprocessProbe('opencode', 'opencode', {
139
+ versionArgs: ['--version'],
140
+ helpArgs: ['--help'],
141
+ availability: 'local-manual',
142
+ timeoutMs: 30000,
143
+ stdinSignals: ['opencode auth', 'approval', 'interactive'],
144
+ fileOperationPatterns: ['write_file', 'tool_result', 'session_end'],
145
+ environmentVariables: ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GOOGLE_API_KEY', 'OPENCODE_SESSION_ID', 'OPENCODE_CONFIG'],
146
+ cliPatterns: {
147
+ command: 'opencode',
148
+ versionArgs: '--version',
149
+ helpArgs: '--help',
150
+ },
151
+ outputFormat: 'jsonl',
152
+ outputFormatTraits: ['jsonl', 'session-envelopes', 'tool-events', 'cost-events'],
153
+ probeNotes: ['OpenCode CLI prompt probes are local-manual because auth and provider configuration vary by machine.'],
154
+ scenarios: [
155
+ { name: 'error', args: ['--definitely-invalid-flag'] },
156
+ ],
157
+ }),
158
+ 'pi': createSubprocessProbe('pi', 'pi', {
159
+ versionArgs: ['--version'],
160
+ helpArgs: ['--help'],
161
+ args: ['--help'],
162
+ availability: 'ci-or-local',
163
+ timeoutMs: 30000,
164
+ stdinSignals: ['--print', 'interactive', 'resume'],
165
+ fileOperationPatterns: ['read', 'bash', 'edit', 'write'],
166
+ environmentVariables: ['PI_API_KEY', 'ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'PI_RUN_ID', 'PI_SESSION_ID'],
167
+ cliPatterns: {
168
+ command: 'pi',
169
+ args: '--help',
170
+ versionArgs: '--version',
171
+ helpArgs: '--help',
172
+ },
173
+ outputFormat: 'text',
174
+ outputFormatTraits: ['plain-text', 'environment-docs', 'tool-listing'],
175
+ probeNotes: ['pi exposes stable help/version output suitable for CI-safe smoke checks in addition to local/manual prompt probes.'],
176
+ scenarios: [
177
+ { name: 'error', args: ['--definitely-invalid-flag'] },
178
+ ],
179
+ }),
180
+ 'omp': createSubprocessProbe('omp', 'omp', {
181
+ versionArgs: ['--version'],
182
+ helpArgs: ['--help'],
183
+ availability: 'local-manual',
184
+ timeoutMs: 30000,
185
+ stdinSignals: ['interactive', 'approval'],
186
+ fileOperationPatterns: ['search_repo', 'tool_call'],
187
+ environmentVariables: ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY'],
188
+ cliPatterns: {
189
+ command: 'omp',
190
+ versionArgs: '--version',
191
+ helpArgs: '--help',
192
+ },
193
+ outputFormat: 'jsonl',
194
+ outputFormatTraits: ['jsonl', 'session-envelopes', 'tool-events'],
195
+ probeNotes: ['OMP probes are contract-complete but typically local-manual because the binary is not expected in CI.'],
196
+ scenarios: [
197
+ { name: 'error', args: ['--definitely-invalid-flag'] },
198
+ ],
199
+ }),
200
+ 'openclaw': createSubprocessProbe('openclaw', 'openclaw', {
201
+ versionArgs: ['--version'],
202
+ helpArgs: ['--help'],
203
+ availability: 'local-manual',
204
+ timeoutMs: 30000,
205
+ stdinSignals: ['interactive', 'approval'],
206
+ fileOperationPatterns: ['open_plugin_channel', 'tool_call'],
207
+ environmentVariables: ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'OPENCLAW_SESSION', 'OPENCLAW_RUN_ID'],
208
+ cliPatterns: {
209
+ command: 'openclaw',
210
+ versionArgs: '--version',
211
+ helpArgs: '--help',
212
+ },
213
+ outputFormat: 'jsonl',
214
+ outputFormatTraits: ['jsonl', 'session-envelopes', 'plugin-events', 'tool-events'],
215
+ probeNotes: ['OpenClaw probes are contract-complete but typically local-manual because the binary is not expected in CI.'],
216
+ scenarios: [
217
+ { name: 'error', args: ['--definitely-invalid-flag'] },
218
+ ],
219
+ }),
220
+ 'hermes': createSubprocessProbe('hermes', 'hermes', {
221
+ versionArgs: ['--version'],
222
+ helpArgs: ['--help'],
223
+ availability: 'local-manual',
224
+ timeoutMs: 30000,
225
+ stdinSignals: ['interactive', 'approval'],
226
+ fileOperationPatterns: ['apply_patch', 'tool_call'],
227
+ environmentVariables: ['OPENROUTER_API_KEY', 'ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'NOUS_API_KEY', 'GOOGLE_API_KEY', 'GITHUB_TOKEN'],
228
+ cliPatterns: {
229
+ command: 'hermes',
230
+ versionArgs: '--version',
231
+ helpArgs: '--help',
232
+ },
233
+ outputFormat: 'jsonl',
234
+ outputFormatTraits: ['jsonl', 'session-envelopes', 'tool-events'],
235
+ probeNotes: ['Hermes probes are contract-complete but typically local-manual because the binary is not expected in CI.'],
236
+ scenarios: [
237
+ { name: 'error', args: ['--definitely-invalid-flag'] },
238
+ ],
239
+ }),
240
+ 'amp': createSubprocessProbe('amp', 'amp', {
241
+ versionArgs: ['--version'],
242
+ helpArgs: ['--help'],
243
+ availability: 'local-manual',
244
+ timeoutMs: 30000,
245
+ stdinSignals: ['amp auth', 'approval'],
246
+ fileOperationPatterns: ['grep_codebase', 'tool_result'],
247
+ environmentVariables: ['SOURCEGRAPH_ACCESS_TOKEN'],
248
+ cliPatterns: {
249
+ command: 'amp',
250
+ versionArgs: '--version',
251
+ helpArgs: '--help',
252
+ },
253
+ outputFormat: 'jsonl',
254
+ outputFormatTraits: ['jsonl', 'session-envelopes', 'cost-events', 'tool-events'],
255
+ probeNotes: ['Amp probes are contract-complete but typically local-manual because the binary is not expected in CI.'],
256
+ scenarios: [
257
+ { name: 'error', args: ['--definitely-invalid-flag'] },
258
+ ],
259
+ }),
260
+ 'droid': createSubprocessProbe('droid', 'droid', {
261
+ versionArgs: ['--version'],
262
+ helpArgs: ['--help'],
263
+ availability: 'local-manual',
264
+ timeoutMs: 30000,
265
+ stdinSignals: ['droid auth login', 'approval'],
266
+ fileOperationPatterns: ['bash', 'tool_call_ready', 'message_stop'],
267
+ environmentVariables: ['DROID_API_KEY', 'DROID_CONFIG_PATH'],
268
+ cliPatterns: {
269
+ command: 'droid',
270
+ versionArgs: '--version',
271
+ helpArgs: '--help',
272
+ },
273
+ outputFormat: 'jsonl',
274
+ outputFormatTraits: ['jsonl', 'session-envelopes', 'tool-events', 'cost-events'],
275
+ probeNotes: ['Droid probes are contract-complete but typically local-manual because the binary is not expected in CI.'],
276
+ scenarios: [
277
+ { name: 'error', args: ['--definitely-invalid-flag'] },
278
+ ],
279
+ }),
280
+ 'qwen': createSubprocessProbe('qwen', 'qwen', {
281
+ versionArgs: ['--version'],
282
+ helpArgs: ['--help'],
283
+ availability: 'local-manual',
284
+ timeoutMs: 30000,
285
+ stdinSignals: ['OAuth', 'interactive', 'approval'],
286
+ fileOperationPatterns: ['read_file', 'tool_call'],
287
+ environmentVariables: ['OPENAI_API_KEY', 'OPENAI_BASE_URL', 'OPENAI_MODEL', 'QWEN_CODE', 'QWEN_SESSION_ID'],
288
+ cliPatterns: {
289
+ command: 'qwen',
290
+ versionArgs: '--version',
291
+ helpArgs: '--help',
292
+ },
293
+ outputFormat: 'jsonl',
294
+ outputFormatTraits: ['jsonl', 'tool-events', 'error-events'],
295
+ probeNotes: ['Qwen probes are contract-complete but typically local-manual because the binary is not expected in CI.'],
296
+ scenarios: [
297
+ { name: 'error', args: ['--definitely-invalid-flag'] },
298
+ ],
299
+ }),
300
+ 'claude-agent-sdk': createOfflineContractProbe('claude-agent-sdk', 'sdk', {
301
+ timeoutMs: 30000,
302
+ stdinSignals: ['ANTHROPIC_API_KEY', 'CLAUDE_AGENT_API_KEY', 'claude'],
303
+ fileOperationPatterns: ['tool-use', 'tool-result', 'session files'],
304
+ environmentVariables: ['ANTHROPIC_API_KEY', 'CLAUDE_AGENT_API_KEY'],
305
+ cliPatterns: {
306
+ command: 'node',
307
+ module: '@anthropic-ai/claude-agent-sdk',
308
+ installCommand: 'npm install -g @anthropic-ai/claude-agent-sdk',
309
+ verifyCommand: 'node -e "import(\'@anthropic-ai/claude-agent-sdk\').then(() => console.log(\'OK\'))"',
310
+ loginCommand: 'claude',
311
+ },
312
+ outputFormat: 'sdk-events',
313
+ outputFormatTraits: ['sdk', 'json-events', 'session-envelopes', 'tool-events'],
314
+ probeNotes: ['SDK targets are fixture-backed contract probes in CI because there is no standalone harness binary to execute.', 'Review the checked-in baseline fixture to detect drift in installation, auth, and event-shape expectations.'],
315
+ contractVersion: 'manual-capture',
316
+ contractStartupTimeMs: 500,
317
+ contractExitCodes: { load: 0, 'auth-missing': 1 },
318
+ }),
319
+ 'codex-sdk': createOfflineContractProbe('codex-sdk', 'sdk', {
320
+ timeoutMs: 30000,
321
+ stdinSignals: ['OPENAI_API_KEY'],
322
+ fileOperationPatterns: ['execute_code', 'read_file', 'write_file'],
323
+ environmentVariables: ['OPENAI_API_KEY'],
324
+ cliPatterns: {
325
+ command: 'node',
326
+ module: 'openai',
327
+ installCommand: 'npm install -g openai',
328
+ verifyCommand: 'node -e "console.log(process.env.OPENAI_API_KEY ? \'OK\' : \'Missing\')"',
329
+ configPath: '~/.codex/config.json',
330
+ },
331
+ outputFormat: 'sdk-events',
332
+ outputFormatTraits: ['sdk', 'json-events', 'session-envelopes', 'function-call-events'],
333
+ probeNotes: ['SDK targets are fixture-backed contract probes in CI because there is no standalone harness binary to execute.', 'Codex SDK drift review focuses on auth expectations, tool/event shape, and installation contract changes.'],
334
+ contractVersion: 'manual-capture',
335
+ contractStartupTimeMs: 500,
336
+ contractExitCodes: { load: 0, 'auth-missing': 1 },
337
+ }),
338
+ 'pi-sdk': createOfflineContractProbe('pi-sdk', 'sdk', {
339
+ timeoutMs: 30000,
340
+ stdinSignals: ['PI_API_KEY', 'ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'OAuth'],
341
+ fileOperationPatterns: ['tool-call', 'parallel tool calls', 'session files'],
342
+ environmentVariables: ['PI_API_KEY', 'ANTHROPIC_API_KEY', 'OPENAI_API_KEY'],
343
+ cliPatterns: {
344
+ command: 'node',
345
+ module: '@pi-ai/sdk',
346
+ installCommand: 'npm install -g @pi-ai/sdk',
347
+ verifyCommand: 'pi --version',
348
+ configPath: '~/.pi/agent/settings.json',
349
+ },
350
+ outputFormat: 'sdk-events',
351
+ outputFormatTraits: ['sdk', 'json-events', 'session-envelopes', 'tool-events'],
352
+ probeNotes: ['SDK targets are fixture-backed contract probes in CI because there is no standalone harness binary to execute.', 'Pi SDK drift review focuses on provider auth expectations and structured event streaming.'],
353
+ contractVersion: 'manual-capture',
354
+ contractStartupTimeMs: 500,
355
+ contractExitCodes: { load: 0, 'auth-missing': 1 },
356
+ }),
357
+ 'codex-websocket': {
358
+ harness: 'codex-websocket',
359
+ executionType: 'websocket',
360
+ command: 'codex',
361
+ versionArgs: ['--version'],
362
+ helpArgs: ['app-server', '--help'],
363
+ availability: 'local-manual',
364
+ timeoutMs: 30000,
365
+ stdinSignals: ['OPENAI_API_KEY', 'CODEX_APP_SERVER'],
366
+ fileOperationPatterns: ['persistent session files', 'websocket event stream'],
367
+ environmentVariables: ['CODEX_APP_SERVER', 'OPENAI_API_KEY', 'CODEX_CLI'],
368
+ cliPatterns: {
369
+ command: 'codex',
370
+ versionArgs: '--version',
371
+ helpArgs: 'app-server --help',
372
+ transportArgs: 'app-server --listen ws://127.0.0.1:<port>',
373
+ },
374
+ outputFormat: 'websocket-json',
375
+ outputFormatTraits: ['websocket', 'json-frames', 'session-envelopes', 'tool-events'],
376
+ probeNotes: ['Live transport startup is local-manual; CI should review the checked-in contract fixture instead.'],
377
+ scenarios: [
378
+ { name: 'error', args: ['app-server', '--definitely-invalid-flag'] },
379
+ ],
380
+ },
381
+ 'opencode-http': {
382
+ harness: 'opencode-http',
383
+ executionType: 'http',
384
+ command: 'opencode',
385
+ versionArgs: ['--version'],
386
+ helpArgs: ['serve', '--help'],
387
+ availability: 'local-manual',
388
+ timeoutMs: 30000,
389
+ stdinSignals: ['OPENCODE_CONFIG', 'auth'],
390
+ fileOperationPatterns: ['HTTP / SSE event stream', 'session persistence'],
391
+ environmentVariables: ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GOOGLE_API_KEY', 'OPENCODE_SESSION_ID', 'OPENCODE_CONFIG'],
392
+ cliPatterns: {
393
+ command: 'opencode',
394
+ versionArgs: '--version',
395
+ helpArgs: 'serve --help',
396
+ transportArgs: 'serve --port <port> --host <host>',
397
+ },
398
+ outputFormat: 'http-sse',
399
+ outputFormatTraits: ['http', 'sse', 'json-events', 'tool-events'],
400
+ probeNotes: ['Live server startup is local-manual; CI should review the checked-in contract fixture instead.'],
401
+ scenarios: [
402
+ { name: 'error', args: ['serve', '--definitely-invalid-flag'] },
403
+ ],
404
+ },
405
+ };
406
+ // ---------------------------------------------------------------------------
407
+ // Probing logic
408
+ // ---------------------------------------------------------------------------
409
+ /**
410
+ * Probe a real harness installation and capture its behavior profile.
411
+ */
412
+ export async function probeHarness(config) {
413
+ if (config.availability === 'offline-only') {
414
+ return {
415
+ success: true,
416
+ profile: buildProfile(config, {}, undefined),
417
+ stdout: '',
418
+ stderr: '',
419
+ exitCode: 0,
420
+ durationMs: config.contractStartupTimeMs ?? 0,
421
+ scenarioResults: {},
422
+ };
423
+ }
424
+ const observations = {};
425
+ const versionObservation = config.versionArgs == null
426
+ ? undefined
427
+ : await runObservation(config, {
428
+ name: 'version',
429
+ args: config.versionArgs,
430
+ });
431
+ if (versionObservation) {
432
+ observations.version = versionObservation;
433
+ if (!versionObservation.success && !versionObservation.stdout && !versionObservation.stderr) {
434
+ return {
435
+ success: false,
436
+ error: versionObservation.error ?? 'Probe command failed before producing output.',
437
+ durationMs: versionObservation.durationMs,
438
+ exitCode: versionObservation.exitCode,
439
+ scenarioResults: observations,
440
+ };
441
+ }
442
+ }
443
+ if (config.helpArgs != null) {
444
+ observations.help = await runObservation(config, {
445
+ name: 'help',
446
+ args: config.helpArgs,
447
+ });
448
+ }
449
+ const scenarios = config.scenarios ?? (config.args == null ? [] : [{ name: 'success', args: config.args }]);
450
+ for (const scenario of scenarios) {
451
+ observations[scenario.name] = await runObservation(config, scenario);
452
+ }
453
+ const primary = selectPrimaryObservation(observations);
454
+ const profile = buildProfile(config, observations, primary);
455
+ return {
456
+ success: true,
457
+ profile,
458
+ stdout: primary?.stdout ?? '',
459
+ stderr: primary?.stderr ?? '',
460
+ exitCode: primary?.exitCode ?? versionObservation?.exitCode ?? 0,
461
+ durationMs: primary?.durationMs ?? versionObservation?.durationMs ?? 0,
462
+ scenarioResults: observations,
463
+ };
464
+ }
465
+ /**
466
+ * Probe all configured harnesses and save profiles to a directory.
467
+ */
468
+ export async function probeAllHarnesses(outputDir, configs) {
469
+ const results = new Map();
470
+ const allConfigs = configs ?? PROBE_CONFIGS;
471
+ fs.mkdirSync(outputDir, { recursive: true });
472
+ for (const [name, config] of Object.entries(allConfigs)) {
473
+ const result = await probeHarness(config);
474
+ results.set(name, result);
475
+ if (result.profile) {
476
+ const profilePath = path.join(outputDir, `${name}.profile.json`);
477
+ fs.writeFileSync(profilePath, JSON.stringify(result.profile, null, 2), 'utf-8');
478
+ }
479
+ const resultPath = path.join(outputDir, `${name}.result.json`);
480
+ fs.writeFileSync(resultPath, JSON.stringify({
481
+ success: result.success,
482
+ error: result.error,
483
+ exitCode: result.exitCode,
484
+ durationMs: result.durationMs,
485
+ stdoutLength: result.stdout?.length ?? 0,
486
+ stderrLength: result.stderr?.length ?? 0,
487
+ scenarioResults: result.scenarioResults ?? {},
488
+ }, null, 2), 'utf-8');
489
+ }
490
+ return results;
491
+ }
492
+ /**
493
+ * Compare a behavior profile against a previous baseline.
494
+ * Returns a list of differences.
495
+ */
496
+ export function compareProfiles(baseline, current) {
497
+ const diffs = [];
498
+ if (baseline.version !== current.version) {
499
+ diffs.push({ field: 'version', baseline: baseline.version, current: current.version, severity: 'info' });
500
+ }
501
+ if (baseline.outputFormat !== current.outputFormat) {
502
+ diffs.push({ field: 'outputFormat', baseline: baseline.outputFormat, current: current.outputFormat, severity: 'breaking' });
503
+ }
504
+ if (!sameStringArray(baseline.outputFormatTraits, current.outputFormatTraits)) {
505
+ diffs.push({ field: 'outputFormatTraits', baseline: baseline.outputFormatTraits, current: current.outputFormatTraits, severity: 'warning' });
506
+ }
507
+ if (baseline.supportsStdin !== current.supportsStdin) {
508
+ diffs.push({ field: 'supportsStdin', baseline: baseline.supportsStdin, current: current.supportsStdin, severity: 'breaking' });
509
+ }
510
+ if (!sameStringArray(baseline.stdinSignals, current.stdinSignals)) {
511
+ diffs.push({ field: 'stdinSignals', baseline: baseline.stdinSignals, current: current.stdinSignals, severity: 'warning' });
512
+ }
513
+ if (!sameStringArray(baseline.fileOperationPatterns, current.fileOperationPatterns)) {
514
+ diffs.push({ field: 'fileOperationPatterns', baseline: baseline.fileOperationPatterns, current: current.fileOperationPatterns, severity: 'warning' });
515
+ }
516
+ if (!sameStringArray(baseline.environmentVariables, current.environmentVariables)) {
517
+ diffs.push({ field: 'environmentVariables', baseline: baseline.environmentVariables, current: current.environmentVariables, severity: 'warning' });
518
+ }
519
+ if (baseline.executionType !== current.executionType) {
520
+ diffs.push({ field: 'executionType', baseline: baseline.executionType, current: current.executionType, severity: 'breaking' });
521
+ }
522
+ if (baseline.availability !== current.availability) {
523
+ diffs.push({ field: 'availability', baseline: baseline.availability, current: current.availability, severity: 'info' });
524
+ }
525
+ // Check exit code changes
526
+ for (const [scenario, code] of Object.entries(baseline.exitCodes)) {
527
+ if (current.exitCodes[scenario] !== undefined && current.exitCodes[scenario] !== code) {
528
+ diffs.push({ field: `exitCodes.${scenario}`, baseline: code, current: current.exitCodes[scenario], severity: 'warning' });
529
+ }
530
+ }
531
+ // Check CLI pattern changes
532
+ for (const [key, pattern] of Object.entries(baseline.cliPatterns)) {
533
+ if (current.cliPatterns[key] !== undefined && current.cliPatterns[key] !== pattern) {
534
+ diffs.push({ field: `cliPatterns.${key}`, baseline: pattern, current: current.cliPatterns[key], severity: 'breaking' });
535
+ }
536
+ }
537
+ // Check startup time drift (>2x is a warning)
538
+ if (current.startupTimeMs > baseline.startupTimeMs * 2) {
539
+ diffs.push({ field: 'startupTimeMs', baseline: baseline.startupTimeMs, current: current.startupTimeMs, severity: 'warning' });
540
+ }
541
+ return diffs;
542
+ }
543
+ // ---------------------------------------------------------------------------
544
+ // Internal helpers
545
+ // ---------------------------------------------------------------------------
546
+ function buildProfile(config, observations, primary) {
547
+ const outputFormat = config.outputFormat ?? inferOutputFormat(primary);
548
+ const inferredSignals = inferStdinSignals(primary);
549
+ const stdinSignals = uniqueStrings([...(config.stdinSignals ?? []), ...inferredSignals]);
550
+ const supportsStdin = stdinSignals.length > 0 || Object.values(observations).some((entry) => entry.stdout.includes('stdin') || entry.stderr.includes('stdin'));
551
+ const outputFormatTraits = uniqueStrings([
552
+ ...(config.outputFormatTraits ?? []),
553
+ ...inferOutputTraits(primary, outputFormat),
554
+ ]);
555
+ return {
556
+ harness: config.harness,
557
+ executionType: config.executionType ?? 'subprocess',
558
+ version: extractVersion(observations.version) ?? config.contractVersion ?? 'unknown',
559
+ capturedAt: new Date().toISOString(),
560
+ startupTimeMs: Math.min(primary?.durationMs ?? observations.version?.durationMs ?? config.contractStartupTimeMs ?? 0, 5000),
561
+ outputFormat,
562
+ outputFormatTraits,
563
+ supportsStdin,
564
+ stdinSignals,
565
+ fileOperationPatterns: [...(config.fileOperationPatterns ?? [])],
566
+ exitCodes: {
567
+ ...(config.contractExitCodes ?? {}),
568
+ ...Object.fromEntries(Object.entries(observations).map(([name, entry]) => [name, entry.exitCode])),
569
+ },
570
+ environmentVariables: [...(config.environmentVariables ?? [])],
571
+ cliPatterns: {
572
+ command: config.command,
573
+ args: (config.args ?? []).join(' '),
574
+ versionArgs: (config.versionArgs ?? []).join(' '),
575
+ helpArgs: (config.helpArgs ?? []).join(' '),
576
+ ...config.cliPatterns,
577
+ },
578
+ availability: config.availability ?? 'local-manual',
579
+ probeNotes: [...(config.probeNotes ?? [])],
580
+ };
581
+ }
582
+ async function runObservation(config, scenario) {
583
+ const startTime = Date.now();
584
+ const cwd = scenario.cwd ?? config.cwd ?? os.tmpdir();
585
+ const timeout = scenario.timeoutMs ?? config.timeoutMs ?? 30000;
586
+ return new Promise((resolve) => {
587
+ const child = execFile(config.command, scenario.args, {
588
+ cwd,
589
+ timeout,
590
+ maxBuffer: 10 * 1024 * 1024,
591
+ env: { ...process.env, ...config.env, ...scenario.env },
592
+ }, (error, stdout, stderr) => {
593
+ const durationMs = Date.now() - startTime;
594
+ const exitCode = error?.code !== undefined
595
+ ? (typeof error.code === 'number' ? error.code : 1)
596
+ : 0;
597
+ if (error && !stdout && !stderr) {
598
+ resolve({
599
+ success: false,
600
+ error: error.message,
601
+ stdout,
602
+ stderr,
603
+ exitCode: typeof exitCode === 'number' ? exitCode : 1,
604
+ durationMs,
605
+ });
606
+ return;
607
+ }
608
+ resolve({
609
+ success: true,
610
+ stdout,
611
+ stderr,
612
+ exitCode: typeof exitCode === 'number' ? exitCode : 0,
613
+ durationMs,
614
+ error: error?.message,
615
+ });
616
+ });
617
+ if (scenario.stdin != null) {
618
+ child.stdin?.end(scenario.stdin);
619
+ }
620
+ setTimeout(() => {
621
+ if (!child.killed) {
622
+ child.kill('SIGKILL');
623
+ }
624
+ }, timeout + 5000);
625
+ });
626
+ }
627
+ function selectPrimaryObservation(observations) {
628
+ const orderedNames = ['success', 'stdin-prompt', 'help', 'version'];
629
+ for (const name of orderedNames) {
630
+ if (observations[name] != null) {
631
+ return observations[name];
632
+ }
633
+ }
634
+ return Object.values(observations)[0];
635
+ }
636
+ function inferOutputFormat(observation) {
637
+ if (observation == null) {
638
+ return 'text';
639
+ }
640
+ const stdout = observation.stdout.trim();
641
+ if (!stdout) {
642
+ return 'text';
643
+ }
644
+ const lines = stdout.split('\n').map((line) => line.trim()).filter(Boolean);
645
+ if (lines.length > 0 && lines.every((line) => isJson(line))) {
646
+ return 'jsonl';
647
+ }
648
+ if (isJson(stdout)) {
649
+ return 'json';
650
+ }
651
+ return 'text';
652
+ }
653
+ function inferOutputTraits(observation, outputFormat) {
654
+ if (observation == null) {
655
+ return [];
656
+ }
657
+ const traits = [];
658
+ const combined = `${observation.stdout}\n${observation.stderr}`;
659
+ const lines = observation.stdout.split('\n').map((line) => line.trim()).filter(Boolean);
660
+ if (outputFormat === 'jsonl') {
661
+ traits.push('jsonl', 'newline-delimited');
662
+ }
663
+ else if (outputFormat === 'json') {
664
+ traits.push('json');
665
+ }
666
+ else if (observation.stdout.trim()) {
667
+ traits.push('plain-text');
668
+ }
669
+ if (lines.some((line) => /session[_-](start|end)|thread|turn/i.test(line))) {
670
+ traits.push('session-envelopes');
671
+ }
672
+ if (lines.some((line) => /tool|function_call|apply_patch/i.test(line))) {
673
+ traits.push('tool-events');
674
+ }
675
+ if (combined && /error|failed|denied|invalid/i.test(combined)) {
676
+ traits.push('error-events');
677
+ }
678
+ if (observation.stderr.trim()) {
679
+ traits.push('stderr-output');
680
+ }
681
+ return uniqueStrings(traits);
682
+ }
683
+ function inferStdinSignals(observation) {
684
+ if (observation == null) {
685
+ return [];
686
+ }
687
+ const signals = [];
688
+ const lines = `${observation.stdout}\n${observation.stderr}`.split('\n');
689
+ for (const rawLine of lines) {
690
+ const line = rawLine.trim();
691
+ if (!line)
692
+ continue;
693
+ if (/[?][ \t]*$/.test(line) || /\(y\/n\)|\(y\/N\)|stdin|interactive|approval|prompt/i.test(line)) {
694
+ signals.push(line);
695
+ }
696
+ }
697
+ return uniqueStrings(signals);
698
+ }
699
+ function extractVersion(observation) {
700
+ if (observation == null) {
701
+ return null;
702
+ }
703
+ const combined = `${observation.stdout}\n${observation.stderr}`;
704
+ const match = combined.match(/\bv?\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?\b/);
705
+ return match?.[0] ?? null;
706
+ }
707
+ function isJson(text) {
708
+ try {
709
+ JSON.parse(text);
710
+ return true;
711
+ }
712
+ catch {
713
+ return false;
714
+ }
715
+ }
716
+ function sameStringArray(left, right) {
717
+ return JSON.stringify([...left].sort()) === JSON.stringify([...right].sort());
718
+ }
719
+ function uniqueStrings(values) {
720
+ return [...new Set(values.filter((value) => value.length > 0))];
721
+ }
722
+ //# sourceMappingURL=probe.js.map