open-research-protocol 0.4.23 → 0.4.25

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.
@@ -57,6 +57,120 @@ async function withTempConfigHome(fn) {
57
57
  }
58
58
  }
59
59
 
60
+ async function makeFakeIdeaBridgeOrpCommand(tempDir, options = {}) {
61
+ const scriptPath = path.join(tempDir, "fake-orp.cjs");
62
+ const logPath = path.join(tempDir, "orp-calls.jsonl");
63
+ const failCreate = options.failCreate === true;
64
+ const script = `#!/usr/bin/env node
65
+ const fs = require("node:fs");
66
+
67
+ const args = process.argv.slice(2);
68
+ const logPath = process.env.FAKE_ORP_LOG;
69
+ if (logPath) {
70
+ fs.appendFileSync(logPath, JSON.stringify({ args }) + "\\n", "utf8");
71
+ }
72
+
73
+ const ideaId = "idea-main-123";
74
+ const bridgeWorkspaceId = "captured-iterm-window-20260401t032225z";
75
+ const hostedWorkspaceId = "ws-main-cody-1";
76
+ const failCreate = ${JSON.stringify(failCreate)};
77
+ const baseTabs = [
78
+ { tab_id: "tab-orp", title: "orp", project_root: "/Volumes/Code_2TB/code/orp" },
79
+ { tab_id: "tab-frg", title: "frg-site", project_root: "/Volumes/Code_2TB/code/frg-site" },
80
+ ];
81
+
82
+ function write(payload) {
83
+ process.stdout.write(JSON.stringify(payload) + "\\n");
84
+ }
85
+
86
+ function linkedIdea() {
87
+ return {
88
+ idea_id: ideaId,
89
+ idea_title: "Terminal paths and codex sessions 03-26-2026",
90
+ relationship: "primary",
91
+ };
92
+ }
93
+
94
+ function state(tabs) {
95
+ return {
96
+ captured_at_utc: "2026-04-15T12:00:00.000Z",
97
+ updated_at_utc: "2026-04-15T12:00:00.000Z",
98
+ tab_count: tabs.length,
99
+ capture_context: {
100
+ source_app: "test",
101
+ mode: "snapshot",
102
+ machine_id: "test-mac:darwin",
103
+ machine_label: "Test Mac",
104
+ platform: "darwin",
105
+ },
106
+ tabs,
107
+ };
108
+ }
109
+
110
+ function bridgeWorkspace() {
111
+ return {
112
+ workspace_id: bridgeWorkspaceId,
113
+ title: "Main Cody 1",
114
+ source_kind: "idea_bridge",
115
+ linked_idea: linkedIdea(),
116
+ state: state(baseTabs),
117
+ };
118
+ }
119
+
120
+ function hostedWorkspace(tabs = []) {
121
+ return {
122
+ workspace_id: hostedWorkspaceId,
123
+ title: "main-cody-1",
124
+ source_kind: "hosted",
125
+ linked_idea: linkedIdea(),
126
+ state: state(tabs),
127
+ };
128
+ }
129
+
130
+ if (args[0] === "workspaces" && args[1] === "list") {
131
+ write({ ok: true, source: "hosted", workspaces: [bridgeWorkspace()], has_more: false, cursor: "" });
132
+ } else if (args[0] === "workspaces" && args[1] === "show") {
133
+ if (args[2] === bridgeWorkspaceId) {
134
+ write({ ok: true, workspace: bridgeWorkspace() });
135
+ } else if (args[2] === hostedWorkspaceId) {
136
+ write({ ok: true, workspace: hostedWorkspace(baseTabs) });
137
+ } else {
138
+ process.stderr.write("unknown workspace: " + args[2] + "\\n");
139
+ process.exit(1);
140
+ }
141
+ } else if (args[0] === "workspaces" && args[1] === "add") {
142
+ if (failCreate) {
143
+ process.stderr.write("Hosted ORP returned an HTML error page instead of JSON (status=404 path=/api/cli/workspaces)\\n");
144
+ process.exit(1);
145
+ }
146
+ const title = args[args.indexOf("--title") + 1];
147
+ const linked = args[args.indexOf("--idea-id") + 1];
148
+ if (title !== "main-cody-1" || linked !== ideaId) {
149
+ process.stderr.write("unexpected add args: " + JSON.stringify(args) + "\\n");
150
+ process.exit(1);
151
+ }
152
+ write({ ok: true, workspace: hostedWorkspace([]) });
153
+ } else if (args[0] === "workspaces" && args[1] === "push-state") {
154
+ const stateFile = args[args.indexOf("--state-file") + 1];
155
+ const pushedState = JSON.parse(fs.readFileSync(stateFile, "utf8"));
156
+ write({ ok: true, workspace: hostedWorkspace(pushedState.tabs || []) });
157
+ } else if (args[0] === "ideas" && args[1] === "list") {
158
+ write({ ok: true, ideas: [], has_more: false, cursor: "" });
159
+ } else if (args[0] === "idea" && args[1] === "update") {
160
+ process.stderr.write("idea update should not be called\\n");
161
+ process.exit(2);
162
+ } else {
163
+ process.stderr.write("unexpected args: " + JSON.stringify(args) + "\\n");
164
+ process.exit(1);
165
+ }
166
+ `;
167
+
168
+ await fs.writeFile(scriptPath, script, "utf8");
169
+ await fs.chmod(scriptPath, 0o755);
170
+ await fs.writeFile(logPath, "", "utf8");
171
+ return { scriptPath, logPath };
172
+ }
173
+
60
174
  function sampleManifest() {
61
175
  return normalizeWorkspaceManifest({
62
176
  version: "1",
@@ -326,6 +440,118 @@ test("runWorkspaceAddTab updates a local workspace manifest file", async () => {
326
440
  });
327
441
  });
328
442
 
443
+ test("runWorkspaceAddTab promotes idea-bridge workspaces to hosted workspace state", async () => {
444
+ await withTempConfigHome(async (configHome) => {
445
+ const tempDir = await makeTempDir();
446
+ const { scriptPath, logPath } = await makeFakeIdeaBridgeOrpCommand(tempDir);
447
+ const originalLogPath = process.env.FAKE_ORP_LOG;
448
+ process.env.FAKE_ORP_LOG = logPath;
449
+
450
+ try {
451
+ const { code, stdout } = await captureStdout(() =>
452
+ runWorkspaceAddTab([
453
+ "main",
454
+ "--path",
455
+ "/Volumes/Code_2TB/code/anthropic-lab",
456
+ "--title",
457
+ "anthropic-lab",
458
+ "--remote-url",
459
+ "git@github.com:anthropic/anthropic-lab.git",
460
+ "--resume-tool",
461
+ "codex",
462
+ "--resume-session-id",
463
+ "019d4f24-c8ba-78b2-a726-48b1ce9f0fe9",
464
+ "--orp-command",
465
+ scriptPath,
466
+ "--json",
467
+ ]),
468
+ );
469
+ const payload = JSON.parse(stdout);
470
+ const calls = (await fs.readFile(logPath, "utf8"))
471
+ .trim()
472
+ .split("\n")
473
+ .filter(Boolean)
474
+ .map((line) => JSON.parse(line).args);
475
+ const slots = JSON.parse(await fs.readFile(path.join(configHome, "orp", "workspace-slots.json"), "utf8"));
476
+
477
+ assert.equal(code, 0);
478
+ assert.equal(payload.persistedTo, "hosted-workspace");
479
+ assert.equal(payload.promotedFromIdeaId, "idea-main-123");
480
+ assert.equal(payload.createdHostedWorkspace, true);
481
+ assert.equal(payload.workspaceSourceId, "ws-main-cody-1");
482
+ assert.equal(payload.tabCount, 3);
483
+ assert.equal(payload.tab.restartCommand, "cd '/Volumes/Code_2TB/code/anthropic-lab' && codex resume 019d4f24-c8ba-78b2-a726-48b1ce9f0fe9");
484
+ assert.equal(payload.manifest.tabs[2]?.path, "/Volumes/Code_2TB/code/anthropic-lab");
485
+ assert.ok(calls.some((args) => args[0] === "workspaces" && args[1] === "add"));
486
+ assert.ok(calls.some((args) => args[0] === "workspaces" && args[1] === "push-state"));
487
+ assert.equal(calls.some((args) => args[0] === "idea" && args[1] === "update"), false);
488
+ assert.equal(slots.slots.main.kind, "hosted-workspace");
489
+ assert.equal(slots.slots.main.hostedWorkspaceId, "ws-main-cody-1");
490
+ } finally {
491
+ if (originalLogPath == null) {
492
+ delete process.env.FAKE_ORP_LOG;
493
+ } else {
494
+ process.env.FAKE_ORP_LOG = originalLogPath;
495
+ }
496
+ }
497
+ });
498
+ });
499
+
500
+ test("runWorkspaceAddTab falls back to local ledger when hosted workspace creation is unavailable", async () => {
501
+ await withTempConfigHome(async (configHome) => {
502
+ const tempDir = await makeTempDir();
503
+ const { scriptPath, logPath } = await makeFakeIdeaBridgeOrpCommand(tempDir, { failCreate: true });
504
+ const originalLogPath = process.env.FAKE_ORP_LOG;
505
+ process.env.FAKE_ORP_LOG = logPath;
506
+
507
+ try {
508
+ const { code, stdout } = await captureStdout(() =>
509
+ runWorkspaceAddTab([
510
+ "main",
511
+ "--path",
512
+ "/Volumes/Code_2TB/code/anthropic-lab",
513
+ "--title",
514
+ "anthropic-lab",
515
+ "--remote-url",
516
+ "git@github.com:anthropic/anthropic-lab.git",
517
+ "--resume-tool",
518
+ "codex",
519
+ "--resume-session-id",
520
+ "019d4f24-c8ba-78b2-a726-48b1ce9f0fe9",
521
+ "--orp-command",
522
+ scriptPath,
523
+ "--json",
524
+ ]),
525
+ );
526
+ const payload = JSON.parse(stdout);
527
+ const calls = (await fs.readFile(logPath, "utf8"))
528
+ .trim()
529
+ .split("\n")
530
+ .filter(Boolean)
531
+ .map((line) => JSON.parse(line).args);
532
+ const saved = JSON.parse(await fs.readFile(payload.manifestPath, "utf8"));
533
+ const slots = JSON.parse(await fs.readFile(path.join(configHome, "orp", "workspace-slots.json"), "utf8"));
534
+
535
+ assert.equal(code, 0);
536
+ assert.equal(payload.persistedTo, "workspace-file");
537
+ assert.equal(payload.promotedFromIdeaId, "idea-main-123");
538
+ assert.match(payload.hostedMigrationSkippedReason, /status=404/);
539
+ assert.equal(payload.tabCount, 3);
540
+ assert.equal(saved.tabs[2]?.path, "/Volumes/Code_2TB/code/anthropic-lab");
541
+ assert.ok(calls.some((args) => args[0] === "workspaces" && args[1] === "add"));
542
+ assert.equal(calls.some((args) => args[0] === "idea" && args[1] === "update"), false);
543
+ assert.equal(slots.slots.main.kind, "workspace-file");
544
+ assert.equal(slots.slots.main.manifestPath, payload.manifestPath);
545
+ } finally {
546
+ if (originalLogPath == null) {
547
+ delete process.env.FAKE_ORP_LOG;
548
+ } else {
549
+ process.env.FAKE_ORP_LOG = originalLogPath;
550
+ }
551
+ }
552
+ });
553
+ });
554
+
329
555
  test("runWorkspaceAddTab upserts an existing tab and returns the rendered recovery command", async () => {
330
556
  await withTempConfigHome(async () => {
331
557
  const tempDir = await makeTempDir();
@@ -123,6 +123,66 @@ test("buildWorkspaceTabsReport keeps duplicate titles unique and exposes generic
123
123
  assert.equal(report.tabs[2]?.codexSessionId, null);
124
124
  });
125
125
 
126
+ test("buildWorkspaceTabsReport ranks Codex tabs by recent local session activity", async () => {
127
+ const tempDir = await makeTempDir();
128
+ const codexHome = path.join(tempDir, "codex-home");
129
+ const sessionsDir = path.join(codexHome, "sessions", "2026", "04", "15");
130
+ await fs.mkdir(sessionsDir, { recursive: true });
131
+
132
+ const olderSessionId = "019d0000-0000-7000-8000-000000000001";
133
+ const newerSessionId = "019d0000-0000-7000-8000-000000000002";
134
+ const olderPath = path.join(sessionsDir, `rollout-2026-04-15T01-00-00-${olderSessionId}.jsonl`);
135
+ const newerPath = path.join(sessionsDir, `rollout-2026-04-15T02-00-00-${newerSessionId}.jsonl`);
136
+ await fs.writeFile(olderPath, "{}\n", "utf8");
137
+ await fs.writeFile(newerPath, "{}\n", "utf8");
138
+ await fs.utimes(olderPath, new Date("2026-04-15T01:00:00Z"), new Date("2026-04-15T01:00:00Z"));
139
+ await fs.utimes(newerPath, new Date("2026-04-15T02:00:00Z"), new Date("2026-04-15T02:00:00Z"));
140
+
141
+ const parsed = parseWorkspaceSource({
142
+ sourceType: "workspace-file",
143
+ sourceLabel: "/tmp/workspace.json",
144
+ title: "workspace",
145
+ workspaceManifest: {
146
+ version: "1",
147
+ workspaceId: "orp-main",
148
+ tabs: [
149
+ {
150
+ title: "older-project",
151
+ path: "/Volumes/Code_2TB/code/older-project",
152
+ resumeCommand: `codex resume ${olderSessionId}`,
153
+ },
154
+ {
155
+ title: "no-session-project",
156
+ path: "/Volumes/Code_2TB/code/no-session-project",
157
+ },
158
+ {
159
+ title: "newer-project",
160
+ path: "/Volumes/Code_2TB/code/newer-project",
161
+ resumeCommand: `codex resume ${newerSessionId}`,
162
+ },
163
+ ],
164
+ },
165
+ notes: "",
166
+ });
167
+
168
+ const report = buildWorkspaceTabsReport(
169
+ {
170
+ sourceType: "workspace-file",
171
+ sourceLabel: "/tmp/workspace.json",
172
+ title: "workspace",
173
+ },
174
+ parsed,
175
+ { codexHome },
176
+ );
177
+
178
+ assert.equal(report.tabs[0]?.title, "newer-project");
179
+ assert.equal(report.tabs[1]?.title, "older-project");
180
+ assert.equal(report.tabs[2]?.title, "no-session-project");
181
+ assert.equal(report.projects[0]?.path, "/Volumes/Code_2TB/code/newer-project");
182
+ assert.equal(report.projects[1]?.path, "/Volumes/Code_2TB/code/older-project");
183
+ assert.equal(report.projects[2]?.path, "/Volumes/Code_2TB/code/no-session-project");
184
+ });
185
+
126
186
  test("runWorkspaceTabs prints JSON without launch commands", async () => {
127
187
  const tempDir = await makeTempDir();
128
188
  const manifestPath = path.join(tempDir, "workspace.json");