open-research-protocol 0.4.14 → 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.
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,166 @@
1
+ import process from "node:process";
2
+
3
+ import { buildDirectCommand, buildLaunchPlan, deriveWorkspaceId, getResumeCommand, parseWorkspaceSource } from "./core-plan.js";
4
+ import { loadWorkspaceSource } from "./orp.js";
5
+
6
+ export function parseWorkspaceTabsArgs(argv = []) {
7
+ const options = {
8
+ json: false,
9
+ };
10
+
11
+ for (let index = 0; index < argv.length; index += 1) {
12
+ const arg = argv[index];
13
+
14
+ if (arg === "-h" || arg === "--help") {
15
+ options.help = true;
16
+ continue;
17
+ }
18
+ if (arg === "--json") {
19
+ options.json = true;
20
+ continue;
21
+ }
22
+ if (arg.startsWith("--")) {
23
+ const next = argv[index + 1];
24
+ if (next == null || next.startsWith("--")) {
25
+ throw new Error(`missing value for ${arg}`);
26
+ }
27
+ if (arg === "--notes-file") {
28
+ options.notesFile = next;
29
+ } else if (arg === "--hosted-workspace-id") {
30
+ options.hostedWorkspaceId = next;
31
+ } else if (arg === "--workspace-file") {
32
+ options.workspaceFile = next;
33
+ } else if (arg === "--base-url") {
34
+ options.baseUrl = next;
35
+ } else if (arg === "--orp-command") {
36
+ options.orpCommand = next;
37
+ } else {
38
+ throw new Error(`unknown option: ${arg}`);
39
+ }
40
+ index += 1;
41
+ continue;
42
+ }
43
+
44
+ if (options.ideaId) {
45
+ throw new Error(`unexpected argument: ${arg}`);
46
+ }
47
+ options.ideaId = arg;
48
+ }
49
+
50
+ return options;
51
+ }
52
+
53
+ export function buildWorkspaceTabsReport(source, parsed, options = {}) {
54
+ const launchTabs = buildLaunchPlan(parsed.entries, {
55
+ tmux: false,
56
+ resume: true,
57
+ });
58
+
59
+ return {
60
+ sourceType: source.sourceType,
61
+ sourceLabel: source.sourceLabel,
62
+ title: parsed.manifest?.title || source.title,
63
+ workspaceId: deriveWorkspaceId(source, parsed),
64
+ parseMode: parsed.parseMode,
65
+ tabCount: launchTabs.length,
66
+ skippedCount: parsed.skipped.length,
67
+ tabs: launchTabs.map((tab, index) => ({
68
+ index: index + 1,
69
+ title: tab.title,
70
+ path: tab.path,
71
+ resumeCommand: getResumeCommand(tab),
72
+ restartCommand: buildDirectCommand(
73
+ {
74
+ path: tab.path,
75
+ resumeCommand: tab.resumeCommand || null,
76
+ resumeTool: tab.resumeTool || null,
77
+ resumeSessionId: tab.sessionId || null,
78
+ sessionId: tab.sessionId || null,
79
+ },
80
+ { resume: true },
81
+ ),
82
+ resumeTool: tab.resumeTool || null,
83
+ resumeSessionId: tab.sessionId || null,
84
+ codexSessionId: tab.resumeTool === "codex" ? tab.sessionId || null : null,
85
+ claudeSessionId: tab.resumeTool === "claude" ? tab.sessionId || null : null,
86
+ })),
87
+ skipped: parsed.skipped,
88
+ };
89
+ }
90
+
91
+ export function summarizeWorkspaceTabs(report) {
92
+ const lines = [
93
+ `Source: ${report.sourceLabel}`,
94
+ `Workspace ID: ${report.workspaceId}`,
95
+ `Saved tabs: ${report.tabCount}`,
96
+ `Parse mode: ${report.parseMode}`,
97
+ "",
98
+ ];
99
+
100
+ for (const tab of report.tabs) {
101
+ lines.push(`${String(tab.index).padStart(2, "0")}. ${tab.title}`);
102
+ lines.push(` path: ${tab.path}`);
103
+ if (tab.resumeCommand) {
104
+ lines.push(` resume: ${tab.restartCommand}`);
105
+ }
106
+ }
107
+
108
+ if (report.skipped.length > 0) {
109
+ lines.push("");
110
+ lines.push("Skipped lines:");
111
+ for (const skipped of report.skipped) {
112
+ lines.push(` line ${skipped.lineNumber}: ${skipped.rawLine}`);
113
+ }
114
+ }
115
+
116
+ return lines.join("\n");
117
+ }
118
+
119
+ function printWorkspaceTabsHelp() {
120
+ console.log(`ORP workspace tabs
121
+
122
+ Usage:
123
+ orp workspace tabs <name-or-id> [--json]
124
+ orp workspace tabs --hosted-workspace-id <workspace-id> [--json]
125
+ orp workspace tabs --notes-file <path> [--json]
126
+ orp workspace tabs --workspace-file <path> [--json]
127
+
128
+ Options:
129
+ --json Print saved tab metadata as JSON
130
+ --hosted-workspace-id <id> Read a first-class hosted workspace instead of an idea
131
+ --notes-file <path> Read a local notes file instead of ORP
132
+ --workspace-file <path> Read a structured workspace manifest JSON file
133
+ --base-url <url> Override the ORP hosted base URL
134
+ --orp-command <cmd> Override the ORP CLI executable used for hosted fetches
135
+ -h, --help Show this help text
136
+
137
+ Notes:
138
+ - This shows the saved tab order plus any stored \`codex resume ...\` or \`claude --resume ...\` command metadata.
139
+ - The human-readable \`resume:\` line is already copyable and includes the saved \`cd ... && resume ...\` recovery command.
140
+ - The selector can be \`main\`, \`offhand\`, a hosted idea id, a hosted workspace id, a local workspace id, or a saved workspace title/slug.
141
+ `);
142
+ }
143
+
144
+ export async function runWorkspaceTabs(argv = process.argv.slice(2)) {
145
+ const options = parseWorkspaceTabsArgs(argv);
146
+ if (options.help) {
147
+ printWorkspaceTabsHelp();
148
+ return 0;
149
+ }
150
+
151
+ const source = await loadWorkspaceSource(options);
152
+ const parsed = parseWorkspaceSource(source);
153
+ const report = buildWorkspaceTabsReport(source, parsed, options);
154
+
155
+ if (report.tabCount === 0) {
156
+ throw new Error("No saved tabs were found in the provided workspace source.");
157
+ }
158
+
159
+ if (options.json) {
160
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
161
+ return 0;
162
+ }
163
+
164
+ process.stdout.write(`${summarizeWorkspaceTabs(report)}\n`);
165
+ return 0;
166
+ }
@@ -0,0 +1,164 @@
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
+ buildWorkspaceCommandsReport,
9
+ parseWorkspaceCommandsArgs,
10
+ parseWorkspaceSource,
11
+ runWorkspaceCommands,
12
+ } from "../src/index.js";
13
+
14
+ async function makeTempDir() {
15
+ return fs.mkdtemp(path.join(os.tmpdir(), "orp-workspace-commands-"));
16
+ }
17
+
18
+ async function captureStdout(fn) {
19
+ const chunks = [];
20
+ const originalWrite = process.stdout.write;
21
+ process.stdout.write = (chunk, encoding, callback) => {
22
+ chunks.push(typeof chunk === "string" ? chunk : chunk.toString(encoding || "utf8"));
23
+ if (typeof callback === "function") {
24
+ callback();
25
+ }
26
+ return true;
27
+ };
28
+
29
+ try {
30
+ const code = await fn();
31
+ return {
32
+ code,
33
+ stdout: chunks.join(""),
34
+ };
35
+ } finally {
36
+ process.stdout.write = originalWrite;
37
+ }
38
+ }
39
+
40
+ test("parseWorkspaceCommandsArgs accepts JSON and workspace selectors", () => {
41
+ const parsed = parseWorkspaceCommandsArgs([
42
+ "--workspace-file",
43
+ "/tmp/workspace.json",
44
+ "--json",
45
+ ]);
46
+
47
+ assert.equal(parsed.workspaceFile, "/tmp/workspace.json");
48
+ assert.equal(parsed.json, true);
49
+ assert.throws(() => parseWorkspaceCommandsArgs(["idea-123", "idea-456"]), /unexpected argument/);
50
+ assert.throws(() => parseWorkspaceCommandsArgs(["--wat"]), /missing value|unknown option/);
51
+ });
52
+
53
+ test("buildWorkspaceCommandsReport exposes direct restart commands and exact saved resume commands", () => {
54
+ const parsed = parseWorkspaceSource({
55
+ sourceType: "hosted-idea",
56
+ sourceLabel: "Workspace idea",
57
+ title: "Workspace idea",
58
+ notes: `
59
+ /Volumes/Code_2TB/code/collaboration: codex resume abc-123
60
+ /Volumes/Code_2TB/code/anthropic-lab: claude resume claude-456
61
+ /Volumes/Code_2TB/code/collaboration
62
+ `,
63
+ });
64
+
65
+ const report = buildWorkspaceCommandsReport(
66
+ {
67
+ sourceType: "hosted-idea",
68
+ sourceLabel: "Workspace idea",
69
+ title: "Workspace idea",
70
+ },
71
+ parsed,
72
+ );
73
+
74
+ assert.equal(report.commandCount, 3);
75
+ assert.equal(report.tabs[0]?.resumeCommand, "codex resume abc-123");
76
+ assert.equal(report.tabs[0]?.restartCommand, "cd '/Volumes/Code_2TB/code/collaboration' && codex resume abc-123");
77
+ assert.equal(report.tabs[1]?.resumeCommand, "claude resume claude-456");
78
+ assert.equal(
79
+ report.tabs[1]?.restartCommand,
80
+ "cd '/Volumes/Code_2TB/code/anthropic-lab' && claude resume claude-456",
81
+ );
82
+ assert.equal(report.tabs[2]?.restartCommand, "cd '/Volumes/Code_2TB/code/collaboration'");
83
+ });
84
+
85
+ test("runWorkspaceCommands prints JSON with copyable commands", async () => {
86
+ const tempDir = await makeTempDir();
87
+ const manifestPath = path.join(tempDir, "workspace.json");
88
+ await fs.writeFile(
89
+ manifestPath,
90
+ `${JSON.stringify(
91
+ {
92
+ version: "1",
93
+ workspaceId: "orp-main",
94
+ title: "ORP Main",
95
+ tabs: [
96
+ {
97
+ title: "orp",
98
+ path: "/Volumes/Code_2TB/code/orp",
99
+ resumeCommand: "claude resume claude-999",
100
+ resumeTool: "claude",
101
+ resumeSessionId: "claude-999",
102
+ },
103
+ {
104
+ title: "web",
105
+ path: "/Volumes/Code_2TB/code/orp-web-app",
106
+ },
107
+ ],
108
+ },
109
+ null,
110
+ 2,
111
+ )}\n`,
112
+ "utf8",
113
+ );
114
+
115
+ const { code, stdout } = await captureStdout(() =>
116
+ runWorkspaceCommands(["--workspace-file", manifestPath, "--json"]),
117
+ );
118
+ const parsed = JSON.parse(stdout);
119
+
120
+ assert.equal(code, 0);
121
+ assert.equal(parsed.workspaceId, "orp-main");
122
+ assert.equal(parsed.commandCount, 2);
123
+ assert.equal(parsed.tabs[0]?.resumeCommand, "claude resume claude-999");
124
+ assert.equal(parsed.tabs[0]?.claudeSessionId, "claude-999");
125
+ assert.equal(parsed.tabs[0]?.restartCommand, "cd '/Volumes/Code_2TB/code/orp' && claude resume claude-999");
126
+ assert.equal(parsed.tabs[1]?.restartCommand, "cd '/Volumes/Code_2TB/code/orp-web-app'");
127
+ });
128
+
129
+ test("buildWorkspaceCommandsReport canonicalizes Claude restart commands from tool and session metadata", () => {
130
+ const parsed = parseWorkspaceSource({
131
+ sourceType: "workspace-file",
132
+ sourceLabel: "/tmp/workspace.json",
133
+ title: "workspace",
134
+ workspaceManifest: {
135
+ version: "1",
136
+ workspaceId: "orp-main",
137
+ tabs: [
138
+ {
139
+ title: "anthropic-lab",
140
+ path: "/Volumes/Code_2TB/code/anthropic-lab",
141
+ resumeTool: "claude",
142
+ resumeSessionId: "claude-456",
143
+ },
144
+ ],
145
+ notes: "",
146
+ },
147
+ notes: "",
148
+ });
149
+
150
+ const report = buildWorkspaceCommandsReport(
151
+ {
152
+ sourceType: "workspace-file",
153
+ sourceLabel: "/tmp/workspace.json",
154
+ title: "workspace",
155
+ },
156
+ parsed,
157
+ );
158
+
159
+ assert.equal(report.tabs[0]?.resumeCommand, "claude --resume claude-456");
160
+ assert.equal(
161
+ report.tabs[0]?.restartCommand,
162
+ "cd '/Volumes/Code_2TB/code/anthropic-lab' && claude --resume claude-456",
163
+ );
164
+ });
@@ -0,0 +1,253 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import {
5
+ buildLaunchPlan,
6
+ buildWorkspaceSyncPreview,
7
+ deriveBaseTitle,
8
+ extractStructuredWorkspaceFromNotes,
9
+ normalizeWorkspaceManifest,
10
+ parseCorePlanNotes,
11
+ parseWorkspaceSource,
12
+ resolveWorkspaceSyncTargetIdeaId,
13
+ } from "../src/index.js";
14
+ import { extractWorkspaceNarrativeNotes } from "../src/sync.js";
15
+
16
+ test("parseCorePlanNotes extracts paths and generic resume commands", () => {
17
+ const notes = `
18
+ /Volumes/Code_2TB/code/orp-web-app: codex resume 019ce2ee-6083-7d02-9d3a-d27a761132a8
19
+ /Volumes/Code_2TB/code/anthropic-lab: claude resume claude-456
20
+
21
+ /Volumes/Code_2TB/code/care-evidence-support
22
+ not a path
23
+ `;
24
+
25
+ const parsed = parseCorePlanNotes(notes);
26
+ assert.equal(parsed.entries.length, 3);
27
+ assert.deepEqual(
28
+ parsed.entries.map((entry) => ({
29
+ path: entry.path,
30
+ resumeCommand: entry.resumeCommand,
31
+ resumeTool: entry.resumeTool,
32
+ sessionId: entry.sessionId,
33
+ })),
34
+ [
35
+ {
36
+ path: "/Volumes/Code_2TB/code/orp-web-app",
37
+ resumeCommand: "codex resume 019ce2ee-6083-7d02-9d3a-d27a761132a8",
38
+ resumeTool: "codex",
39
+ sessionId: "019ce2ee-6083-7d02-9d3a-d27a761132a8",
40
+ },
41
+ {
42
+ path: "/Volumes/Code_2TB/code/anthropic-lab",
43
+ resumeCommand: "claude resume claude-456",
44
+ resumeTool: "claude",
45
+ sessionId: "claude-456",
46
+ },
47
+ {
48
+ path: "/Volumes/Code_2TB/code/care-evidence-support",
49
+ resumeCommand: null,
50
+ resumeTool: null,
51
+ sessionId: null,
52
+ },
53
+ ],
54
+ );
55
+ assert.equal(parsed.skipped.length, 1);
56
+ });
57
+
58
+ test("parseCorePlanNotes accepts real Claude resume flag syntax", () => {
59
+ const parsed = parseCorePlanNotes(`
60
+ /Volumes/Code_2TB/code/anthropic-lab: claude --resume claude-456
61
+ `);
62
+
63
+ assert.equal(parsed.entries.length, 1);
64
+ assert.equal(parsed.entries[0]?.resumeCommand, "claude --resume claude-456");
65
+ assert.equal(parsed.entries[0]?.resumeTool, "claude");
66
+ assert.equal(parsed.entries[0]?.sessionId, "claude-456");
67
+ });
68
+
69
+ test("buildLaunchPlan keeps duplicate titles unique and preserves exact resume commands", () => {
70
+ const parsed = parseCorePlanNotes(`
71
+ /Volumes/Code_2TB/code/collaboration: codex resume abc-123
72
+ /Volumes/Code_2TB/code/anthropic-lab: claude resume claude-456
73
+ /Volumes/Code_2TB/code/collaboration
74
+ `);
75
+
76
+ const plan = buildLaunchPlan(parsed.entries, {
77
+ tmux: false,
78
+ });
79
+
80
+ assert.equal(plan[0].title, "collaboration");
81
+ assert.equal(plan[1].title, "anthropic-lab");
82
+ assert.equal(plan[2].title, "collaboration (2)");
83
+ assert.equal(plan[0].resumeCommand, "codex resume abc-123");
84
+ assert.equal(plan[1].resumeCommand, "claude resume claude-456");
85
+ });
86
+
87
+ test("structured workspace blocks are extracted from notes", () => {
88
+ const notes = `
89
+ Some overview text.
90
+
91
+ \`\`\`orp-workspace
92
+ {
93
+ "version": "1",
94
+ "workspaceId": "workspace-idea",
95
+ "tabs": [
96
+ { "title": "orp", "path": "/Volumes/Code_2TB/code/orp" },
97
+ { "title": "web", "path": "/Volumes/Code_2TB/code/orp-web-app", "resumeCommand": "claude resume claude-456", "resumeTool": "claude", "resumeSessionId": "claude-456" }
98
+ ]
99
+ }
100
+ \`\`\`
101
+ `;
102
+ const manifest = extractStructuredWorkspaceFromNotes(notes);
103
+ const parsed = parseWorkspaceSource({
104
+ sourceType: "hosted-idea",
105
+ sourceLabel: "Workspace idea",
106
+ notes,
107
+ });
108
+
109
+ assert.equal(manifest.workspaceId, "workspace-idea");
110
+ assert.equal(parsed.parseMode, "manifest");
111
+ assert.equal(parsed.entries.length, 2);
112
+ assert.equal(parsed.entries[1]?.sessionId, "claude-456");
113
+ assert.equal(parsed.entries[1]?.resumeCommand, "claude resume claude-456");
114
+ });
115
+
116
+ test("normalizeWorkspaceManifest keeps ledger fields and strips nothing important", () => {
117
+ const manifest = normalizeWorkspaceManifest({
118
+ version: "1",
119
+ workspaceId: "terminal-paths",
120
+ title: "Terminal Paths",
121
+ tabs: [
122
+ {
123
+ title: "orp",
124
+ path: "/Volumes/Code_2TB/code/orp",
125
+ },
126
+ {
127
+ title: "lab",
128
+ path: "/Volumes/Code_2TB/code/anthropic-lab",
129
+ resumeTool: "claude",
130
+ resumeSessionId: "claude-456",
131
+ },
132
+ ],
133
+ });
134
+
135
+ assert.equal(manifest.workspaceId, "terminal-paths");
136
+ assert.equal(manifest.title, "Terminal Paths");
137
+ assert.equal(manifest.tabs[0]?.title, "orp");
138
+ assert.equal(manifest.tabs[1]?.resumeTool, "claude");
139
+ });
140
+
141
+ test("extractWorkspaceNarrativeNotes removes structured workspace blocks and legacy path lines", () => {
142
+ const notes = `
143
+ Workspace summary.
144
+
145
+ /Volumes/Code_2TB/code/orp
146
+
147
+ \`\`\`orp-workspace
148
+ {
149
+ "version": "1",
150
+ "workspaceId": "workspace-demo",
151
+ "tabs": [
152
+ { "path": "/Volumes/Code_2TB/code/orp" }
153
+ ]
154
+ }
155
+ \`\`\`
156
+ `;
157
+
158
+ assert.equal(
159
+ extractWorkspaceNarrativeNotes(notes, { stripLegacyWorkspaceLines: true }),
160
+ "Workspace summary.",
161
+ );
162
+ });
163
+
164
+ test("buildWorkspaceSyncPreview converts path notes into a structured workspace block", () => {
165
+ const source = {
166
+ sourceType: "hosted-idea",
167
+ sourceLabel: "Workspace idea",
168
+ title: "Workspace idea",
169
+ notes: `
170
+ Workspace summary.
171
+
172
+ /Volumes/Code_2TB/code/orp: codex resume abc-123
173
+ /Volumes/Code_2TB/code/orp-web-app
174
+ `,
175
+ idea: { id: "idea-123" },
176
+ };
177
+ const parsed = parseWorkspaceSource(source);
178
+ const preview = buildWorkspaceSyncPreview({
179
+ source,
180
+ parsed,
181
+ targetIdea: {
182
+ id: "idea-123",
183
+ title: "Workspace idea",
184
+ notes: source.notes,
185
+ },
186
+ });
187
+
188
+ assert.equal(preview.workspaceId, "idea-idea-123");
189
+ assert.equal(preview.tabs.length, 2);
190
+ assert.equal(preview.tabs[0]?.resumeCommand, "codex resume abc-123");
191
+ assert.equal(preview.tabs[1]?.title, deriveBaseTitle(preview.tabs[1]));
192
+ assert.match(preview.nextNotes, /```orp-workspace/);
193
+ assert.match(preview.nextNotes, /"workspaceId": "idea-idea-123"/);
194
+ assert.match(preview.nextNotes, /Workspace summary\./);
195
+ assert.match(preview.nextNotes, /\/Volumes\/Code_2TB\/code\/orp: codex resume abc-123/);
196
+ assert.match(preview.nextNotes, /\/Volumes\/Code_2TB\/code\/orp-web-app/);
197
+ });
198
+
199
+ test("buildWorkspaceSyncPreview strips stale legacy path lines when syncing from a workspace file", () => {
200
+ const source = {
201
+ sourceType: "workspace-file",
202
+ sourceLabel: "/tmp/workspace.json",
203
+ sourcePath: "/tmp/workspace.json",
204
+ title: "Workspace idea",
205
+ notes: "",
206
+ workspaceManifest: {
207
+ version: "1",
208
+ workspaceId: "workspace-file-demo",
209
+ tabs: [{ title: "orp", path: "/Volumes/Code_2TB/code/orp" }],
210
+ },
211
+ };
212
+ const parsed = parseWorkspaceSource(source);
213
+ const preview = buildWorkspaceSyncPreview({
214
+ source,
215
+ parsed,
216
+ targetIdea: {
217
+ id: "idea-123",
218
+ title: "Workspace idea",
219
+ notes: `
220
+ Workspace summary.
221
+
222
+ /Volumes/Code_2TB/code/orp: codex resume stale-session
223
+ `,
224
+ },
225
+ });
226
+
227
+ assert.match(preview.nextNotes, /Workspace summary\./);
228
+ assert.match(preview.nextNotes, /```orp-workspace/);
229
+ assert.doesNotMatch(preview.nextNotes, /stale-session/);
230
+ assert.doesNotMatch(preview.nextNotes, /\/Volumes\/Code_2TB\/code\/orp: codex resume/);
231
+ });
232
+
233
+ test("resolveWorkspaceSyncTargetIdeaId supports hosted idea and hosted workspace sources", () => {
234
+ assert.equal(
235
+ resolveWorkspaceSyncTargetIdeaId({
236
+ sourceType: "hosted-idea",
237
+ idea: { id: "idea-123" },
238
+ }),
239
+ "idea-123",
240
+ );
241
+
242
+ assert.equal(
243
+ resolveWorkspaceSyncTargetIdeaId({
244
+ sourceType: "hosted-workspace",
245
+ hostedWorkspace: {
246
+ linkedIdea: {
247
+ ideaId: "idea-456",
248
+ },
249
+ },
250
+ }),
251
+ "idea-456",
252
+ );
253
+ });
@@ -0,0 +1,2 @@
1
+ /Volumes/Code_2TB/code/orp
2
+ /Volumes/Code_2TB/code/orp-web-app
@@ -0,0 +1,17 @@
1
+ {
2
+ "version": "1",
3
+ "workspaceId": "terminal-paths-and-codex-sessions",
4
+ "title": "Terminal paths and codex sessions",
5
+ "tmuxPrefix": "backtothefort",
6
+ "tabs": [
7
+ {
8
+ "title": "orp",
9
+ "path": "/Volumes/Code_2TB/code/orp"
10
+ },
11
+ {
12
+ "title": "orp-web-app",
13
+ "path": "/Volumes/Code_2TB/code/orp-web-app",
14
+ "codexSessionId": "019ce2ee-6083-7d02-9d3a-d27a761132a8"
15
+ }
16
+ ]
17
+ }