open-research-protocol 0.4.24 → 0.4.26
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/CHANGELOG.md +456 -0
- package/README.md +47 -13
- package/cli/orp.py +2998 -70
- package/docs/AGENT_RUNTIME_BORROWING_NOTES.md +68 -0
- package/docs/RESEARCH_COUNCIL.md +123 -0
- package/docs/START_HERE.md +4 -0
- package/package.json +2 -1
- package/packages/orp-workspace-launcher/src/index.js +3 -0
- package/packages/orp-workspace-launcher/src/ledger.js +192 -33
- package/packages/orp-workspace-launcher/src/orp.js +61 -1
- package/packages/orp-workspace-launcher/src/tabs.js +147 -4
- package/packages/orp-workspace-launcher/test/ledger.test.js +226 -0
- package/packages/orp-workspace-launcher/test/tabs.test.js +60 -0
- package/scripts/orp-mcp +205 -0
- package/spec/v1/project-context.schema.json +223 -0
- package/spec/v1/research-run.schema.json +245 -0
- package/cli/__pycache__/orp.cpython-311.pyc +0 -0
- package/scripts/__pycache__/orp-kernel-agent-pilot.cpython-311.pyc +0 -0
- package/scripts/__pycache__/orp-kernel-agent-replication.cpython-311.pyc +0 -0
- package/scripts/__pycache__/orp-kernel-benchmark.cpython-311.pyc +0 -0
- package/scripts/__pycache__/orp-kernel-canonical-continuation.cpython-311.pyc +0 -0
- package/scripts/__pycache__/orp-kernel-continuation-pilot.cpython-311.pyc +0 -0
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
1
4
|
import process from "node:process";
|
|
2
5
|
|
|
3
6
|
import {
|
|
@@ -12,6 +15,144 @@ import {
|
|
|
12
15
|
} from "./core-plan.js";
|
|
13
16
|
import { loadWorkspaceSource } from "./orp.js";
|
|
14
17
|
|
|
18
|
+
const SESSION_ID_PATTERN = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
|
|
19
|
+
|
|
20
|
+
function normalizeOptionalString(value) {
|
|
21
|
+
if (value == null) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const trimmed = String(value).trim();
|
|
25
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveCodexHome(options = {}) {
|
|
29
|
+
return (
|
|
30
|
+
normalizeOptionalString(options.codexHome) ||
|
|
31
|
+
normalizeOptionalString(process.env.CODEX_HOME) ||
|
|
32
|
+
path.join(os.homedir(), ".codex")
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function collectCodexSessionIds(tabs = []) {
|
|
37
|
+
return new Set(
|
|
38
|
+
tabs
|
|
39
|
+
.filter((tab) => tab.resumeTool === "codex")
|
|
40
|
+
.map((tab) => normalizeOptionalString(tab.sessionId))
|
|
41
|
+
.map((sessionId) => sessionId?.toLowerCase())
|
|
42
|
+
.filter(Boolean),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function sessionIdsInFilename(filename, wantedSessionIds) {
|
|
47
|
+
const matches = new Set();
|
|
48
|
+
for (const match of filename.matchAll(SESSION_ID_PATTERN)) {
|
|
49
|
+
const sessionId = match[0].toLowerCase();
|
|
50
|
+
if (wantedSessionIds.has(sessionId)) {
|
|
51
|
+
matches.add(sessionId);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (matches.size > 0) {
|
|
56
|
+
return matches;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const sessionId of wantedSessionIds) {
|
|
60
|
+
if (filename.includes(sessionId)) {
|
|
61
|
+
matches.add(sessionId);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return matches;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function rememberActivity(activityBySessionId, sessionId, filePath, stat) {
|
|
68
|
+
const current = activityBySessionId.get(sessionId);
|
|
69
|
+
if (!current || stat.mtimeMs > current.mtimeMs) {
|
|
70
|
+
activityBySessionId.set(sessionId, {
|
|
71
|
+
mtimeMs: stat.mtimeMs,
|
|
72
|
+
filePath,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function scanActivityDirectory(rootDir, wantedSessionIds, activityBySessionId) {
|
|
78
|
+
let entries;
|
|
79
|
+
try {
|
|
80
|
+
entries = fs.readdirSync(rootDir, { withFileTypes: true });
|
|
81
|
+
} catch {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
const entryPath = path.join(rootDir, entry.name);
|
|
87
|
+
if (entry.isDirectory()) {
|
|
88
|
+
scanActivityDirectory(entryPath, wantedSessionIds, activityBySessionId);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (!entry.isFile()) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const matchingSessionIds = sessionIdsInFilename(entry.name, wantedSessionIds);
|
|
96
|
+
if (matchingSessionIds.size === 0) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let stat;
|
|
101
|
+
try {
|
|
102
|
+
stat = fs.statSync(entryPath);
|
|
103
|
+
} catch {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
for (const sessionId of matchingSessionIds) {
|
|
107
|
+
rememberActivity(activityBySessionId, sessionId, entryPath, stat);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildCodexActivityIndex(tabs = [], options = {}) {
|
|
113
|
+
const wantedSessionIds = collectCodexSessionIds(tabs);
|
|
114
|
+
if (wantedSessionIds.size === 0) {
|
|
115
|
+
return new Map();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const activityBySessionId = new Map();
|
|
119
|
+
const codexHome = resolveCodexHome(options);
|
|
120
|
+
scanActivityDirectory(path.join(codexHome, "sessions"), wantedSessionIds, activityBySessionId);
|
|
121
|
+
scanActivityDirectory(path.join(codexHome, "shell_snapshots"), wantedSessionIds, activityBySessionId);
|
|
122
|
+
return activityBySessionId;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function orderTabsByRecentActivity(tabs = [], options = {}) {
|
|
126
|
+
const activityBySessionId = buildCodexActivityIndex(tabs, options);
|
|
127
|
+
const rankedTabs = tabs.map((tab, originalIndex) => {
|
|
128
|
+
const sessionActivity =
|
|
129
|
+
tab.resumeTool === "codex" && tab.sessionId ? activityBySessionId.get(String(tab.sessionId).toLowerCase()) : null;
|
|
130
|
+
return {
|
|
131
|
+
tab,
|
|
132
|
+
originalIndex,
|
|
133
|
+
activityMs: sessionActivity?.mtimeMs || 0,
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const projectActivity = new Map();
|
|
138
|
+
for (const ranked of rankedTabs) {
|
|
139
|
+
const current = projectActivity.get(ranked.tab.path) || 0;
|
|
140
|
+
projectActivity.set(ranked.tab.path, Math.max(current, ranked.activityMs));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return rankedTabs
|
|
144
|
+
.sort((left, right) => {
|
|
145
|
+
const leftProjectActivity = projectActivity.get(left.tab.path) || 0;
|
|
146
|
+
const rightProjectActivity = projectActivity.get(right.tab.path) || 0;
|
|
147
|
+
return (
|
|
148
|
+
rightProjectActivity - leftProjectActivity ||
|
|
149
|
+
right.activityMs - left.activityMs ||
|
|
150
|
+
left.originalIndex - right.originalIndex
|
|
151
|
+
);
|
|
152
|
+
})
|
|
153
|
+
.map((ranked) => ranked.tab);
|
|
154
|
+
}
|
|
155
|
+
|
|
15
156
|
export function parseWorkspaceTabsArgs(argv = []) {
|
|
16
157
|
const options = {
|
|
17
158
|
json: false,
|
|
@@ -64,7 +205,8 @@ export function buildWorkspaceTabsReport(source, parsed, options = {}) {
|
|
|
64
205
|
tmux: false,
|
|
65
206
|
resume: true,
|
|
66
207
|
});
|
|
67
|
-
const
|
|
208
|
+
const orderedLaunchTabs = orderTabsByRecentActivity(launchTabs, options);
|
|
209
|
+
const projectGroups = buildWorkspaceProjectGroups(orderedLaunchTabs);
|
|
68
210
|
|
|
69
211
|
return {
|
|
70
212
|
sourceType: source.sourceType,
|
|
@@ -73,7 +215,7 @@ export function buildWorkspaceTabsReport(source, parsed, options = {}) {
|
|
|
73
215
|
workspaceId: deriveWorkspaceId(source, parsed),
|
|
74
216
|
machine: parsed.manifest?.machine || null,
|
|
75
217
|
parseMode: parsed.parseMode,
|
|
76
|
-
tabCount:
|
|
218
|
+
tabCount: orderedLaunchTabs.length,
|
|
77
219
|
projectCount: projectGroups.length,
|
|
78
220
|
skippedCount: parsed.skipped.length,
|
|
79
221
|
projects: projectGroups.map((project) => ({
|
|
@@ -92,7 +234,7 @@ export function buildWorkspaceTabsReport(source, parsed, options = {}) {
|
|
|
92
234
|
),
|
|
93
235
|
})),
|
|
94
236
|
})),
|
|
95
|
-
tabs:
|
|
237
|
+
tabs: orderedLaunchTabs.map((tab, index) => ({
|
|
96
238
|
index: index + 1,
|
|
97
239
|
title: tab.title,
|
|
98
240
|
path: tab.path,
|
|
@@ -191,7 +333,8 @@ Options:
|
|
|
191
333
|
-h, --help Show this help text
|
|
192
334
|
|
|
193
335
|
Notes:
|
|
194
|
-
- This shows
|
|
336
|
+
- This shows activity-ranked saved tabs plus any stored local path, remote repo, bootstrap command, and \`codex resume ...\` / \`claude --resume ...\` metadata.
|
|
337
|
+
- Codex-backed tabs are ranked by the latest matching local session or shell snapshot activity; ties keep the saved ledger order.
|
|
195
338
|
- JSON output includes grouped \`projects[].sessions[]\` so duplicate project paths can be reviewed and sunset together.
|
|
196
339
|
- The human-readable \`resume:\` line is already copyable and includes the saved \`cd ... && resume ...\` recovery command.
|
|
197
340
|
- When a tab also has \`remote:\` or \`setup:\` lines, those are the portable cross-machine clues for cloning and preparing the repo on another rig.
|
|
@@ -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");
|