open-research-protocol 0.4.27 → 0.4.29

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.
@@ -1,3 +1,13 @@
1
+ export {
2
+ applyCodexReconcilePlan,
3
+ buildCodexReconcilePlan,
4
+ buildCodexStatusReport,
5
+ parseCodexSessionMetaLine,
6
+ runOrpCodexCommand,
7
+ scanCodexSessions,
8
+ summarizeCodexReconcile,
9
+ summarizeCodexStatus,
10
+ } from "./codex.js";
1
11
  export {
2
12
  buildCloneCommand,
3
13
  buildDirectCommand,
@@ -945,7 +945,7 @@ function summarizeWorkspaceLedgerMutation(result) {
945
945
  return lines.join("\n");
946
946
  }
947
947
 
948
- async function runWorkspaceLedgerMutation(options, mutate, action) {
948
+ async function applyWorkspaceLedgerMutation(options, mutate, action) {
949
949
  const source = await loadWorkspaceSource(options);
950
950
  const parsed = parseWorkspaceSource(source);
951
951
  const manifest = normalizeEditableManifest(source, parsed);
@@ -974,6 +974,12 @@ async function runWorkspaceLedgerMutation(options, mutate, action) {
974
974
  manifest: finalManifest,
975
975
  };
976
976
 
977
+ return result;
978
+ }
979
+
980
+ async function runWorkspaceLedgerMutation(options, mutate, action) {
981
+ const result = await applyWorkspaceLedgerMutation(options, mutate, action);
982
+
977
983
  if (options.json) {
978
984
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
979
985
  return 0;
@@ -983,6 +989,10 @@ async function runWorkspaceLedgerMutation(options, mutate, action) {
983
989
  return 0;
984
990
  }
985
991
 
992
+ export async function applyWorkspaceAddTabOptions(options = {}) {
993
+ return applyWorkspaceLedgerMutation(options, addTabToManifest, "add-tab");
994
+ }
995
+
986
996
  export async function runWorkspaceAddTab(argv = process.argv.slice(2)) {
987
997
  const options = parseWorkspaceAddTabArgs(argv);
988
998
  if (options.help) {
@@ -0,0 +1,309 @@
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
+ applyCodexReconcilePlan,
9
+ buildCodexReconcilePlan,
10
+ buildCodexStatusReport,
11
+ parseCodexSessionMetaLine,
12
+ runOrpCodexCommand,
13
+ scanCodexSessions,
14
+ } from "../src/index.js";
15
+
16
+ async function makeTempDir() {
17
+ return fs.mkdtemp(path.join(os.tmpdir(), "orp-codex-"));
18
+ }
19
+
20
+ async function writeSession(codexHome, sessionId, cwd, timestamp, extraPayload = {}) {
21
+ const day = timestamp.slice(0, 10).split("-");
22
+ const sessionsDir = path.join(codexHome, "sessions", day[0], day[1], day[2]);
23
+ await fs.mkdir(sessionsDir, { recursive: true });
24
+ const filePath = path.join(sessionsDir, `rollout-${timestamp.replaceAll(":", "-")}-${sessionId}.jsonl`);
25
+ const row = {
26
+ timestamp,
27
+ type: "session_meta",
28
+ payload: {
29
+ id: sessionId,
30
+ timestamp,
31
+ cwd,
32
+ originator: "codex-tui",
33
+ cli_version: "0.125.0",
34
+ ...extraPayload,
35
+ },
36
+ };
37
+ await fs.writeFile(filePath, `${JSON.stringify(row)}\n`, "utf8");
38
+ const mtime = new Date(timestamp);
39
+ await fs.utimes(filePath, mtime, mtime);
40
+ return filePath;
41
+ }
42
+
43
+ async function writeSessionWithPrefix(codexHome, sessionId, cwd, timestamp) {
44
+ const filePath = await writeSession(codexHome, sessionId, cwd, timestamp);
45
+ const original = await fs.readFile(filePath, "utf8");
46
+ await fs.writeFile(filePath, `${JSON.stringify({ type: "response_item", payload: {} })}\n${original}`, "utf8");
47
+ const mtime = new Date(timestamp);
48
+ await fs.utimes(filePath, mtime, mtime);
49
+ return filePath;
50
+ }
51
+
52
+ async function writeWorkspaceManifest(filePath, tabs) {
53
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
54
+ await fs.writeFile(
55
+ filePath,
56
+ `${JSON.stringify(
57
+ {
58
+ version: "1",
59
+ workspaceId: "orp-main",
60
+ title: "ORP Main",
61
+ tabs,
62
+ },
63
+ null,
64
+ 2,
65
+ )}\n`,
66
+ "utf8",
67
+ );
68
+ }
69
+
70
+ test("parseCodexSessionMetaLine reads stable session metadata", () => {
71
+ const row = JSON.stringify({
72
+ timestamp: "2026-04-25T12:00:00Z",
73
+ type: "session_meta",
74
+ payload: {
75
+ id: "019dc2cb-d435-7072-bbfd-4ae4280474dd",
76
+ timestamp: "2026-04-25T12:00:00Z",
77
+ cwd: "/tmp/example",
78
+ originator: "codex-tui",
79
+ cli_version: "0.125.0",
80
+ },
81
+ });
82
+
83
+ const parsed = parseCodexSessionMetaLine(row, "/tmp/session.jsonl", { mtimeMs: 123 });
84
+ assert.equal(parsed.sessionId, "019dc2cb-d435-7072-bbfd-4ae4280474dd");
85
+ assert.equal(parsed.cwd, "/tmp/example");
86
+ assert.equal(parsed.cliVersion, "0.125.0");
87
+ assert.equal(parsed.updatedMs, 123);
88
+ assert.equal(parseCodexSessionMetaLine("not json", "/tmp/session.jsonl"), null);
89
+ });
90
+
91
+ test("scanCodexSessions ignores delegated sessions by default", async () => {
92
+ const tempDir = await makeTempDir();
93
+ const codexHome = path.join(tempDir, "codex-home");
94
+ await writeSession(
95
+ codexHome,
96
+ "019dc2cb-d435-7072-bbfd-4ae4280474aa",
97
+ path.join(tempDir, "repo"),
98
+ "2026-04-25T12:00:00Z",
99
+ );
100
+ await writeSession(
101
+ codexHome,
102
+ "019dc2cb-d435-7072-bbfd-4ae4280474bb",
103
+ path.join(tempDir, "repo"),
104
+ "2026-04-25T12:01:00Z",
105
+ { source: { subagent: { other: "guardian" } } },
106
+ );
107
+ await writeSession(
108
+ codexHome,
109
+ "019dc2cb-d435-7072-bbfd-4ae4280474cc",
110
+ path.join(tempDir, "repo"),
111
+ "2026-04-25T12:02:00Z",
112
+ { originator: "clawdad" },
113
+ );
114
+
115
+ const defaultSessions = await scanCodexSessions({ codexHome, sinceMs: 0 });
116
+ assert.deepEqual(
117
+ defaultSessions.map((session) => session.sessionId),
118
+ ["019dc2cb-d435-7072-bbfd-4ae4280474aa"],
119
+ );
120
+
121
+ const allSessions = await scanCodexSessions({ codexHome, sinceMs: 0, includeSubagents: true });
122
+ assert.deepEqual(
123
+ allSessions.map((session) => session.sessionId),
124
+ [
125
+ "019dc2cb-d435-7072-bbfd-4ae4280474cc",
126
+ "019dc2cb-d435-7072-bbfd-4ae4280474bb",
127
+ "019dc2cb-d435-7072-bbfd-4ae4280474aa",
128
+ ],
129
+ );
130
+ });
131
+
132
+ test("scanCodexSessions finds session metadata near the start of a rollout file", async () => {
133
+ const tempDir = await makeTempDir();
134
+ const codexHome = path.join(tempDir, "codex-home");
135
+ const repoRoot = path.join(tempDir, "repo");
136
+ await writeSessionWithPrefix(
137
+ codexHome,
138
+ "019dc2cb-d435-7072-bbfd-4ae4280474dd",
139
+ repoRoot,
140
+ "2026-04-25T12:00:00Z",
141
+ );
142
+
143
+ const sessions = await scanCodexSessions({ codexHome, sinceMs: 0 });
144
+ assert.equal(sessions.length, 1);
145
+ assert.equal(sessions[0].sessionId, "019dc2cb-d435-7072-bbfd-4ae4280474dd");
146
+ });
147
+
148
+ test("buildCodexStatusReport marks a tracked repo stale when local Codex metadata is newer", async () => {
149
+ const tempDir = await makeTempDir();
150
+ const repoRoot = path.join(tempDir, "repo");
151
+ const codexHome = path.join(tempDir, "codex-home");
152
+ const workspaceFile = path.join(tempDir, "workspace.json");
153
+ await fs.mkdir(repoRoot, { recursive: true });
154
+ await writeWorkspaceManifest(workspaceFile, [
155
+ {
156
+ title: "repo",
157
+ path: repoRoot,
158
+ resumeTool: "codex",
159
+ resumeSessionId: "019dc2cb-d435-7072-bbfd-4ae428047401",
160
+ },
161
+ ]);
162
+ await writeSession(codexHome, "019dc2cb-d435-7072-bbfd-4ae428047402", repoRoot, "2026-04-25T12:00:00Z");
163
+
164
+ const report = await buildCodexStatusReport({
165
+ workspaceFile,
166
+ codexHome,
167
+ path: repoRoot,
168
+ sinceDays: 0,
169
+ });
170
+
171
+ assert.equal(report.status, "stale");
172
+ assert.equal(report.trackedTab.codexSessionId, "019dc2cb-d435-7072-bbfd-4ae428047401");
173
+ assert.equal(report.latestCodexSession.sessionId, "019dc2cb-d435-7072-bbfd-4ae428047402");
174
+ });
175
+
176
+ test("Codex reconcile updates tracked workspace tabs without appending by default", async () => {
177
+ const tempDir = await makeTempDir();
178
+ const repoRoot = path.join(tempDir, "repo");
179
+ const codexHome = path.join(tempDir, "codex-home");
180
+ const workspaceFile = path.join(tempDir, "workspace.json");
181
+ await fs.mkdir(repoRoot, { recursive: true });
182
+ await writeWorkspaceManifest(workspaceFile, [
183
+ {
184
+ title: "repo",
185
+ path: repoRoot,
186
+ resumeTool: "codex",
187
+ resumeSessionId: "019dc2cb-d435-7072-bbfd-4ae428047411",
188
+ },
189
+ ]);
190
+ await writeSession(codexHome, "019dc2cb-d435-7072-bbfd-4ae428047412", repoRoot, "2026-04-25T12:00:00Z");
191
+
192
+ const plan = await buildCodexReconcilePlan({
193
+ workspaceFile,
194
+ codexHome,
195
+ sinceDays: 0,
196
+ });
197
+ assert.equal(plan.actionCount, 1);
198
+ assert.equal(plan.actions[0].action, "update");
199
+
200
+ const applied = await applyCodexReconcilePlan(plan, {
201
+ workspaceFile,
202
+ codexHome,
203
+ });
204
+ assert.equal(applied.actions[0].applied, true);
205
+
206
+ const manifest = JSON.parse(await fs.readFile(workspaceFile, "utf8"));
207
+ assert.equal(manifest.tabs.length, 1);
208
+ assert.equal(manifest.tabs[0].resumeSessionId, "019dc2cb-d435-7072-bbfd-4ae428047412");
209
+ assert.equal(manifest.tabs[0].codexSessionId, "019dc2cb-d435-7072-bbfd-4ae428047412");
210
+ });
211
+
212
+ test("bare orp codex routes to start", async () => {
213
+ const tempDir = await makeTempDir();
214
+ const codexHome = path.join(tempDir, "codex-home");
215
+ const fakeCodex = path.join(tempDir, "fake-codex");
216
+ await fs.writeFile(fakeCodex, "#!/bin/sh\nexit 0\n", "utf8");
217
+ await fs.chmod(fakeCodex, 0o755);
218
+
219
+ const originalWrite = process.stderr.write;
220
+ let stderr = "";
221
+ process.stderr.write = (chunk) => {
222
+ stderr += String(chunk);
223
+ return true;
224
+ };
225
+ try {
226
+ const code = await runOrpCodexCommand([
227
+ "--path",
228
+ tempDir,
229
+ "--codex-home",
230
+ codexHome,
231
+ "--codex-bin",
232
+ fakeCodex,
233
+ "--watch-timeout-ms",
234
+ "1",
235
+ "--search",
236
+ ]);
237
+ assert.equal(code, 0);
238
+ assert.match(stderr, /Fallback: orp workspace add-tab main --here --current-codex/);
239
+ } finally {
240
+ process.stderr.write = originalWrite;
241
+ }
242
+ });
243
+
244
+ test("bare orp codex saves the new session when Codex writes metadata", async () => {
245
+ const tempDir = await makeTempDir();
246
+ const repoRoot = path.join(tempDir, "repo");
247
+ const codexHome = path.join(tempDir, "codex-home");
248
+ const workspaceFile = path.join(tempDir, "workspace.json");
249
+ const fakeCodex = path.join(tempDir, "fake-codex.js");
250
+ const sessionId = "019dc2cb-d435-7072-bbfd-4ae4280474ee";
251
+ await fs.mkdir(repoRoot, { recursive: true });
252
+ await writeWorkspaceManifest(workspaceFile, []);
253
+ await fs.writeFile(
254
+ fakeCodex,
255
+ `#!/usr/bin/env node
256
+ const fs = require("node:fs");
257
+ const path = require("node:path");
258
+ if (!process.argv.includes("--search")) process.exit(7);
259
+ const codexHome = ${JSON.stringify(codexHome)};
260
+ const repoRoot = ${JSON.stringify(repoRoot)};
261
+ const sessionId = ${JSON.stringify(sessionId)};
262
+ const timestamp = new Date().toISOString();
263
+ const dir = path.join(codexHome, "sessions", "2026", "04", "25");
264
+ fs.mkdirSync(dir, { recursive: true });
265
+ fs.writeFileSync(
266
+ path.join(dir, "rollout-2026-04-25T12-00-00Z-" + sessionId + ".jsonl"),
267
+ JSON.stringify({
268
+ timestamp,
269
+ type: "session_meta",
270
+ payload: { id: sessionId, timestamp, cwd: repoRoot, originator: "codex-tui" },
271
+ }) + "\\n",
272
+ );
273
+ `,
274
+ "utf8",
275
+ );
276
+ await fs.chmod(fakeCodex, 0o755);
277
+
278
+ const originalWrite = process.stderr.write;
279
+ let stderr = "";
280
+ process.stderr.write = (chunk) => {
281
+ stderr += String(chunk);
282
+ return true;
283
+ };
284
+ try {
285
+ const code = await runOrpCodexCommand([
286
+ "--path",
287
+ repoRoot,
288
+ "--workspace-file",
289
+ workspaceFile,
290
+ "--codex-home",
291
+ codexHome,
292
+ "--codex-bin",
293
+ fakeCodex,
294
+ "--watch-timeout-ms",
295
+ "2000",
296
+ "--search",
297
+ ]);
298
+ assert.equal(code, 0);
299
+ assert.match(stderr, new RegExp(`ORP saved Codex session for .*: codex resume ${sessionId}`));
300
+ } finally {
301
+ process.stderr.write = originalWrite;
302
+ }
303
+
304
+ const manifest = JSON.parse(await fs.readFile(workspaceFile, "utf8"));
305
+ assert.equal(manifest.tabs.length, 1);
306
+ assert.equal(manifest.tabs[0].path, repoRoot);
307
+ assert.equal(manifest.tabs[0].resumeTool, "codex");
308
+ assert.equal(manifest.tabs[0].resumeSessionId, sessionId);
309
+ });