open-research-protocol 0.4.9 → 0.4.10

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 CHANGED
@@ -58,6 +58,7 @@ ORP should feel like one CLI with built-in abilities:
58
58
  - `collaborate` for repository collaboration setup and workflow execution
59
59
  - `erdos` for Erdos-specific data and workflow support
60
60
  - `report` and `packet` for ORP artifacts
61
+ - `compute` for targeted compute admission, local execution, and paid approval gating
61
62
 
62
63
  The `pack` layer still exists, but it is now an advanced/internal surface rather
63
64
  than the main product story.
@@ -145,6 +146,8 @@ orp pack install --pack-id erdos-open-problems --json
145
146
  orp pack fetch --source <git-url> --pack-id <pack-id> --install-target . --json
146
147
  orp gate run --profile default --json
147
148
  orp packet emit --profile default --json
149
+ orp compute decide --input orp.compute.json --json
150
+ orp compute run-local --input orp.compute.json --task orp.compute.task.json --json
148
151
  orp report summary --json
149
152
  ```
150
153
 
@@ -153,7 +156,8 @@ These surfaces are meant to help automated systems discover ORP quickly:
153
156
  - bare `orp` opens a home screen with repo/runtime status, available packs, and next commands
154
157
  - `orp home --json` returns the same landing context in machine-readable form
155
158
  - `orp auth ...`, `orp ideas ...`, `orp world ...`, `orp checkpoint ...`, `orp runner ...`, and `orp agent ...` expose the hosted workspace surface directly through ORP
156
- - `orp youtube inspect ...` exposes public YouTube metadata and transcript retrieval through a stable ORP artifact shape for agent use
159
+ - `orp compute ...` exposes targeted-compute admission, local execution, and paid-approval gating through a stable ORP wrapper surface
160
+ - `orp youtube inspect ...` exposes public YouTube metadata plus full transcript ingestion through a stable ORP artifact shape for agent use when caption tracks are available
157
161
  - `orp init`, `orp status`, `orp branch start`, `orp checkpoint create`, `orp backup`, `orp ready`, `orp doctor`, and `orp cleanup` expose the local-first repo governance surface directly through ORP
158
162
  - `orp discover ...` exposes profile-based GitHub scanning as a built-in ORP ability
159
163
  - `orp collaborate ...` exposes built-in collaboration setup and workflow execution without asking users to think in terms of separate governance packs
@@ -0,0 +1,341 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import process from "node:process";
6
+ import {
7
+ buildOrpComputeGateResult,
8
+ buildOrpComputePacket,
9
+ defineComputePacket,
10
+ defineDecision,
11
+ defineImpactRead,
12
+ definePolicy,
13
+ defineResultBundle,
14
+ defineRung,
15
+ evaluateDispatch,
16
+ runLocalShellPacket,
17
+ } from "breakthroughs";
18
+
19
+ function printHelp() {
20
+ console.log(`ORP compute
21
+
22
+ Usage:
23
+ orp compute decide --input <path> [--packet-out <path>] [--json]
24
+ orp compute run-local --input <path> --task <path> [--receipt-out <path>] [--packet-out <path>] [--json]
25
+
26
+ Input JSON shape:
27
+ {
28
+ "decision": { ... },
29
+ "rung": { ... },
30
+ "policy": { ... },
31
+ "packet": { ... },
32
+ "repo": {
33
+ "rootPath": "/abs/path",
34
+ "git": { "branch": "main", "commit": "abc123" }
35
+ },
36
+ "orp": {
37
+ "boardId": "targeted_compute",
38
+ "problemId": "adult-vs-developmental-rgc",
39
+ "artifactRoot": "orp/artifacts"
40
+ }
41
+ }
42
+
43
+ Task JSON shape for run-local:
44
+ {
45
+ "command": "node",
46
+ "args": ["-e", "console.log('hello')"],
47
+ "cwd": "/abs/path",
48
+ "timeoutMs": 30000,
49
+ "env": {}
50
+ }
51
+
52
+ Policy semantics:
53
+ - local admitted rungs can resolve to run_local
54
+ - paid admitted rungs resolve to request_paid_approval unless the rung is explicitly approved in policy.paid.approvedRungs
55
+ `);
56
+ }
57
+
58
+ function parseArgs(argv) {
59
+ const options = {
60
+ json: false,
61
+ };
62
+
63
+ for (let i = 0; i < argv.length; i += 1) {
64
+ const arg = argv[i];
65
+
66
+ if (arg === "--json") {
67
+ options.json = true;
68
+ continue;
69
+ }
70
+ if (arg === "-h" || arg === "--help") {
71
+ options.help = true;
72
+ continue;
73
+ }
74
+ if (arg.startsWith("--")) {
75
+ const key = arg.slice(2).replace(/-([a-z])/g, (_, ch) => ch.toUpperCase());
76
+ const value = argv[i + 1];
77
+ if (value == null || value.startsWith("--")) {
78
+ throw new Error(`missing value for ${arg}`);
79
+ }
80
+ options[key] = value;
81
+ i += 1;
82
+ continue;
83
+ }
84
+
85
+ if (!options.command) {
86
+ options.command = arg;
87
+ } else {
88
+ throw new Error(`unexpected argument: ${arg}`);
89
+ }
90
+ }
91
+
92
+ return options;
93
+ }
94
+
95
+ async function readJson(filePath) {
96
+ const raw = await fs.readFile(filePath, "utf8");
97
+ return JSON.parse(raw);
98
+ }
99
+
100
+ async function writeJson(filePath, payload) {
101
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
102
+ await fs.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
103
+ }
104
+
105
+ function buildContext(raw) {
106
+ if (!raw || typeof raw !== "object") {
107
+ throw new Error("input must be a JSON object");
108
+ }
109
+
110
+ return {
111
+ raw,
112
+ decision: defineDecision(raw.decision),
113
+ rung: defineRung(raw.rung),
114
+ policy: definePolicy(raw.policy),
115
+ packet: defineComputePacket(raw.packet),
116
+ };
117
+ }
118
+
119
+ function commandLabel(subcommand, options) {
120
+ const parts = ["orp", "compute", subcommand];
121
+ if (options.input) {
122
+ parts.push("--input", options.input);
123
+ }
124
+ if (options.task) {
125
+ parts.push("--task", options.task);
126
+ }
127
+ return parts.join(" ");
128
+ }
129
+
130
+ function gateStatusForDispatch(action) {
131
+ if (action === "hold_packet") {
132
+ return "fail";
133
+ }
134
+ if (action === "request_paid_approval") {
135
+ return "hold";
136
+ }
137
+ return "pass";
138
+ }
139
+
140
+ function summarizeDispatch(dispatchResult) {
141
+ if (dispatchResult.action === "request_paid_approval") {
142
+ return `compute packet requires explicit paid approval for rung ${dispatchResult.rungId}`;
143
+ }
144
+ if (dispatchResult.action === "hold_packet") {
145
+ return `compute packet is being held because ${dispatchResult.reason}`;
146
+ }
147
+ return `compute packet admitted with action ${dispatchResult.action}`;
148
+ }
149
+
150
+ async function runDecide(options) {
151
+ if (!options.input) {
152
+ throw new Error("compute decide requires --input <path>");
153
+ }
154
+
155
+ const context = buildContext(await readJson(options.input));
156
+ const dispatchResult = evaluateDispatch(context);
157
+ const gateResult = buildOrpComputeGateResult({
158
+ gateId: context.packet.rungId,
159
+ command: commandLabel("decide", options),
160
+ status: gateStatusForDispatch(dispatchResult.action),
161
+ evidenceNote: summarizeDispatch(dispatchResult),
162
+ });
163
+
164
+ const payload = {
165
+ ok: dispatchResult.action !== "hold_packet",
166
+ command: "compute decide",
167
+ dispatch_result: dispatchResult,
168
+ gate_result: gateResult,
169
+ };
170
+
171
+ if (options.packetOut) {
172
+ const orpPacket = buildOrpComputePacket({
173
+ repoRoot: context.raw.repo?.rootPath || process.cwd(),
174
+ repoGit: context.raw.repo?.git,
175
+ decision: context.decision,
176
+ packet: context.packet,
177
+ dispatchResult,
178
+ gateResults: [gateResult],
179
+ artifactRoot: context.raw.orp?.artifactRoot,
180
+ boardId: context.raw.orp?.boardId,
181
+ problemId: context.raw.orp?.problemId,
182
+ stateNote: summarizeDispatch(dispatchResult),
183
+ });
184
+ await writeJson(options.packetOut, orpPacket);
185
+ payload.orp_packet_path = path.resolve(options.packetOut);
186
+ }
187
+
188
+ if (options.json) {
189
+ console.log(JSON.stringify(payload, null, 2));
190
+ } else {
191
+ console.log(`${dispatchResult.action}: ${summarizeDispatch(dispatchResult)}`);
192
+ }
193
+
194
+ return dispatchResult.action === "hold_packet" ? 1 : 0;
195
+ }
196
+
197
+ async function runLocal(options) {
198
+ if (!options.input) {
199
+ throw new Error("compute run-local requires --input <path>");
200
+ }
201
+ if (!options.task) {
202
+ throw new Error("compute run-local requires --task <path>");
203
+ }
204
+
205
+ const context = buildContext(await readJson(options.input));
206
+ const task = await readJson(options.task);
207
+ const dispatchResult = evaluateDispatch(context);
208
+
209
+ if (dispatchResult.action !== "run_local") {
210
+ const message = `compute packet is not locally runnable; dispatch action is ${dispatchResult.action}`;
211
+ if (options.json) {
212
+ console.log(JSON.stringify({ ok: false, error: message, dispatch_result: dispatchResult }, null, 2));
213
+ } else {
214
+ console.error(message);
215
+ }
216
+ return 1;
217
+ }
218
+
219
+ const executionReceipt = await runLocalShellPacket({
220
+ decision: context.decision,
221
+ rung: context.rung,
222
+ packet: context.packet,
223
+ dispatchResult,
224
+ task,
225
+ });
226
+
227
+ const gateResult = buildOrpComputeGateResult({
228
+ gateId: context.packet.rungId,
229
+ command: `${executionReceipt.command} ${executionReceipt.args.join(" ")}`.trim(),
230
+ status: executionReceipt.status === "pass" ? "pass" : "fail",
231
+ exitCode: executionReceipt.exitCode == null ? 1 : executionReceipt.exitCode,
232
+ durationMs: executionReceipt.durationMs,
233
+ evidenceNote: `local shell execution completed with status ${executionReceipt.status}`,
234
+ });
235
+
236
+ const resultBundle = defineResultBundle({
237
+ id: `${context.packet.id}-result`,
238
+ packetId: context.packet.id,
239
+ outputs: context.packet.requiredOutputs,
240
+ status: executionReceipt.status,
241
+ metrics: {
242
+ exitCode: executionReceipt.exitCode,
243
+ durationMs: executionReceipt.durationMs,
244
+ timedOut: executionReceipt.timedOut,
245
+ },
246
+ });
247
+
248
+ const impactRead = defineImpactRead({
249
+ id: `${context.packet.id}-impact`,
250
+ bundleId: resultBundle.id,
251
+ nextAction: executionReceipt.status === "pass" ? "review_result_bundle" : "reroute_or_debug",
252
+ summary:
253
+ executionReceipt.status === "pass"
254
+ ? `local compute packet ${context.packet.id} completed successfully`
255
+ : `local compute packet ${context.packet.id} failed and needs follow-up`,
256
+ });
257
+
258
+ const payload = {
259
+ ok: executionReceipt.status === "pass",
260
+ command: "compute run-local",
261
+ dispatch_result: dispatchResult,
262
+ execution_receipt: executionReceipt,
263
+ gate_result: gateResult,
264
+ result_bundle: resultBundle,
265
+ impact_read: impactRead,
266
+ };
267
+
268
+ if (options.receiptOut) {
269
+ await writeJson(options.receiptOut, executionReceipt);
270
+ payload.execution_receipt_path = path.resolve(options.receiptOut);
271
+ }
272
+
273
+ if (options.packetOut) {
274
+ const orpPacket = buildOrpComputePacket({
275
+ repoRoot: context.raw.repo?.rootPath || process.cwd(),
276
+ repoGit: context.raw.repo?.git,
277
+ decision: context.decision,
278
+ packet: context.packet,
279
+ dispatchResult,
280
+ resultBundle,
281
+ impactRead,
282
+ gateResults: [gateResult],
283
+ artifactRoot: context.raw.orp?.artifactRoot,
284
+ boardId: context.raw.orp?.boardId,
285
+ problemId: context.raw.orp?.problemId,
286
+ extraPaths: options.receiptOut ? [path.resolve(options.receiptOut)] : [],
287
+ stateNote: impactRead.summary,
288
+ });
289
+ await writeJson(options.packetOut, orpPacket);
290
+ payload.orp_packet_path = path.resolve(options.packetOut);
291
+ }
292
+
293
+ if (options.json) {
294
+ console.log(JSON.stringify(payload, null, 2));
295
+ } else {
296
+ console.log(`${executionReceipt.status}: ${impactRead.summary}`);
297
+ }
298
+
299
+ if (executionReceipt.exitCode == null) {
300
+ return 1;
301
+ }
302
+ return executionReceipt.exitCode;
303
+ }
304
+
305
+ export async function runComputeCli(argv = process.argv.slice(2)) {
306
+ let options;
307
+ try {
308
+ options = parseArgs(argv);
309
+ } catch (error) {
310
+ console.error(String(error.message || error));
311
+ printHelp();
312
+ return 1;
313
+ }
314
+
315
+ if (options.help || !options.command) {
316
+ printHelp();
317
+ return 0;
318
+ }
319
+
320
+ try {
321
+ if (options.command === "decide") {
322
+ return await runDecide(options);
323
+ }
324
+ if (options.command === "run-local") {
325
+ return await runLocal(options);
326
+ }
327
+ throw new Error(`unknown compute command: ${options.command}`);
328
+ } catch (error) {
329
+ if (options.json) {
330
+ console.log(JSON.stringify({ ok: false, error: String(error.message || error) }, null, 2));
331
+ } else {
332
+ console.error(String(error.message || error));
333
+ }
334
+ return 1;
335
+ }
336
+ }
337
+
338
+ if (import.meta.url === `file://${process.argv[1]}`) {
339
+ const code = await runComputeCli(process.argv.slice(2));
340
+ process.exit(code == null ? 0 : code);
341
+ }
package/bin/orp.js CHANGED
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const path = require("path");
4
+ const { pathToFileURL } = require("url");
4
5
  const { spawnSync } = require("child_process");
