open-research-protocol 0.4.14 → 0.4.16

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 (52) hide show
  1. package/AGENT_INTEGRATION.md +50 -0
  2. package/README.md +273 -144
  3. package/bin/orp.js +14 -1
  4. package/cli/orp.py +14846 -9925
  5. package/docs/AGENT_LOOP.md +13 -0
  6. package/docs/AGENT_MODES.md +79 -0
  7. package/docs/CANONICAL_CLI_BOUNDARY.md +15 -0
  8. package/docs/EXCHANGE.md +94 -0
  9. package/docs/LAUNCH_KIT.md +107 -0
  10. package/docs/ORP_HOSTED_WORKSPACE_CONTRACT.md +295 -0
  11. package/docs/ORP_PUBLIC_LAUNCH_CHECKLIST.md +5 -0
  12. package/docs/START_HERE.md +567 -0
  13. package/package.json +4 -2
  14. package/packages/lifeops-orp/README.md +67 -0
  15. package/packages/lifeops-orp/package.json +48 -0
  16. package/packages/lifeops-orp/src/index.d.ts +106 -0
  17. package/packages/lifeops-orp/src/index.js +7 -0
  18. package/packages/lifeops-orp/src/mapping.js +309 -0
  19. package/packages/lifeops-orp/src/workspace.js +108 -0
  20. package/packages/lifeops-orp/test/orp.test.js +187 -0
  21. package/packages/orp-workspace-launcher/README.md +82 -0
  22. package/packages/orp-workspace-launcher/package.json +39 -0
  23. package/packages/orp-workspace-launcher/src/commands.js +77 -0
  24. package/packages/orp-workspace-launcher/src/core-plan.js +506 -0
  25. package/packages/orp-workspace-launcher/src/hosted-state.js +208 -0
  26. package/packages/orp-workspace-launcher/src/index.js +82 -0
  27. package/packages/orp-workspace-launcher/src/ledger.js +745 -0
  28. package/packages/orp-workspace-launcher/src/list.js +488 -0
  29. package/packages/orp-workspace-launcher/src/orp-command.js +126 -0
  30. package/packages/orp-workspace-launcher/src/orp.js +912 -0
  31. package/packages/orp-workspace-launcher/src/registry.js +558 -0
  32. package/packages/orp-workspace-launcher/src/slot.js +188 -0
  33. package/packages/orp-workspace-launcher/src/sync.js +363 -0
  34. package/packages/orp-workspace-launcher/src/tabs.js +166 -0
  35. package/packages/orp-workspace-launcher/test/commands.test.js +164 -0
  36. package/packages/orp-workspace-launcher/test/core-plan.test.js +253 -0
  37. package/packages/orp-workspace-launcher/test/fixtures/smoke-notes.txt +2 -0
  38. package/packages/orp-workspace-launcher/test/fixtures/workspace-manifest.json +17 -0
  39. package/packages/orp-workspace-launcher/test/ledger.test.js +244 -0
  40. package/packages/orp-workspace-launcher/test/list.test.js +299 -0
  41. package/packages/orp-workspace-launcher/test/orp-command.test.js +44 -0
  42. package/packages/orp-workspace-launcher/test/orp.test.js +224 -0
  43. package/packages/orp-workspace-launcher/test/tabs.test.js +168 -0
  44. package/scripts/orp-kernel-agent-pilot.py +10 -1
  45. package/scripts/orp-kernel-agent-replication.py +10 -1
  46. package/scripts/orp-kernel-canonical-continuation.py +10 -1
  47. package/scripts/orp-kernel-continuation-pilot.py +10 -1
  48. package/scripts/render-terminal-demo.py +416 -0
  49. package/spec/v1/exchange-report.schema.json +105 -0
  50. package/spec/v1/hosted-workspace-event.schema.json +102 -0
  51. package/spec/v1/hosted-workspace.schema.json +332 -0
  52. package/spec/v1/workspace.schema.json +108 -0
