pi-reason-harness 1.0.1

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/harness/cli.ts ADDED
@@ -0,0 +1,472 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * pi-reason-harness — CLI for iterative reasoning with verification.
4
+ *
5
+ * Usage:
6
+ * pi-reason-harness init --name "..." --type code-reasoning --models '["openai/gpt-4o"]'
7
+ * pi-reason-harness solve --problem "..." [--train-inputs '[[...]]'] [--train-outputs '[[...]]'] [--test-inputs '[[...]]']
8
+ * pi-reason-harness status
9
+ * pi-reason-harness results [--last 10]
10
+ * pi-reason-harness learn
11
+ * pi-reason-harness reset-learn
12
+ * pi-reason-harness clear
13
+ *
14
+ * Server management:
15
+ * pi-reason-harness --status
16
+ * pi-reason-harness --start
17
+ * pi-reason-harness --stop
18
+ * pi-reason-harness --restart
19
+ * pi-reason-harness --logs
20
+ */
21
+
22
+ import { spawn as spawnChild } from 'node:child_process';
23
+ import * as fs from 'node:fs';
24
+ import * as path from 'node:path';
25
+ import * as http from 'node:http';
26
+ import { fileURLToPath } from 'node:url';
27
+
28
+ const __filename = fileURLToPath(import.meta.url);
29
+ const __dirname = path.dirname(__filename);
30
+
31
+ const PORT = Number(process.env.PI_REASON_HARNESS_PORT ?? 9880);
32
+ const HOST = '127.0.0.1';
33
+ const BASE_URL = `http://${HOST}:${PORT}`;
34
+ const LOG = process.env.PI_REASON_HARNESS_LOG ?? '/tmp/pi-reason-harness.log';
35
+
36
+ function httpGet(url: string): Promise<{ status: number; body: string }> {
37
+ return new Promise((resolve) => {
38
+ const req = http.get(url, { timeout: 2000 }, (res) => {
39
+ const chunks: Buffer[] = [];
40
+ res.on('data', (c: Buffer) => chunks.push(c));
41
+ res.on('end', () => {
42
+ resolve({ status: res.statusCode ?? 0, body: Buffer.concat(chunks).toString('utf-8') });
43
+ });
44
+ });
45
+ req.on('error', (err) => resolve({ status: 0, body: err.message }));
46
+ req.on('timeout', () => { req.destroy(); resolve({ status: 0, body: 'timeout' }); });
47
+ });
48
+ }
49
+
50
+ function httpPost(
51
+ url: string,
52
+ body: string,
53
+ extraHeaders?: Record<string, string>
54
+ ): Promise<{ status: number; body: string }> {
55
+ return new Promise((resolve) => {
56
+ const data = Buffer.from(body, 'utf-8');
57
+ const req = http.request(
58
+ url,
59
+ {
60
+ method: 'POST',
61
+ headers: { 'content-type': 'application/json; charset=utf-8', 'content-length': data.length, ...extraHeaders },
62
+ timeout: 60 * 60 * 1000,
63
+ },
64
+ (res) => {
65
+ const chunks: Buffer[] = [];
66
+ res.on('data', (c: Buffer) => chunks.push(c));
67
+ res.on('end', () => resolve({ status: res.statusCode ?? 0, body: Buffer.concat(chunks).toString('utf-8') }));
68
+ }
69
+ );
70
+ req.on('error', (err) => resolve({ status: 0, body: err.message }));
71
+ req.on('timeout', () => { req.destroy(); resolve({ status: 0, body: 'timeout' }); });
72
+ req.write(data);
73
+ req.end();
74
+ });
75
+ }
76
+
77
+ function readSessionIdFromFile(): string | undefined {
78
+ try {
79
+ const cwd = process.cwd();
80
+ const sessionFilePath = path.join(cwd, '.pi', 'reason-harness', 'session-id');
81
+ if (fs.existsSync(sessionFilePath)) {
82
+ const id = fs.readFileSync(sessionFilePath, 'utf-8').trim();
83
+ if (id) return id;
84
+ }
85
+ } catch {}
86
+ return undefined;
87
+ }
88
+
89
+ function agentHeaders(): Record<string, string> {
90
+ const headers: Record<string, string> = {};
91
+ const sessionId = readSessionIdFromFile();
92
+ if (sessionId) headers['x-session-id'] = sessionId;
93
+ return headers;
94
+ }
95
+
96
+ async function isUp(): Promise<boolean> {
97
+ const { status } = await httpGet(`${BASE_URL}/health`);
98
+ return status === 200;
99
+ }
100
+
101
+ async function startServer(): Promise<boolean> {
102
+ if (await isUp()) return true;
103
+
104
+ let serverScript = path.resolve(__dirname, 'server.js');
105
+ if (!fs.existsSync(serverScript)) {
106
+ const tsPath = path.resolve(__dirname, 'server.ts');
107
+ if (fs.existsSync(tsPath)) serverScript = tsPath;
108
+ }
109
+
110
+ const useTsx = serverScript.endsWith('.ts');
111
+ const cmd = useTsx ? 'npx' : 'node';
112
+ const args = useTsx ? ['tsx', serverScript] : [serverScript];
113
+
114
+ const child = spawnChild(cmd, args, {
115
+ cwd: process.cwd(),
116
+ stdio: ['ignore', 'ignore', 'ignore'],
117
+ detached: true,
118
+ env: { ...process.env, PI_REASON_HARNESS_PORT: String(PORT), PI_REASON_HARNESS_LOG: LOG },
119
+ });
120
+ child.unref();
121
+
122
+ for (let i = 0; i < 150; i++) {
123
+ await new Promise((r) => setTimeout(r, 100));
124
+ if (await isUp()) return true;
125
+ }
126
+
127
+ process.stderr.write(`pi-reason-harness: server failed to start on ${BASE_URL} (see ${LOG})\n`);
128
+ return false;
129
+ }
130
+
131
+ async function postAction(jsonBody: string): Promise<void> {
132
+ const { status, body } = await httpPost(`${BASE_URL}/action`, jsonBody, agentHeaders());
133
+ if (status === 200) {
134
+ try {
135
+ const parsed = JSON.parse(body);
136
+ if (parsed.ok && parsed.result?.text) {
137
+ process.stdout.write(parsed.result.text + '\n');
138
+ } else if (!parsed.ok) {
139
+ process.stderr.write(`Error: ${parsed.error}\n`);
140
+ process.exit(1);
141
+ }
142
+ } catch {
143
+ if (body.trim()) process.stdout.write(body + '\n');
144
+ }
145
+ } else if (status === 0) {
146
+ process.stderr.write(`Error: cannot reach harness server at ${BASE_URL}\n`);
147
+ process.exit(1);
148
+ } else {
149
+ try {
150
+ const parsed = JSON.parse(body);
151
+ process.stderr.write(`Error: ${parsed.error ?? body}\n`);
152
+ } catch {
153
+ process.stderr.write(`Error: HTTP ${status} — ${body}\n`);
154
+ }
155
+ process.exit(1);
156
+ }
157
+ }
158
+
159
+ function extractFlag(args: string[], name: string): string | undefined {
160
+ const idx = args.findIndex((a) => a === `--${name}`);
161
+ if (idx !== -1 && idx + 1 < args.length) {
162
+ const val = args[idx + 1];
163
+ args.splice(idx, 2);
164
+ return val;
165
+ }
166
+ return undefined;
167
+ }
168
+
169
+ async function main(): Promise<void> {
170
+ const rawArgs = process.argv.slice(2);
171
+
172
+ if (rawArgs.length === 0) {
173
+ process.stderr.write(`pi-reason-harness — iterative reasoning with verification CLI
174
+
175
+ Usage:
176
+ pi-reason-harness init --name "..." --type code-reasoning|knowledge-extraction|hybrid [--models '["openai/gpt-4o"]'] [--num-experts 1] [--verification sandbox|self-audit|external|none] [--verify-command "..."] [--max-cost 1.0] [--max-time 300]
177
+ pi-reason-harness solve --problem "..." [--train-inputs '[[...]]'] [--train-outputs '[[...]]'] [--test-inputs '[[...]]']
178
+ pi-reason-harness status
179
+ pi-reason-harness results [--last 10]
180
+ pi-reason-harness learn
181
+ pi-reason-harness reset-learn
182
+ pi-reason-harness clear
183
+
184
+ Server management:
185
+ pi-reason-harness --status
186
+ pi-reason-harness --start
187
+ pi-reason-harness --stop
188
+ pi-reason-harness --restart
189
+ pi-reason-harness --logs
190
+
191
+ Environment:
192
+ PI_REASON_HARNESS_PORT Server port (default: 9880)
193
+ PI_REASON_HARNESS_LOG Log file (default: /tmp/pi-reason-harness.log)
194
+ `);
195
+ return;
196
+ }
197
+
198
+ const first = rawArgs[0];
199
+
200
+ // Server management
201
+ if (first === '--status') {
202
+ const { status, body } = await httpGet(`${BASE_URL}/health`);
203
+ process.stdout.write(status === 200 ? body + '\n' : '{"ok":false,"error":"down"}\n');
204
+ process.exit(status === 200 ? 0 : 1);
205
+ }
206
+ if (first === '--start') {
207
+ await startServer();
208
+ const { body } = await httpGet(`${BASE_URL}/health`);
209
+ process.stdout.write(body + '\n');
210
+ return;
211
+ }
212
+ if (first === '--stop') {
213
+ if (await isUp()) {
214
+ await httpPost(`${BASE_URL}/quit`, '');
215
+ process.stdout.write('{"ok":true,"stopped":true}\n');
216
+ } else {
217
+ process.stdout.write('{"ok":true,"stopped":false,"note":"already down"}\n');
218
+ }
219
+ return;
220
+ }
221
+ if (first === '--restart') {
222
+ if (await isUp()) {
223
+ await httpPost(`${BASE_URL}/quit`, '');
224
+ await new Promise((r) => setTimeout(r, 200));
225
+ }
226
+ await startServer();
227
+ const { body } = await httpGet(`${BASE_URL}/health`);
228
+ process.stdout.write(body + '\n');
229
+ return;
230
+ }
231
+ if (first === '--logs') {
232
+ const { spawn } = await import('node:child_process');
233
+ spawn('tail', ['-f', LOG], { stdio: 'inherit' });
234
+ return;
235
+ }
236
+
237
+ // JSON passthrough
238
+ if (first.startsWith('{')) {
239
+ if (!(await startServer())) process.exit(1);
240
+ await postAction(first);
241
+ return;
242
+ }
243
+
244
+ // Action subcommands
245
+ if (!(await startServer())) process.exit(1);
246
+
247
+ const args = [...rawArgs];
248
+ const action = args.shift()!;
249
+
250
+ switch (action) {
251
+ case 'init': {
252
+ const name = extractFlag(args, 'name');
253
+ const type = extractFlag(args, 'type');
254
+ const modelsRaw = extractFlag(args, 'models');
255
+ const numExperts = extractFlag(args, 'num-experts');
256
+ const verification = extractFlag(args, 'verification');
257
+ const verifyCommand = extractFlag(args, 'verify-command');
258
+ const maxCost = extractFlag(args, 'max-cost');
259
+ const maxTime = extractFlag(args, 'max-time');
260
+
261
+ if (!name) {
262
+ process.stderr.write('Error: init requires --name.\n');
263
+ process.exit(1);
264
+ }
265
+
266
+ let models: string[] = ['openai/gpt-4o'];
267
+ if (modelsRaw) {
268
+ try { models = JSON.parse(modelsRaw); } catch {
269
+ process.stderr.write('Error: --models must be a JSON array of strings.\n');
270
+ process.exit(1);
271
+ }
272
+ }
273
+
274
+ await postAction(
275
+ JSON.stringify({
276
+ action: 'init',
277
+ name,
278
+ type: type || 'code-reasoning',
279
+ models,
280
+ numExperts: numExperts ? Number(numExperts) : 1,
281
+ verification: verification || 'sandbox',
282
+ verifyCommand,
283
+ maxCostPerProblem: maxCost ? Number(maxCost) : undefined,
284
+ maxTimePerProblem: maxTime ? Number(maxTime) : undefined,
285
+ })
286
+ );
287
+ break;
288
+ }
289
+
290
+ case 'solve': {
291
+ const problem = extractFlag(args, 'problem') ?? '';
292
+ const trainInputsRaw = extractFlag(args, 'train-inputs');
293
+ const trainOutputsRaw = extractFlag(args, 'train-outputs');
294
+ const testInputsRaw = extractFlag(args, 'test-inputs');
295
+ const meta = args.includes('--meta') || args.includes('-m');
296
+
297
+ if (!problem && !trainInputsRaw) {
298
+ process.stderr.write('Error: solve requires --problem or --train-inputs/--train-outputs.\n');
299
+ process.exit(1);
300
+ }
301
+
302
+ let trainInputs: unknown[] = [];
303
+ let trainOutputs: unknown[] = [];
304
+ let testInputs: unknown[] = [];
305
+
306
+ if (trainInputsRaw) {
307
+ try { trainInputs = JSON.parse(trainInputsRaw); } catch {
308
+ process.stderr.write('Error: --train-inputs must be a JSON array.\n');
309
+ process.exit(1);
310
+ }
311
+ }
312
+ if (trainOutputsRaw) {
313
+ try { trainOutputs = JSON.parse(trainOutputsRaw); } catch {
314
+ process.stderr.write('Error: --train-outputs must be a JSON array.\n');
315
+ process.exit(1);
316
+ }
317
+ }
318
+ if (testInputsRaw) {
319
+ try { testInputs = JSON.parse(testInputsRaw); } catch {
320
+ process.stderr.write('Error: --test-inputs must be a JSON array.\n');
321
+ process.exit(1);
322
+ }
323
+ }
324
+
325
+ await postAction(
326
+ JSON.stringify({
327
+ action: 'solve',
328
+ problem,
329
+ trainInputs,
330
+ trainOutputs,
331
+ testInputs,
332
+ meta,
333
+ })
334
+ );
335
+ break;
336
+ }
337
+
338
+ case 'status': {
339
+ await postAction(JSON.stringify({ action: 'status' }));
340
+ break;
341
+ }
342
+
343
+ case 'results': {
344
+ const last = extractFlag(args, 'last');
345
+ await postAction(JSON.stringify({ action: 'results', last: last ? Number(last) : 10 }));
346
+ break;
347
+ }
348
+
349
+ case 'learn': {
350
+ await postAction(JSON.stringify({ action: 'learn' }));
351
+ break;
352
+ }
353
+
354
+ case 'reset-learn': {
355
+ await postAction(JSON.stringify({ action: 'reset-learn' }));
356
+ break;
357
+ }
358
+
359
+ case 'meta-analyze': {
360
+ const problem = extractFlag(args, 'problem');
361
+ if (!problem) {
362
+ process.stderr.write('meta-analyze requires --problem\n');
363
+ process.exit(1);
364
+ }
365
+ await postAction(JSON.stringify({ action: 'meta-analyze', problem }));
366
+ break;
367
+ }
368
+
369
+ case 'meta-improve': {
370
+ await postAction(JSON.stringify({ action: 'meta-improve' }));
371
+ break;
372
+ }
373
+
374
+ case 'strategies': {
375
+ await postAction(JSON.stringify({ action: 'strategies' }));
376
+ break;
377
+ }
378
+
379
+ case 'meta-rules': {
380
+ await postAction(JSON.stringify({ action: 'meta-rules' }));
381
+ break;
382
+ }
383
+
384
+ case 'model-routes': {
385
+ await postAction(JSON.stringify({ action: 'model-routes' }));
386
+ break;
387
+ }
388
+
389
+ case 'harness-specs': {
390
+ await postAction(JSON.stringify({ action: 'harness-specs' }));
391
+ break;
392
+ }
393
+
394
+ case 'evolve-harness': {
395
+ await postAction(JSON.stringify({ action: 'evolve-harness' }));
396
+ break;
397
+ }
398
+
399
+ case 'transfer': {
400
+ const sourceCategory = extractFlag(args, 'source-category');
401
+ const targetCategory = extractFlag(args, 'target-category');
402
+ if (!sourceCategory || !targetCategory) {
403
+ process.stderr.write('transfer requires --source-category and --target-category\n');
404
+ process.exit(1);
405
+ }
406
+ await postAction(JSON.stringify({ action: 'transfer', sourceCategory, targetCategory }));
407
+ break;
408
+ }
409
+
410
+ case 'decompose': {
411
+ const problem = extractFlag(args, 'problem');
412
+ if (!problem) {
413
+ process.stderr.write('decompose requires --problem\n');
414
+ process.exit(1);
415
+ }
416
+ await postAction(JSON.stringify({ action: 'decompose', problem }));
417
+ break;
418
+ }
419
+
420
+ case 'synth-prompts': {
421
+ await postAction(JSON.stringify({ action: 'synth-prompts' }));
422
+ break;
423
+ }
424
+
425
+ case 'meta-harnesses': {
426
+ await postAction(JSON.stringify({ action: 'meta-harnesses' }));
427
+ break;
428
+ }
429
+
430
+ case 'generate-meta-harness': {
431
+ await postAction(JSON.stringify({ action: 'generate-meta-harness' }));
432
+ break;
433
+ }
434
+
435
+ case 'arc-benchmark': {
436
+ const dataPath = extractFlag(args, 'data-path');
437
+ const maxChallenges = extractFlag(args, 'max-challenges');
438
+ await postAction(JSON.stringify({
439
+ action: 'arc-benchmark',
440
+ ...(dataPath ? { dataPath } : {}),
441
+ ...(maxChallenges ? { maxChallenges: parseInt(maxChallenges) } : {}),
442
+ }));
443
+ break;
444
+ }
445
+
446
+ case 'route-decompose': {
447
+ const problem = extractFlag(args, 'problem');
448
+ if (!problem) {
449
+ process.stderr.write('route-decompose requires --problem\n');
450
+ process.exit(1);
451
+ }
452
+ const doSolve = args.includes('--solve');
453
+ await postAction(JSON.stringify({ action: 'route-decompose', problem, solve: doSolve }));
454
+ break;
455
+ }
456
+
457
+ case 'clear': {
458
+ await postAction(JSON.stringify({ action: 'clear' }));
459
+ break;
460
+ }
461
+
462
+ default: {
463
+ process.stderr.write(`Unknown command: ${action}. Use --help for usage.\n`);
464
+ process.exit(1);
465
+ }
466
+ }
467
+ }
468
+
469
+ main().catch((err) => {
470
+ process.stderr.write(`pi-reason-harness: ${err instanceof Error ? err.message : err}\n`);
471
+ process.exit(1);
472
+ });