5
6
 
6
7
  const cliPath = path.resolve(__dirname, "..", "cli", "orp.py");
8
+ const computeCliUrl = pathToFileURL(path.resolve(__dirname, "orp-compute.mjs")).href;
7
9
  const argv = process.argv.slice(2);
8
10
 
9
11
  const candidates = [];
@@ -15,24 +17,65 @@ if (process.platform === "win32") {
15
17
  }
16
18
  candidates.push("python3", "python");
17
19
 
18
- let lastErr = null;
20
+ function isTopLevelHelp(args) {
21
+ return args.length === 0 || args.includes("-h") || args.includes("--help");
22
+ }
23
+
24
+ async function runCompute(args) {
25
+ const mod = await import(computeCliUrl);
26
+ const code = await mod.runComputeCli(args);
27
+ process.exit(code == null ? 0 : code);
28
+ }
19
29
 
20
- for (const py of candidates) {
21
- const args = py === "py" ? ["-3", cliPath, ...argv] : [cliPath, ...argv];
22
- const result = spawnSync(py, args, { stdio: "inherit" });
23
- if (!result.error) {
24
- process.exit(result.status == null ? 1 : result.status);
30
+ async function main() {
31
+ if (argv[0] === "compute") {
32
+ await runCompute(argv.slice(1));
33
+ return;
25
34
  }
26
- if (result.error && result.error.code === "ENOENT") {
27
- continue;
35
+
36
+ const captureOutput = isTopLevelHelp(argv);
37
+ let lastErr = null;
38
+
39
+ for (const py of candidates) {
40
+ const args = py === "py" ? ["-3", cliPath, ...argv] : [cliPath, ...argv];
41
+ const result = spawnSync(
42
+ py,
43
+ args,
44
+ captureOutput
45
+ ? { encoding: "utf8" }
46
+ : { stdio: "inherit" },
47
+ );
48
+
49
+ if (!result.error) {
50
+ if (captureOutput) {
51
+ if (result.stdout) {
52
+ process.stdout.write(result.stdout);
53
+ }
54
+ if (result.stderr) {
55
+ process.stderr.write(result.stderr);
56
+ }
57
+ if (result.status === 0) {
58
+ process.stdout.write("\nAdditional wrapper surface:\n orp compute -h\n");
59
+ }
60
+ }
61
+ process.exit(result.status == null ? 1 : result.status);
62
+ }
63
+
64
+ if (result.error && result.error.code === "ENOENT") {
65
+ continue;
66
+ }
67
+ lastErr = result.error;
28
68
  }
29
- lastErr = result.error;
30
- }
31
69
 
32
- console.error("ORP CLI requires Python 3 on PATH.");
33
- console.error("Tried: " + candidates.join(", "));
34
- if (lastErr) {
35
- console.error(String(lastErr));
70
+ console.error("ORP CLI requires Python 3 on PATH.");
71
+ console.error("Tried: " + candidates.join(", "));
72
+ if (lastErr) {
73
+ console.error(String(lastErr));
74
+ }
75
+ process.exit(1);
36
76
  }
37
- process.exit(1);
38
77
 
78
+ main().catch((error) => {
79
+ console.error(String(error && error.stack ? error.stack : error));
80
+ process.exit(1);
81
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-research-protocol",
3
- "version": "0.4.9",
3
+ "version": "0.4.10",
4
4
  "description": "ORP CLI (Open Research Protocol): agent-friendly research workflows, runtime, reports, and pack tooling.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -201,9 +201,9 @@ def _benchmark_init_starter(iterations: int) -> dict[str, Any]:
201
201
  run_records.append(gate_payload["run_record"])
202
202
 
203
203
  targets = {
204
- "init_mean_lt_ms": 350.0,
205
- "validate_mean_lt_ms": 200.0,
206
- "gate_mean_lt_ms": 325.0,
204
+ "init_mean_lt_ms": 375.0,
205
+ "validate_mean_lt_ms": 210.0,
206
+ "gate_mean_lt_ms": 350.0,
207
207
  }
208
208
  observed = {
209
209
  "init": _stats(init_times),
@@ -264,8 +264,8 @@ def _benchmark_artifact_roundtrip() -> dict[str, Any]:
264
264
  "validate": _stats(validate_times),
265
265
  }
266
266
  targets = {
267
- "scaffold_mean_lt_ms": 200.0,
268
- "validate_mean_lt_ms": 200.0,
267
+ "scaffold_mean_lt_ms": 210.0,
268
+ "validate_mean_lt_ms": 210.0,
269
269
  }
270
270
  return {
271
271
  "artifact_classes_total": len(rows),
@@ -490,7 +490,7 @@ def _benchmark_cross_domain_corpus() -> dict[str, Any]:
490
490
  targets = {
491
491
  "domains_min": 5,
492
492
  "fixtures_min": 7,
493
- "validate_mean_lt_ms": 200.0,
493
+ "validate_mean_lt_ms": 210.0,
494
494
  }
495
495
  return {
496
496
  "fixtures_total": len(rows),
@@ -549,7 +549,7 @@ def _benchmark_requirement_enforcement() -> dict[str, Any]:
549
549
  observed = {"validate": _stats(validate_times)}
550
550
  targets = {
551
551
  "all_cases_detected": sum(len(fields) for fields in requirements.values()),
552
- "validate_mean_lt_ms": 200.0,
552
+ "validate_mean_lt_ms": 210.0,
553
553
  }
554
554
  return {
555
555
  "cases_total": len(rows),
@@ -717,7 +717,7 @@ def _benchmark_mutation_stress() -> dict[str, Any]:
717
717
  observed = {"validate": _stats(validate_times)}
718
718
  targets = {
719
719
  "cases_total": len(cases),
720
- "validate_mean_lt_ms": 200.0,
720
+ "validate_mean_lt_ms": 210.0,
721
721
  }
722
722
  return {
723
723
  "cases_total": len(rows),