@@ -0,0 +1,224 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ import {
8
+ chooseImplicitMainCandidate,
9
+ loadWorkspaceSource,
10
+ resolveWorkspaceSelectorFromCollections,
11
+ resolveWorkspaceWatchTargets,
12
+ } from "../src/orp.js";
13
+ import { setWorkspaceSlot } from "../src/registry.js";
14
+
15
+ async function makeTempDir() {
16
+ return fs.mkdtemp(path.join(os.tmpdir(), "orp-workspace-selector-"));
17
+ }
18
+
19
+ test("resolveWorkspaceSelectorFromCollections matches hosted ideas by saved workspace id and title slug", () => {
20
+ const ideas = [
21
+ {
22
+ id: "ef86bd4c-35b0-454c-ac90-9136a006b0af",
23
+ title: "Terminal paths and codex sessions 03-26-2026",
24
+ notes: `
25
+ \`\`\`orp-workspace
26
+ {
27
+ "version": "1",
28
+ "workspaceId": "workspace-main-1",
29
+ "title": "Main Cody 1",
30
+ "tabs": [
31
+ { "title": "orp", "path": "/Volumes/Code_2TB/code/orp" }
32
+ ]
33
+ }
34
+ \`\`\`
35
+ `,
36
+ },
37
+ ];
38
+
39
+ const byWorkspaceId = resolveWorkspaceSelectorFromCollections("workspace-main-1", { ideas });
40
+ assert.equal(byWorkspaceId?.kind, "hosted-idea");
41
+ assert.equal(byWorkspaceId?.idea?.id, "ef86bd4c-35b0-454c-ac90-9136a006b0af");
42
+
43
+ const byTitleSlug = resolveWorkspaceSelectorFromCollections("main-cody-1", { ideas });
44
+ assert.equal(byTitleSlug?.title, "Main Cody 1");
45
+ });
46
+
47
+ test("resolveWorkspaceSelectorFromCollections can match local tracked workspaces by title", () => {
48
+ const resolved = resolveWorkspaceSelectorFromCollections("ORP Main", {
49
+ localWorkspaces: [
50
+ {
51
+ manifestPath: "/tmp/orp-main.json",
52
+ workspaceId: "orp-main",
53
+ title: "ORP Main",
54
+ status: "ok",
55
+ },
56
+ ],
57
+ });
58
+
59
+ assert.equal(resolved?.kind, "workspace-file");
60
+ assert.equal(resolved?.manifestPath, "/tmp/orp-main.json");
61
+ });
62
+
63
+ test("resolveWorkspaceSelectorFromCollections prefers hosted saved workspaces over local duplicates", () => {
64
+ const ideas = [
65
+ {
66
+ id: "idea-main",
67
+ title: "ORP Main",
68
+ notes: `
69
+ \`\`\`orp-workspace
70
+ {
71
+ "version": "1",
72
+ "workspaceId": "orp-main",
73
+ "title": "ORP Main",
74
+ "tabs": [
75
+ { "path": "/Volumes/Code_2TB/code/orp" }
76
+ ]
77
+ }
78
+ \`\`\`
79
+ `,
80
+ },
81
+ ];
82
+ const localWorkspaces = [
83
+ {
84
+ manifestPath: "/tmp/orp-main.json",
85
+ workspaceId: "orp-main",
86
+ title: "ORP Main",
87
+ status: "ok",
88
+ },
89
+ ];
90
+
91
+ const resolved = resolveWorkspaceSelectorFromCollections("orp-main", {
92
+ ideas,
93
+ localWorkspaces,
94
+ });
95
+
96
+ assert.equal(resolved?.kind, "hosted-idea");
97
+ assert.equal(resolved?.idea?.id, "idea-main");
98
+ });
99
+
100
+ test("resolveWorkspaceSelectorFromCollections raises on ambiguous hosted titles", () => {
101
+ const ideas = [
102
+ {
103
+ id: "idea-1",
104
+ title: "Main Cody 1",
105
+ notes: `
106
+ /Volumes/Code_2TB/code/orp
107
+ `,
108
+ },
109
+ {
110
+ id: "idea-2",
111
+ title: "Main Cody 1",
112
+ notes: `
113
+ /Volumes/Code_2TB/code/orp-web-app
114
+ `,
115
+ },
116
+ ];
117
+
118
+ assert.throws(
119
+ () => resolveWorkspaceSelectorFromCollections("Main Cody 1", { ideas }),
120
+ /ambiguous/i,
121
+ );
122
+ });
123
+
124
+ test("loadWorkspaceSource resolves the main slot to an explicit local manifest", async () => {
125
+ const tempDir = await makeTempDir();
126
+ const env = {
127
+ ...process.env,
128
+ XDG_CONFIG_HOME: path.join(tempDir, "config"),
129
+ };
130
+ const manifestPath = path.join(tempDir, "main-cody-1.json");
131
+ await fs.writeFile(
132
+ manifestPath,
133
+ `${JSON.stringify(
134
+ {
135
+ version: "1",
136
+ workspaceId: "main-cody-1",
137
+ title: "main-cody-1",
138
+ tabs: [{ path: "/Volumes/Code_2TB/code/orp" }],
139
+ },
140
+ null,
141
+ 2,
142
+ )}\n`,
143
+ "utf8",
144
+ );
145
+ await setWorkspaceSlot(
146
+ "main",
147
+ {
148
+ kind: "workspace-file",
149
+ workspaceId: "main-cody-1",
150
+ title: "main-cody-1",
151
+ manifestPath,
152
+ },
153
+ { env },
154
+ );
155
+
156
+ const source = await loadWorkspaceSource({
157
+ ideaId: "main",
158
+ env,
159
+ });
160
+
161
+ assert.equal(source.sourceType, "workspace-file");
162
+ assert.equal(source.sourcePath, path.resolve(manifestPath));
163
+ assert.equal(source.workspaceManifest?.workspaceId, "main-cody-1");
164
+ });
165
+
166
+ test("chooseImplicitMainCandidate prefers the single hosted-backed workspace over generated local captures", () => {
167
+ const candidate = chooseImplicitMainCandidate([
168
+ {
169
+ kind: "hosted-idea",
170
+ workspaceId: "idea-ef86bd4c-35b0-454c-ac90-9136a006b0af",
171
+ title: "Terminal paths and codex sessions 03-26-2026",
172
+ },
173
+ {
174
+ kind: "workspace-file",
175
+ workspaceId: "captured-iterm-window-20260401t032225z",
176
+ title: "captured-iterm-window-20260401t032225z",
177
+ manifestPath: "/tmp/captured-1.json",
178
+ },
179
+ {
180
+ kind: "workspace-file",
181
+ workspaceId: "captured-iterm-window-20260401t032215z",
182
+ title: "captured-iterm-window-20260401t032215z",
183
+ manifestPath: "/tmp/captured-2.json",
184
+ },
185
+ ]);
186
+
187
+ assert.equal(candidate?.kind, "hosted-idea");
188
+ assert.equal(candidate?.workspaceId, "idea-ef86bd4c-35b0-454c-ac90-9136a006b0af");
189
+ });
190
+
191
+ test("resolveWorkspaceWatchTargets syncs idea-bridge hosted workspaces through the linked idea", () => {
192
+ const targets = resolveWorkspaceWatchTargets(
193
+ {
194
+ sourceType: "hosted-workspace",
195
+ hostedWorkspace: {
196
+ workspace_id: "idea-ef86bd4c-35b0-454c-ac90-9136a006b0af",
197
+ source_kind: "idea_bridge",
198
+ linkedIdea: {
199
+ ideaId: "ef86bd4c-35b0-454c-ac90-9136a006b0af",
200
+ },
201
+ },
202
+ },
203
+ {},
204
+ );
205
+
206
+ assert.equal(targets.hostedWorkspaceId, null);
207
+ assert.equal(targets.syncIdeaSelector, "ef86bd4c-35b0-454c-ac90-9136a006b0af");
208
+ });
209
+
210
+ test("resolveWorkspaceWatchTargets preserves true hosted workspace ids", () => {
211
+ const targets = resolveWorkspaceWatchTargets(
212
+ {
213
+ sourceType: "hosted-workspace",
214
+ hostedWorkspace: {
215
+ workspace_id: "ws_orp_main",
216
+ source_kind: "hosted",
217
+ },
218
+ },
219
+ {},
220
+ );
221
+
222
+ assert.equal(targets.hostedWorkspaceId, "ws_orp_main");
223
+ assert.equal(targets.syncIdeaSelector, null);
224
+ });
@@ -0,0 +1,168 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ import { buildWorkspaceTabsReport, parseWorkspaceSource, parseWorkspaceTabsArgs, runWorkspaceTabs } from "../src/index.js";
8
+
9
+ async function makeTempDir() {
10
+ return fs.mkdtemp(path.join(os.tmpdir(), "orp-workspace-tabs-"));
11
+ }
12
+
13
+ async function captureStdout(fn) {
14
+ const chunks = [];
15
+ const originalWrite = process.stdout.write;
16
+ process.stdout.write = (chunk, encoding, callback) => {
17
+ chunks.push(typeof chunk === "string" ? chunk : chunk.toString(encoding || "utf8"));
18
+ if (typeof callback === "function") {
19
+ callback();
20
+ }
21
+ return true;
22
+ };
23
+
24
+ try {
25
+ const code = await fn();
26
+ return {
27
+ code,
28
+ stdout: chunks.join(""),
29
+ };
30
+ } finally {
31
+ process.stdout.write = originalWrite;
32
+ }
33
+ }
34
+
35
+ test("parseWorkspaceTabsArgs accepts JSON and workspace selectors", () => {
36
+ const parsed = parseWorkspaceTabsArgs([
37
+ "--workspace-file",
38
+ "/tmp/workspace.json",
39
+ "--json",
40
+ ]);
41
+
42
+ assert.equal(parsed.workspaceFile, "/tmp/workspace.json");
43
+ assert.equal(parsed.json, true);
44
+ assert.throws(() => parseWorkspaceTabsArgs(["idea-123", "idea-456"]), /unexpected argument/);
45
+ assert.throws(() => parseWorkspaceTabsArgs(["--wat"]), /missing value|unknown option/);
46
+ });
47
+
48
+ test("buildWorkspaceTabsReport keeps duplicate titles unique and exposes generic resume metadata", () => {
49
+ const parsed = parseWorkspaceSource({
50
+ sourceType: "hosted-idea",
51
+ sourceLabel: "Workspace idea",
52
+ title: "Workspace idea",
53
+ notes: `
54
+ /Volumes/Code_2TB/code/collaboration: codex resume abc-123
55
+ /Volumes/Code_2TB/code/anthropic-lab: claude resume claude-456
56
+ /Volumes/Code_2TB/code/collaboration
57
+ `,
58
+ });
59
+
60
+ const report = buildWorkspaceTabsReport(
61
+ {
62
+ sourceType: "hosted-idea",
63
+ sourceLabel: "Workspace idea",
64
+ title: "Workspace idea",
65
+ },
66
+ parsed,
67
+ );
68
+
69
+ assert.match(report.workspaceId, /^workspace-/);
70
+ assert.equal(report.tabCount, 3);
71
+ assert.equal(report.tabs[0]?.title, "collaboration");
72
+ assert.equal(report.tabs[0]?.resumeCommand, "codex resume abc-123");
73
+ assert.equal(
74
+ report.tabs[0]?.restartCommand,
75
+ "cd '/Volumes/Code_2TB/code/collaboration' && codex resume abc-123",
76
+ );
77
+ assert.equal(report.tabs[0]?.codexSessionId, "abc-123");
78
+ assert.equal(report.tabs[1]?.title, "anthropic-lab");
79
+ assert.equal(report.tabs[1]?.resumeCommand, "claude resume claude-456");
80
+ assert.equal(
81
+ report.tabs[1]?.restartCommand,
82
+ "cd '/Volumes/Code_2TB/code/anthropic-lab' && claude resume claude-456",
83
+ );
84
+ assert.equal(report.tabs[1]?.claudeSessionId, "claude-456");
85
+ assert.equal(report.tabs[2]?.title, "collaboration (2)");
86
+ assert.equal(report.tabs[2]?.codexSessionId, null);
87
+ });
88
+
89
+ test("runWorkspaceTabs prints JSON without launch commands", async () => {
90
+ const tempDir = await makeTempDir();
91
+ const manifestPath = path.join(tempDir, "workspace.json");
92
+ await fs.writeFile(
93
+ manifestPath,
94
+ `${JSON.stringify(
95
+ {
96
+ version: "1",
97
+ workspaceId: "orp-main",
98
+ title: "ORP Main",
99
+ tabs: [
100
+ {
101
+ title: "orp",
102
+ path: "/Volumes/Code_2TB/code/orp",
103
+ resumeCommand: "claude resume claude-999",
104
+ resumeTool: "claude",
105
+ resumeSessionId: "claude-999",
106
+ },
107
+ {
108
+ title: "web",
109
+ path: "/Volumes/Code_2TB/code/orp-web-app",
110
+ },
111
+ ],
112
+ },
113
+ null,
114
+ 2,
115
+ )}\n`,
116
+ "utf8",
117
+ );
118
+
119
+ const { code, stdout } = await captureStdout(() => runWorkspaceTabs(["--workspace-file", manifestPath, "--json"]));
120
+ const parsed = JSON.parse(stdout);
121
+
122
+ assert.equal(code, 0);
123
+ assert.equal(parsed.workspaceId, "orp-main");
124
+ assert.equal(parsed.tabCount, 2);
125
+ assert.equal(parsed.tabs[0]?.title, "orp");
126
+ assert.equal(parsed.tabs[0]?.resumeCommand, "claude resume claude-999");
127
+ assert.equal(parsed.tabs[0]?.restartCommand, "cd '/Volumes/Code_2TB/code/orp' && claude resume claude-999");
128
+ assert.equal(parsed.tabs[0]?.claudeSessionId, "claude-999");
129
+ assert.equal(parsed.tabs[1]?.title, "web");
130
+ assert.ok(!stdout.includes('"command"'));
131
+ });
132
+
133
+ test("buildWorkspaceTabsReport canonicalizes Claude resume commands from tool and session metadata", () => {
134
+ const parsed = parseWorkspaceSource({
135
+ sourceType: "workspace-file",
136
+ sourceLabel: "/tmp/workspace.json",
137
+ title: "workspace",
138
+ workspaceManifest: {
139
+ version: "1",
140
+ workspaceId: "orp-main",
141
+ tabs: [
142
+ {
143
+ title: "anthropic-lab",
144
+ path: "/Volumes/Code_2TB/code/anthropic-lab",
145
+ resumeTool: "claude",
146
+ resumeSessionId: "claude-456",
147
+ },
148
+ ],
149
+ },
150
+ notes: "",
151
+ });
152
+
153
+ const report = buildWorkspaceTabsReport(
154
+ {
155
+ sourceType: "workspace-file",
156
+ sourceLabel: "/tmp/workspace.json",
157
+ title: "workspace",
158
+ },
159
+ parsed,
160
+ );
161
+
162
+ assert.equal(report.tabs[0]?.resumeCommand, "claude --resume claude-456");
163
+ assert.equal(
164
+ report.tabs[0]?.restartCommand,
165
+ "cd '/Volumes/Code_2TB/code/anthropic-lab' && claude --resume claude-456",
166
+ );
167
+ assert.equal(report.tabs[0]?.claudeSessionId, "claude-456");
168
+ });
@@ -542,7 +542,16 @@ def _gather_metadata(model: str) -> dict[str, Any]:
542
542
  package_version = _read_json(REPO_ROOT / "package.json")["version"]
