open-research-protocol 0.4.13 → 0.4.15
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/AGENT_INTEGRATION.md +50 -0
- package/README.md +273 -144
- package/bin/orp.js +14 -1
- package/cli/orp.py +14846 -9925
- package/docs/AGENT_LOOP.md +13 -0
- package/docs/AGENT_MODES.md +79 -0
- package/docs/CANONICAL_CLI_BOUNDARY.md +15 -0
- package/docs/EXCHANGE.md +94 -0
- package/docs/LAUNCH_KIT.md +107 -0
- package/docs/ORP_HOSTED_WORKSPACE_CONTRACT.md +295 -0
- package/docs/ORP_PUBLIC_LAUNCH_CHECKLIST.md +5 -0
- package/docs/START_HERE.md +567 -0
- package/package.json +5 -2
- package/packages/lifeops-orp/README.md +67 -0
- package/packages/lifeops-orp/package.json +48 -0
- package/packages/lifeops-orp/src/index.d.ts +106 -0
- package/packages/lifeops-orp/src/index.js +7 -0
- package/packages/lifeops-orp/src/mapping.js +309 -0
- package/packages/lifeops-orp/src/workspace.js +108 -0
- package/packages/lifeops-orp/test/orp.test.js +187 -0
- package/packages/orp-workspace-launcher/README.md +82 -0
- package/packages/orp-workspace-launcher/package.json +39 -0
- package/packages/orp-workspace-launcher/src/commands.js +77 -0
- package/packages/orp-workspace-launcher/src/core-plan.js +506 -0
- package/packages/orp-workspace-launcher/src/hosted-state.js +208 -0
- package/packages/orp-workspace-launcher/src/index.js +82 -0
- package/packages/orp-workspace-launcher/src/ledger.js +745 -0
- package/packages/orp-workspace-launcher/src/list.js +488 -0
- package/packages/orp-workspace-launcher/src/orp-command.js +126 -0
- package/packages/orp-workspace-launcher/src/orp.js +912 -0
- package/packages/orp-workspace-launcher/src/registry.js +558 -0
- package/packages/orp-workspace-launcher/src/slot.js +188 -0
- package/packages/orp-workspace-launcher/src/sync.js +363 -0
- package/packages/orp-workspace-launcher/src/tabs.js +166 -0
- package/packages/orp-workspace-launcher/test/commands.test.js +164 -0
- package/packages/orp-workspace-launcher/test/core-plan.test.js +253 -0
- package/packages/orp-workspace-launcher/test/fixtures/smoke-notes.txt +2 -0
- package/packages/orp-workspace-launcher/test/fixtures/workspace-manifest.json +17 -0
- package/packages/orp-workspace-launcher/test/ledger.test.js +244 -0
- package/packages/orp-workspace-launcher/test/list.test.js +299 -0
- package/packages/orp-workspace-launcher/test/orp-command.test.js +44 -0
- package/packages/orp-workspace-launcher/test/orp.test.js +224 -0
- package/packages/orp-workspace-launcher/test/tabs.test.js +168 -0
- package/scripts/orp-kernel-agent-pilot.py +10 -1
- package/scripts/orp-kernel-agent-replication.py +10 -1
- package/scripts/orp-kernel-canonical-continuation.py +10 -1
- package/scripts/orp-kernel-continuation-pilot.py +10 -1
- package/scripts/render-terminal-demo.py +416 -0
- package/spec/v1/exchange-report.schema.json +105 -0
- package/spec/v1/hosted-workspace-event.schema.json +102 -0
- package/spec/v1/hosted-workspace.schema.json +332 -0
- package/spec/v1/workspace.schema.json +108 -0
|
@@ -0,0 +1,244 @@
|
|
|
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
|
+
addTabToManifest,
|
|
9
|
+
normalizeWorkspaceManifest,
|
|
10
|
+
parseWorkspaceCreateArgs,
|
|
11
|
+
parseWorkspaceAddTabArgs,
|
|
12
|
+
parseWorkspaceRemoveTabArgs,
|
|
13
|
+
removeTabsFromManifest,
|
|
14
|
+
runWorkspaceCreate,
|
|
15
|
+
runWorkspaceAddTab,
|
|
16
|
+
runWorkspaceRemoveTab,
|
|
17
|
+
} from "../src/index.js";
|
|
18
|
+
|
|
19
|
+
async function makeTempDir() {
|
|
20
|
+
return fs.mkdtemp(path.join(os.tmpdir(), "orp-workspace-ledger-"));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function captureStdout(fn) {
|
|
24
|
+
const chunks = [];
|
|
25
|
+
const originalWrite = process.stdout.write;
|
|
26
|
+
process.stdout.write = (chunk, encoding, callback) => {
|
|
27
|
+
chunks.push(typeof chunk === "string" ? chunk : chunk.toString(encoding || "utf8"));
|
|
28
|
+
if (typeof callback === "function") {
|
|
29
|
+
callback();
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const code = await fn();
|
|
36
|
+
return {
|
|
37
|
+
code,
|
|
38
|
+
stdout: chunks.join(""),
|
|
39
|
+
};
|
|
40
|
+
} finally {
|
|
41
|
+
process.stdout.write = originalWrite;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function withTempConfigHome(fn) {
|
|
46
|
+
const tempDir = await makeTempDir();
|
|
47
|
+
const original = process.env.XDG_CONFIG_HOME;
|
|
48
|
+
process.env.XDG_CONFIG_HOME = tempDir;
|
|
49
|
+
try {
|
|
50
|
+
return await fn(tempDir);
|
|
51
|
+
} finally {
|
|
52
|
+
if (original == null) {
|
|
53
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
54
|
+
} else {
|
|
55
|
+
process.env.XDG_CONFIG_HOME = original;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function sampleManifest() {
|
|
61
|
+
return normalizeWorkspaceManifest({
|
|
62
|
+
version: "1",
|
|
63
|
+
workspaceId: "main-cody-1",
|
|
64
|
+
title: "main-cody-1",
|
|
65
|
+
tabs: [
|
|
66
|
+
{
|
|
67
|
+
title: "orp",
|
|
68
|
+
path: "/Volumes/Code_2TB/code/orp",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
title: "frg-site",
|
|
72
|
+
path: "/Volumes/Code_2TB/code/frg-site",
|
|
73
|
+
resumeCommand: "codex resume 019d348d-5031-78e1-9840-a66deaac33ae",
|
|
74
|
+
resumeTool: "codex",
|
|
75
|
+
resumeSessionId: "019d348d-5031-78e1-9840-a66deaac33ae",
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
test("parseWorkspaceAddTabArgs accepts explicit resume metadata", () => {
|
|
82
|
+
const parsed = parseWorkspaceAddTabArgs([
|
|
83
|
+
"main",
|
|
84
|
+
"--path",
|
|
85
|
+
"/Volumes/Code_2TB/code/new-project",
|
|
86
|
+
"--resume-tool",
|
|
87
|
+
"claude",
|
|
88
|
+
"--resume-session-id",
|
|
89
|
+
"claude-456",
|
|
90
|
+
"--json",
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
assert.equal(parsed.ideaId, "main");
|
|
94
|
+
assert.equal(parsed.path, "/Volumes/Code_2TB/code/new-project");
|
|
95
|
+
assert.equal(parsed.resumeTool, "claude");
|
|
96
|
+
assert.equal(parsed.resumeSessionId, "claude-456");
|
|
97
|
+
assert.equal(parsed.json, true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("parseWorkspaceCreateArgs validates slug titles and optional seed metadata", () => {
|
|
101
|
+
const parsed = parseWorkspaceCreateArgs([
|
|
102
|
+
"main-cody-1",
|
|
103
|
+
"--slot",
|
|
104
|
+
"main",
|
|
105
|
+
"--path",
|
|
106
|
+
"/Volumes/Code_2TB/code/orp",
|
|
107
|
+
"--resume-tool",
|
|
108
|
+
"claude",
|
|
109
|
+
"--resume-session-id",
|
|
110
|
+
"claude-456",
|
|
111
|
+
"--json",
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
assert.equal(parsed.title, "main-cody-1");
|
|
115
|
+
assert.equal(parsed.slotName, "main");
|
|
116
|
+
assert.equal(parsed.path, "/Volumes/Code_2TB/code/orp");
|
|
117
|
+
assert.equal(parsed.resumeTool, "claude");
|
|
118
|
+
assert.equal(parsed.resumeSessionId, "claude-456");
|
|
119
|
+
assert.equal(parsed.json, true);
|
|
120
|
+
assert.throws(() => parseWorkspaceCreateArgs(["Main Cody"]), /workspace title/);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("parseWorkspaceRemoveTabArgs requires a matching selector", () => {
|
|
124
|
+
assert.throws(
|
|
125
|
+
() => parseWorkspaceRemoveTabArgs(["main"]),
|
|
126
|
+
/Provide at least one selector/,
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("addTabToManifest canonicalizes Claude resume commands from tool plus session id", () => {
|
|
131
|
+
const result = addTabToManifest(sampleManifest(), {
|
|
132
|
+
path: "/Volumes/Code_2TB/code/anthropic-lab",
|
|
133
|
+
title: "anthropic-lab",
|
|
134
|
+
resumeTool: "claude",
|
|
135
|
+
resumeSessionId: "claude-456",
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
assert.equal(result.added, true);
|
|
139
|
+
assert.equal(result.manifest.tabs.length, 3);
|
|
140
|
+
assert.equal(result.manifest.tabs[2]?.path, "/Volumes/Code_2TB/code/anthropic-lab");
|
|
141
|
+
assert.equal(result.manifest.tabs[2]?.title, "anthropic-lab");
|
|
142
|
+
assert.equal(result.manifest.tabs[2]?.resumeCommand, "claude --resume claude-456");
|
|
143
|
+
assert.equal(result.manifest.tabs[2]?.resumeTool, "claude");
|
|
144
|
+
assert.equal(result.manifest.tabs[2]?.sessionId, "claude-456");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("removeTabsFromManifest can target a saved tab by path and resume session id", () => {
|
|
148
|
+
const result = removeTabsFromManifest(sampleManifest(), {
|
|
149
|
+
path: "/Volumes/Code_2TB/code/frg-site",
|
|
150
|
+
resumeSessionId: "019d348d-5031-78e1-9840-a66deaac33ae",
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
assert.equal(result.manifest.tabs.length, 1);
|
|
154
|
+
assert.equal(result.removedTabs.length, 1);
|
|
155
|
+
assert.equal(result.removedTabs[0]?.path, "/Volumes/Code_2TB/code/frg-site");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("runWorkspaceAddTab updates a local workspace manifest file", async () => {
|
|
159
|
+
await withTempConfigHome(async () => {
|
|
160
|
+
const tempDir = await makeTempDir();
|
|
161
|
+
const manifestPath = path.join(tempDir, "workspace.json");
|
|
162
|
+
await fs.writeFile(manifestPath, `${JSON.stringify(sampleManifest(), null, 2)}\n`, "utf8");
|
|
163
|
+
|
|
164
|
+
const { code, stdout } = await captureStdout(() =>
|
|
165
|
+
runWorkspaceAddTab([
|
|
166
|
+
"--workspace-file",
|
|
167
|
+
manifestPath,
|
|
168
|
+
"--path",
|
|
169
|
+
"/Volumes/Code_2TB/code/anthropic-lab",
|
|
170
|
+
"--resume-tool",
|
|
171
|
+
"claude",
|
|
172
|
+
"--resume-session-id",
|
|
173
|
+
"claude-456",
|
|
174
|
+
"--json",
|
|
175
|
+
]),
|
|
176
|
+
);
|
|
177
|
+
const payload = JSON.parse(stdout);
|
|
178
|
+
const saved = JSON.parse(await fs.readFile(manifestPath, "utf8"));
|
|
179
|
+
|
|
180
|
+
assert.equal(code, 0);
|
|
181
|
+
assert.equal(payload.action, "add-tab");
|
|
182
|
+
assert.equal(payload.tabCount, 3);
|
|
183
|
+
assert.equal(payload.tab.resumeCommand, "claude --resume claude-456");
|
|
184
|
+
assert.equal(saved.tabs.length, 3);
|
|
185
|
+
assert.equal(saved.tabs[2]?.resumeCommand, "claude --resume claude-456");
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("runWorkspaceRemoveTab updates a local workspace manifest file", async () => {
|
|
190
|
+
await withTempConfigHome(async () => {
|
|
191
|
+
const tempDir = await makeTempDir();
|
|
192
|
+
const manifestPath = path.join(tempDir, "workspace.json");
|
|
193
|
+
await fs.writeFile(manifestPath, `${JSON.stringify(sampleManifest(), null, 2)}\n`, "utf8");
|
|
194
|
+
|
|
195
|
+
const { code, stdout } = await captureStdout(() =>
|
|
196
|
+
runWorkspaceRemoveTab([
|
|
197
|
+
"--workspace-file",
|
|
198
|
+
manifestPath,
|
|
199
|
+
"--path",
|
|
200
|
+
"/Volumes/Code_2TB/code/frg-site",
|
|
201
|
+
"--resume-session-id",
|
|
202
|
+
"019d348d-5031-78e1-9840-a66deaac33ae",
|
|
203
|
+
"--json",
|
|
204
|
+
]),
|
|
205
|
+
);
|
|
206
|
+
const payload = JSON.parse(stdout);
|
|
207
|
+
const saved = JSON.parse(await fs.readFile(manifestPath, "utf8"));
|
|
208
|
+
|
|
209
|
+
assert.equal(code, 0);
|
|
210
|
+
assert.equal(payload.action, "remove-tab");
|
|
211
|
+
assert.equal(payload.tabCount, 1);
|
|
212
|
+
assert.equal(payload.removedTabs.length, 1);
|
|
213
|
+
assert.equal(saved.tabs.length, 1);
|
|
214
|
+
assert.equal(saved.tabs[0]?.path, "/Volumes/Code_2TB/code/orp");
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("runWorkspaceCreate creates a local managed workspace and auto-assigns main when it is the first one", async () => {
|
|
219
|
+
await withTempConfigHome(async (configHome) => {
|
|
220
|
+
const { code, stdout } = await captureStdout(() =>
|
|
221
|
+
runWorkspaceCreate([
|
|
222
|
+
"main-cody-1",
|
|
223
|
+
"--path",
|
|
224
|
+
"/Volumes/Code_2TB/code/orp",
|
|
225
|
+
"--resume-tool",
|
|
226
|
+
"claude",
|
|
227
|
+
"--resume-session-id",
|
|
228
|
+
"claude-456",
|
|
229
|
+
"--json",
|
|
230
|
+
]),
|
|
231
|
+
);
|
|
232
|
+
const payload = JSON.parse(stdout);
|
|
233
|
+
const saved = JSON.parse(await fs.readFile(payload.manifestPath, "utf8"));
|
|
234
|
+
const slots = JSON.parse(await fs.readFile(path.join(configHome, "orp", "workspace-slots.json"), "utf8"));
|
|
235
|
+
|
|
236
|
+
assert.equal(code, 0);
|
|
237
|
+
assert.equal(payload.action, "create");
|
|
238
|
+
assert.equal(payload.workspaceTitle, "main-cody-1");
|
|
239
|
+
assert.equal(payload.tabCount, 1);
|
|
240
|
+
assert.equal(saved.title, "main-cody-1");
|
|
241
|
+
assert.equal(saved.tabs[0]?.resumeCommand, "claude --resume claude-456");
|
|
242
|
+
assert.equal(slots.slots.main.title, "main-cody-1");
|
|
243
|
+
});
|
|
244
|
+
});
|
|
@@ -0,0 +1,299 @@
|
|
|
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
|
+
applyWorkspaceSlotsToInventory,
|
|
9
|
+
buildWorkspaceInventory,
|
|
10
|
+
cacheManagedWorkspaceManifest,
|
|
11
|
+
getManagedWorkspaceDir,
|
|
12
|
+
listTrackedWorkspaces,
|
|
13
|
+
loadWorkspaceSlots,
|
|
14
|
+
parseWorkspaceListArgs,
|
|
15
|
+
registerWorkspaceManifest,
|
|
16
|
+
setWorkspaceSlot,
|
|
17
|
+
summarizeTrackedWorkspaces,
|
|
18
|
+
summarizeWorkspaceInventory,
|
|
19
|
+
} from "../src/index.js";
|
|
20
|
+
|
|
21
|
+
async function makeTempDir() {
|
|
22
|
+
return fs.mkdtemp(path.join(os.tmpdir(), "orp-workspace-list-"));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test("parseWorkspaceListArgs accepts --json", () => {
|
|
26
|
+
const parsed = parseWorkspaceListArgs(["--json"]);
|
|
27
|
+
assert.equal(parsed.json, true);
|
|
28
|
+
assert.throws(() => parseWorkspaceListArgs(["--wat"]), /unexpected argument/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("registerWorkspaceManifest and listTrackedWorkspaces expose tracked saved resume commands", async () => {
|
|
32
|
+
const tempDir = await makeTempDir();
|
|
33
|
+
const env = {
|
|
34
|
+
...process.env,
|
|
35
|
+
XDG_CONFIG_HOME: path.join(tempDir, "config"),
|
|
36
|
+
};
|
|
37
|
+
const manifestPath = path.join(tempDir, "workspace.json");
|
|
38
|
+
const manifest = {
|
|
39
|
+
version: "1",
|
|
40
|
+
workspaceId: "orp-main",
|
|
41
|
+
title: "ORP Main",
|
|
42
|
+
capture: {
|
|
43
|
+
sourceApp: "iTerm",
|
|
44
|
+
mode: "watch",
|
|
45
|
+
host: "local-macbook",
|
|
46
|
+
windowId: 7,
|
|
47
|
+
windowIndex: 1,
|
|
48
|
+
tabCount: 2,
|
|
49
|
+
capturedAt: "2026-03-28T12:00:00.000Z",
|
|
50
|
+
trackingStartedAt: "2026-03-28T11:55:00.000Z",
|
|
51
|
+
pollSeconds: 2,
|
|
52
|
+
},
|
|
53
|
+
tabs: [
|
|
54
|
+
{
|
|
55
|
+
path: "/Volumes/Code_2TB/code/orp",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
title: "web",
|
|
59
|
+
path: "/Volumes/Code_2TB/code/orp-web-app",
|
|
60
|
+
resumeCommand: "claude resume claude-999",
|
|
61
|
+
resumeTool: "claude",
|
|
62
|
+
resumeSessionId: "claude-999",
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
await fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
68
|
+
const registration = await registerWorkspaceManifest(manifestPath, manifest, { env });
|
|
69
|
+
const result = await listTrackedWorkspaces({ env });
|
|
70
|
+
|
|
71
|
+
assert.match(registration.registryPath, /workspace-registry\.json$/);
|
|
72
|
+
assert.equal(result.workspaces.length, 1);
|
|
73
|
+
assert.deepEqual(result.workspaces[0], {
|
|
74
|
+
manifestPath: path.resolve(manifestPath),
|
|
75
|
+
workspaceId: "orp-main",
|
|
76
|
+
title: "ORP Main",
|
|
77
|
+
host: "local-macbook",
|
|
78
|
+
captureMode: "watch",
|
|
79
|
+
capturedAt: "2026-03-28T12:00:00.000Z",
|
|
80
|
+
trackingStartedAt: "2026-03-28T11:55:00.000Z",
|
|
81
|
+
windowId: 7,
|
|
82
|
+
windowIndex: 1,
|
|
83
|
+
tabCount: 2,
|
|
84
|
+
codexSessionCount: 1,
|
|
85
|
+
tmuxSessionCount: 0,
|
|
86
|
+
resumeSessions: [
|
|
87
|
+
{
|
|
88
|
+
title: "web",
|
|
89
|
+
path: "/Volumes/Code_2TB/code/orp-web-app",
|
|
90
|
+
resumeCommand: "claude resume claude-999",
|
|
91
|
+
resumeTool: "claude",
|
|
92
|
+
resumeSessionId: "claude-999",
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
registeredAt: registration.entry.registeredAt,
|
|
96
|
+
updatedAt: registration.entry.updatedAt,
|
|
97
|
+
status: "ok",
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const summary = summarizeTrackedWorkspaces(result);
|
|
101
|
+
assert.match(summary, /Local tracked workspaces: 1/);
|
|
102
|
+
assert.match(summary, /ORP Main \[orp-main\]/);
|
|
103
|
+
assert.match(summary, /Saved resume sessions: 1/);
|
|
104
|
+
assert.match(summary, /web: claude resume claude-999/);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("listTrackedWorkspaces retains missing manifests in the registry output", async () => {
|
|
108
|
+
const tempDir = await makeTempDir();
|
|
109
|
+
const env = {
|
|
110
|
+
...process.env,
|
|
111
|
+
XDG_CONFIG_HOME: path.join(tempDir, "config"),
|
|
112
|
+
};
|
|
113
|
+
const manifestPath = path.join(tempDir, "workspace.json");
|
|
114
|
+
const manifest = {
|
|
115
|
+
version: "1",
|
|
116
|
+
workspaceId: "orp-main",
|
|
117
|
+
tabs: [{ path: "/Volumes/Code_2TB/code/orp" }],
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
await fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
121
|
+
await registerWorkspaceManifest(manifestPath, manifest, { env });
|
|
122
|
+
await fs.unlink(manifestPath);
|
|
123
|
+
|
|
124
|
+
const result = await listTrackedWorkspaces({ env });
|
|
125
|
+
assert.equal(result.workspaces[0]?.status, "missing");
|
|
126
|
+
assert.equal(result.workspaces[0]?.error, "manifest file not found");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("summarizeTrackedWorkspaces explains how to start when nothing is registered", () => {
|
|
130
|
+
const summary = summarizeTrackedWorkspaces({
|
|
131
|
+
registryPath: "/tmp/workspace-registry.json",
|
|
132
|
+
workspaces: [],
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
assert.match(summary, /No local tracked workspaces yet/);
|
|
136
|
+
assert.match(summary, /orp workspace create main-cody-1/);
|
|
137
|
+
assert.match(summary, /orp workspace list/);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("cacheManagedWorkspaceManifest writes a managed local cache and registers it", async () => {
|
|
141
|
+
const tempDir = await makeTempDir();
|
|
142
|
+
const env = {
|
|
143
|
+
...process.env,
|
|
144
|
+
XDG_CONFIG_HOME: path.join(tempDir, "config"),
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const result = await cacheManagedWorkspaceManifest(
|
|
148
|
+
{
|
|
149
|
+
version: "1",
|
|
150
|
+
workspaceId: "idea-123",
|
|
151
|
+
title: "Main Cody 1",
|
|
152
|
+
tabs: [{ title: "orp", path: "/Volumes/Code_2TB/code/orp" }],
|
|
153
|
+
},
|
|
154
|
+
{ env },
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
assert.match(result.manifestPath, /workspaces\/idea-123-[a-f0-9]{8}\.json$/);
|
|
158
|
+
assert.equal(path.dirname(result.manifestPath), getManagedWorkspaceDir({ env }));
|
|
159
|
+
|
|
160
|
+
const listed = await listTrackedWorkspaces({ env });
|
|
161
|
+
assert.equal(listed.workspaces.length, 1);
|
|
162
|
+
assert.equal(listed.workspaces[0]?.workspaceId, "idea-123");
|
|
163
|
+
assert.equal(listed.workspaces[0]?.title, "Main Cody 1");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("buildWorkspaceInventory merges hosted and local workspace state", () => {
|
|
167
|
+
const result = buildWorkspaceInventory({
|
|
168
|
+
localResult: {
|
|
169
|
+
registryPath: "/tmp/workspace-registry.json",
|
|
170
|
+
workspaces: [
|
|
171
|
+
{
|
|
172
|
+
manifestPath: "/Users/example/.config/orp/workspaces/idea-123-deadbeef.json",
|
|
173
|
+
workspaceId: "idea-123",
|
|
174
|
+
title: "Main Cody 1",
|
|
175
|
+
tabCount: 2,
|
|
176
|
+
codexSessionCount: 1,
|
|
177
|
+
updatedAt: "2026-03-30T12:00:00.000Z",
|
|
178
|
+
status: "ok",
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
manifestPath: "/tmp/local-only.json",
|
|
182
|
+
workspaceId: "local-only",
|
|
183
|
+
title: "Local Only",
|
|
184
|
+
tabCount: 1,
|
|
185
|
+
codexSessionCount: 0,
|
|
186
|
+
updatedAt: "2026-03-29T12:00:00.000Z",
|
|
187
|
+
status: "ok",
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
hostedResult: {
|
|
192
|
+
source: "idea_bridge",
|
|
193
|
+
workspaces: [
|
|
194
|
+
{
|
|
195
|
+
workspace_id: "idea-123",
|
|
196
|
+
title: "Main Cody 1",
|
|
197
|
+
updatedAt: "2026-03-30T12:05:00.000Z",
|
|
198
|
+
linkedIdea: { ideaId: "ef86" },
|
|
199
|
+
metrics: { tabCount: 2 },
|
|
200
|
+
state: {
|
|
201
|
+
tabs: [
|
|
202
|
+
{ project_root: "/Volumes/Code_2TB/code/orp", codex_session_id: "abc-123" },
|
|
203
|
+
{ project_root: "/Volumes/Code_2TB/code/orp-web-app" },
|
|
204
|
+
],
|
|
205
|
+
},
|
|
206
|
+
source_kind: "idea_bridge",
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
assert.equal(result.workspaces.length, 2);
|
|
213
|
+
assert.equal(result.workspaces[0]?.workspaceId, "idea-123");
|
|
214
|
+
assert.equal(result.workspaces[0]?.availability, "hosted+local");
|
|
215
|
+
assert.equal(result.workspaces[0]?.syncStatus, "synced");
|
|
216
|
+
assert.equal(result.workspaces[1]?.workspaceId, "local-only");
|
|
217
|
+
assert.equal(result.workspaces[1]?.availability, "local");
|
|
218
|
+
|
|
219
|
+
const summary = summarizeWorkspaceInventory(result);
|
|
220
|
+
assert.match(summary, /Workspace inventory: 2/);
|
|
221
|
+
assert.match(summary, /Hosted available: 1/);
|
|
222
|
+
assert.match(summary, /Local available: 2/);
|
|
223
|
+
assert.match(summary, /Sync: synced/);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("applyWorkspaceSlotsToInventory annotates explicit and implicit slots", async () => {
|
|
227
|
+
const tempDir = await makeTempDir();
|
|
228
|
+
const env = {
|
|
229
|
+
...process.env,
|
|
230
|
+
XDG_CONFIG_HOME: path.join(tempDir, "config"),
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
await setWorkspaceSlot(
|
|
234
|
+
"offhand",
|
|
235
|
+
{
|
|
236
|
+
kind: "workspace-file",
|
|
237
|
+
workspaceId: "research-lab",
|
|
238
|
+
title: "research-lab",
|
|
239
|
+
manifestPath: "/tmp/research-lab.json",
|
|
240
|
+
},
|
|
241
|
+
{ env },
|
|
242
|
+
);
|
|
243
|
+
const { slots } = await loadWorkspaceSlots({ env });
|
|
244
|
+
|
|
245
|
+
const explicitResult = applyWorkspaceSlotsToInventory(
|
|
246
|
+
buildWorkspaceInventory({
|
|
247
|
+
localResult: {
|
|
248
|
+
registryPath: "/tmp/workspace-registry.json",
|
|
249
|
+
workspaces: [
|
|
250
|
+
{
|
|
251
|
+
manifestPath: "/tmp/research-lab.json",
|
|
252
|
+
workspaceId: "research-lab",
|
|
253
|
+
title: "research-lab",
|
|
254
|
+
tabCount: 1,
|
|
255
|
+
codexSessionCount: 0,
|
|
256
|
+
updatedAt: "2026-03-30T12:00:00.000Z",
|
|
257
|
+
status: "ok",
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
},
|
|
261
|
+
hostedResult: {
|
|
262
|
+
source: "idea_bridge",
|
|
263
|
+
workspaces: [],
|
|
264
|
+
},
|
|
265
|
+
}),
|
|
266
|
+
slots,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
assert.deepEqual(explicitResult.workspaces[0]?.slots, ["offhand", "main"]);
|
|
270
|
+
assert.equal(explicitResult.workspaces[0]?.implicitMain, true);
|
|
271
|
+
|
|
272
|
+
const implicitResult = applyWorkspaceSlotsToInventory(
|
|
273
|
+
buildWorkspaceInventory({
|
|
274
|
+
localResult: {
|
|
275
|
+
registryPath: "/tmp/workspace-registry.json",
|
|
276
|
+
workspaces: [
|
|
277
|
+
{
|
|
278
|
+
manifestPath: "/tmp/main-cody-1.json",
|
|
279
|
+
workspaceId: "main-cody-1",
|
|
280
|
+
title: "main-cody-1",
|
|
281
|
+
tabCount: 2,
|
|
282
|
+
codexSessionCount: 1,
|
|
283
|
+
updatedAt: "2026-03-30T12:00:00.000Z",
|
|
284
|
+
status: "ok",
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
},
|
|
288
|
+
hostedResult: {
|
|
289
|
+
source: "idea_bridge",
|
|
290
|
+
workspaces: [],
|
|
291
|
+
},
|
|
292
|
+
}),
|
|
293
|
+
{},
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
assert.deepEqual(implicitResult.workspaces[0]?.slots, ["main"]);
|
|
297
|
+
assert.equal(implicitResult.workspaces[0]?.implicitMain, true);
|
|
298
|
+
assert.match(summarizeWorkspaceInventory(implicitResult), /Slots: main \(main inferred because this is the only workspace\)/);
|
|
299
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { runOrpWorkspaceCommand } from "../src/index.js";
|
|
5
|
+
|
|
6
|
+
async function captureStdout(fn) {
|
|
7
|
+
const chunks = [];
|
|
8
|
+
const originalWrite = process.stdout.write;
|
|
9
|
+
process.stdout.write = (chunk, encoding, callback) => {
|
|
10
|
+
chunks.push(typeof chunk === "string" ? chunk : chunk.toString(encoding || "utf8"));
|
|
11
|
+
if (typeof callback === "function") {
|
|
12
|
+
callback();
|
|
13
|
+
}
|
|
14
|
+
return true;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const code = await fn();
|
|
19
|
+
return {
|
|
20
|
+
code,
|
|
21
|
+
stdout: chunks.join(""),
|
|
22
|
+
};
|
|
23
|
+
} finally {
|
|
24
|
+
process.stdout.write = originalWrite;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test("runOrpWorkspaceCommand shows the ledger-first help surface", async () => {
|
|
29
|
+
const { code, stdout } = await captureStdout(() => runOrpWorkspaceCommand(["-h"]));
|
|
30
|
+
|
|
31
|
+
assert.equal(code, 0);
|
|
32
|
+
assert.match(stdout, /orp workspace ledger <name-or-id>/);
|
|
33
|
+
assert.match(stdout, /orp workspace ledger add <name-or-id>/);
|
|
34
|
+
assert.match(stdout, /orp workspace ledger remove <name-or-id>/);
|
|
35
|
+
assert.match(stdout, /orp workspace tabs <name-or-id>/);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("runOrpWorkspaceCommand routes ledger help to the tabs help surface", async () => {
|
|
39
|
+
const { code, stdout } = await captureStdout(() => runOrpWorkspaceCommand(["ledger", "-h"]));
|
|
40
|
+
|
|
41
|
+
assert.equal(code, 0);
|
|
42
|
+
assert.match(stdout, /ORP workspace tabs/);
|
|
43
|
+
assert.match(stdout, /copyable and includes the saved `cd \.\.\. && resume \.\.\.` recovery command/);
|
|
44
|
+
});
|