543
543
  commit = subprocess.run(["git", "rev-parse", "HEAD"], cwd=str(REPO_ROOT), capture_output=True, text=True, check=True).stdout.strip()
544
544
  branch = subprocess.run(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=str(REPO_ROOT), capture_output=True, text=True, check=True).stdout.strip()
545
- codex_version = subprocess.run(["codex", "--version"], cwd=str(REPO_ROOT), capture_output=True, text=True, check=True).stdout.strip()
545
+ try:
546
+ codex_version = subprocess.run(
547
+ ["codex", "--version"],
548
+ cwd=str(REPO_ROOT),
549
+ capture_output=True,
550
+ text=True,
551
+ check=True,
552
+ ).stdout.strip()
553
+ except (FileNotFoundError, subprocess.CalledProcessError):
554
+ codex_version = "unavailable"
546
555
  return {
547
556
  "generated_at_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
548
557
  "repo_commit": commit,
@@ -51,7 +51,16 @@ def _gather_metadata(model: str, repeats: int) -> dict[str, Any]:
51
51
  package_version = json.loads((REPO_ROOT / "package.json").read_text(encoding="utf-8"))["version"]
52
52
  commit = subprocess.run(["git", "rev-parse", "HEAD"], cwd=str(REPO_ROOT), capture_output=True, text=True, check=True).stdout.strip()
53
53
  branch = subprocess.run(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=str(REPO_ROOT), capture_output=True, text=True, check=True).stdout.strip()
54
- codex_version = subprocess.run(["codex", "--version"], cwd=str(REPO_ROOT), capture_output=True, text=True, check=True).stdout.strip()
54
+ try:
55
+ codex_version = subprocess.run(
56
+ ["codex", "--version"],
57
+ cwd=str(REPO_ROOT),
58
+ capture_output=True,
59
+ text=True,
60
+ check=True,
61
+ ).stdout.strip()
62
+ except (FileNotFoundError, subprocess.CalledProcessError):
63
+ codex_version = "unavailable"
55
64
  return {
56
65
  "generated_at_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
57
66
  "repo_commit": commit,
@@ -276,7 +276,16 @@ def _gather_metadata(model: str) -> dict[str, Any]:
276
276
  package_version = json.loads((REPO_ROOT / "package.json").read_text(encoding="utf-8"))["version"]
277
277
  commit = subprocess.run(["git", "rev-parse", "HEAD"], cwd=str(REPO_ROOT), capture_output=True, text=True, check=True).stdout.strip()
278
278
  branch = subprocess.run(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=str(REPO_ROOT), capture_output=True, text=True, check=True).stdout.strip()
279
- codex_version = subprocess.run(["codex", "--version"], cwd=str(REPO_ROOT), capture_output=True, text=True, check=True).stdout.strip()
279
+ try:
280
+ codex_version = subprocess.run(
281
+ ["codex", "--version"],
282
+ cwd=str(REPO_ROOT),
283
+ capture_output=True,
284
+ text=True,
285
+ check=True,
286
+ ).stdout.strip()
287
+ except (FileNotFoundError, subprocess.CalledProcessError):
288
+ codex_version = "unavailable"
280
289
  return {
281
290
  "generated_at_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
282
291
  "repo_commit": commit,
@@ -278,7 +278,16 @@ def _gather_metadata(model: str) -> dict[str, Any]:
278
278
  package_version = json.loads((REPO_ROOT / "package.json").read_text(encoding="utf-8"))["version"]
279
279
  commit = subprocess.run(["git", "rev-parse", "HEAD"], cwd=str(REPO_ROOT), capture_output=True, text=True, check=True).stdout.strip()
280
280
  branch = subprocess.run(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=str(REPO_ROOT), capture_output=True, text=True, check=True).stdout.strip()
281
- codex_version = subprocess.run(["codex", "--version"], cwd=str(REPO_ROOT), capture_output=True, text=True, check=True).stdout.strip()
281
+ try:
282
+ codex_version = subprocess.run(
283
+ ["codex", "--version"],
284
+ cwd=str(REPO_ROOT),
285
+ capture_output=True,
286
+ text=True,
287
+ check=True,
288
+ ).stdout.strip()
289
+ except (FileNotFoundError, subprocess.CalledProcessError):
290
+ codex_version = "unavailable"
282
291
  return {
283
292
  "generated_at_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
284
293
  "repo_commit": commit,