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.
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 +5 -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,912 @@
1
+ import fs from "node:fs/promises";
2
+ import syncFs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import process from "node:process";
6
+ import { spawn } from "node:child_process";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ import { extractStructuredWorkspaceFromNotes, parseWorkspaceSource } from "./core-plan.js";
10
+ import { listTrackedWorkspaces, loadWorkspaceSlots, normalizeWorkspaceSlotName } from "./registry.js";
11
+
12
+ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
13
+
14
+ function runCommand(command, args, options = {}) {
15
+ return new Promise((resolve, reject) => {
16
+ const child = spawn(command, args, {
17
+ cwd: options.cwd,
18
+ env: options.env || process.env,
19
+ stdio: ["ignore", "pipe", "pipe"],
20
+ });
21
+
22
+ let stdout = "";
23
+ let stderr = "";
24
+
25
+ child.stdout.on("data", (chunk) => {
26
+ stdout += chunk;
27
+ });
28
+ child.stderr.on("data", (chunk) => {
29
+ stderr += chunk;
30
+ });
31
+ child.on("error", reject);
32
+ child.on("close", (code) => {
33
+ resolve({ code: code == null ? 1 : code, stdout, stderr });
34
+ });
35
+ });
36
+ }
37
+
38
+ function resolveOrpInvocation(options = {}) {
39
+ if (options.orpCommand) {
40
+ return {
41
+ command: options.orpCommand,
42
+ prefixArgs: [],
43
+ };
44
+ }
45
+
46
+ const bundledBin = path.resolve(MODULE_DIR, "../../../bin/orp.js");
47
+ if (syncFs.existsSync(bundledBin)) {
48
+ return {
49
+ command: process.execPath,
50
+ prefixArgs: [bundledBin],
51
+ };
52
+ }
53
+
54
+ return {
55
+ command: "orp",
56
+ prefixArgs: [],
57
+ };
58
+ }
59
+
60
+ function normalizeOptionalString(value) {
61
+ if (value == null) {
62
+ return null;
63
+ }
64
+ const trimmed = String(value).trim();
65
+ return trimmed.length > 0 ? trimmed : null;
66
+ }
67
+
68
+ function slugify(value) {
69
+ const normalized = String(value || "")
70
+ .trim()
71
+ .toLowerCase()
72
+ .replace(/[^a-z0-9]+/g, "-")
73
+ .replace(/^-+|-+$/g, "");
74
+ return normalized || null;
75
+ }
76
+
77
+ function getObjectValue(record, ...keys) {
78
+ for (const key of keys) {
79
+ const value = record?.[key];
80
+ if (value && typeof value === "object" && !Array.isArray(value)) {
81
+ return value;
82
+ }
83
+ }
84
+ return null;
85
+ }
86
+
87
+ function getArrayValue(record, ...keys) {
88
+ for (const key of keys) {
89
+ const value = record?.[key];
90
+ if (Array.isArray(value)) {
91
+ return value;
92
+ }
93
+ }
94
+ return [];
95
+ }
96
+
97
+ function getTextValue(record, ...keys) {
98
+ for (const key of keys) {
99
+ const normalized = normalizeOptionalString(record?.[key]);
100
+ if (normalized) {
101
+ return normalized;
102
+ }
103
+ }
104
+ return null;
105
+ }
106
+
107
+ function parseOrpJsonResult(result, fallbackError) {
108
+ if (result.code !== 0) {
109
+ throw new Error(result.stderr.trim() || result.stdout.trim() || fallbackError);
110
+ }
111
+
112
+ try {
113
+ return JSON.parse(result.stdout);
114
+ } catch (error) {
115
+ throw new Error(`Failed to parse ORP JSON output: ${error instanceof Error ? error.message : String(error)}`);
116
+ }
117
+ }
118
+
119
+ export async function fetchIdeaPayload(ideaId, options = {}) {
120
+ const invocation = resolveOrpInvocation(options);
121
+ const args = [...invocation.prefixArgs, "idea", "show", "--json"];
122
+ if (options.baseUrl) {
123
+ args.push("--base-url", options.baseUrl);
124
+ }
125
+ args.push(ideaId);
126
+
127
+ const result = await runCommand(invocation.command, args, options);
128
+ const payload = parseOrpJsonResult(result, "Failed to fetch ORP idea.");
129
+
130
+ if (!payload || payload.ok !== true || !payload.idea) {
131
+ throw new Error("ORP returned an unexpected idea payload.");
132
+ }
133
+
134
+ return payload;
135
+ }
136
+
137
+ export async function fetchIdeasPayload(options = {}) {
138
+ const invocation = resolveOrpInvocation(options);
139
+ const items = [];
140
+ let cursor = "";
141
+
142
+ for (;;) {
143
+ const args = [...invocation.prefixArgs, "ideas", "list", "--limit", "200", "--json"];
144
+ if (options.baseUrl) {
145
+ args.push("--base-url", options.baseUrl);
146
+ }
147
+ if (cursor) {
148
+ args.push("--cursor", cursor);
149
+ }
150
+
151
+ const result = await runCommand(invocation.command, args, options);
152
+ const payload = parseOrpJsonResult(result, "Failed to fetch ORP ideas.");
153
+ const pageItems = Array.isArray(payload?.ideas) ? payload.ideas : [];
154
+ items.push(...pageItems.filter((row) => row && typeof row === "object" && !Array.isArray(row)));
155
+ cursor = normalizeOptionalString(payload?.cursor);
156
+ if (!payload?.has_more || !cursor) {
157
+ break;
158
+ }
159
+ }
160
+
161
+ return {
162
+ ok: true,
163
+ ideas: items,
164
+ };
165
+ }
166
+
167
+ export async function fetchHostedWorkspacesPayload(options = {}) {
168
+ const invocation = resolveOrpInvocation(options);
169
+ const items = [];
170
+ let cursor = "";
171
+ let source = "";
172
+
173
+ for (;;) {
174
+ const args = [...invocation.prefixArgs, "workspaces", "list", "--limit", "200", "--json"];
175
+ if (options.baseUrl) {
176
+ args.push("--base-url", options.baseUrl);
177
+ }
178
+ if (cursor) {
179
+ args.push("--cursor", cursor);
180
+ }
181
+
182
+ const result = await runCommand(invocation.command, args, options);
183
+ const payload = parseOrpJsonResult(result, "Failed to fetch ORP workspaces.");
184
+ const pageItems = Array.isArray(payload?.workspaces) ? payload.workspaces : [];
185
+ items.push(...pageItems.filter((row) => row && typeof row === "object" && !Array.isArray(row)));
186
+ source = normalizeOptionalString(payload?.source) || source;
187
+ cursor = normalizeOptionalString(payload?.cursor);
188
+ if (!payload?.has_more || !cursor) {
189
+ break;
190
+ }
191
+ }
192
+
193
+ return {
194
+ ok: true,
195
+ source: source || "hosted",
196
+ workspaces: items,
197
+ };
198
+ }
199
+
200
+ function buildWorkspaceTitleFromIdea(idea, manifest) {
201
+ return normalizeOptionalString(manifest?.title) || normalizeOptionalString(idea?.title) || null;
202
+ }
203
+
204
+ function buildWorkspaceIdFromIdea(idea, manifest) {
205
+ return normalizeOptionalString(manifest?.workspaceId) || normalizeOptionalString(idea?.id) || null;
206
+ }
207
+
208
+ function selectorForms(value) {
209
+ const exact = normalizeOptionalString(value);
210
+ const lower = exact ? exact.toLowerCase() : null;
211
+ const slug = exact ? slugify(exact) : null;
212
+ return { exact, lower, slug };
213
+ }
214
+
215
+ function matchQuality(selector, candidate) {
216
+ const selectorFormsValue = selectorForms(selector);
217
+ const candidates = [candidate].flat().filter(Boolean);
218
+ let best = 0;
219
+ for (const value of candidates) {
220
+ const candidateForms = selectorForms(value);
221
+ if (!candidateForms.exact) {
222
+ continue;
223
+ }
224
+ if (selectorFormsValue.exact === candidateForms.exact) {
225
+ best = Math.max(best, 40);
226
+ } else if (selectorFormsValue.lower && selectorFormsValue.lower === candidateForms.lower) {
227
+ best = Math.max(best, 35);
228
+ } else if (selectorFormsValue.slug && selectorFormsValue.slug === candidateForms.slug) {
229
+ best = Math.max(best, 20);
230
+ }
231
+ }
232
+ return best;
233
+ }
234
+
235
+ function extractIdeaWorkspaceCandidate(idea) {
236
+ if (!idea || typeof idea !== "object" || Array.isArray(idea)) {
237
+ return null;
238
+ }
239
+ const notes = typeof idea.notes === "string" ? idea.notes : "";
240
+ let parsed;
241
+ try {
242
+ parsed = parseWorkspaceSource({
243
+ sourceType: "hosted-idea",
244
+ sourceLabel: normalizeOptionalString(idea.title) || normalizeOptionalString(idea.id) || "Hosted idea",
245
+ title: normalizeOptionalString(idea.title) || normalizeOptionalString(idea.id) || "Hosted idea",
246
+ notes,
247
+ idea,
248
+ });
249
+ } catch {
250
+ return null;
251
+ }
252
+
253
+ if (!Array.isArray(parsed.entries) || parsed.entries.length === 0) {
254
+ return null;
255
+ }
256
+
257
+ let manifest = null;
258
+ try {
259
+ manifest = extractStructuredWorkspaceFromNotes(notes);
260
+ } catch {
261
+ manifest = null;
262
+ }
263
+
264
+ const title = buildWorkspaceTitleFromIdea(idea, manifest);
265
+ const workspaceId = buildWorkspaceIdFromIdea(idea, manifest);
266
+ return {
267
+ kind: "hosted-idea",
268
+ idea,
269
+ manifest,
270
+ title,
271
+ workspaceId,
272
+ selectorValues: [
273
+ normalizeOptionalString(idea.id),
274
+ normalizeOptionalString(idea.title),
275
+ workspaceId,
276
+ title,
277
+ ].filter(Boolean),
278
+ tabCount: parsed.entries.length,
279
+ parseMode: parsed.parseMode,
280
+ };
281
+ }
282
+
283
+ function extractLocalWorkspaceCandidate(workspace) {
284
+ if (!workspace || typeof workspace !== "object" || Array.isArray(workspace)) {
285
+ return null;
286
+ }
287
+ if (workspace.status && workspace.status !== "ok") {
288
+ return null;
289
+ }
290
+ return {
291
+ kind: "workspace-file",
292
+ manifestPath: normalizeOptionalString(workspace.manifestPath),
293
+ workspaceId: normalizeOptionalString(workspace.workspaceId),
294
+ title: normalizeOptionalString(workspace.title),
295
+ selectorValues: [
296
+ normalizeOptionalString(workspace.workspaceId),
297
+ normalizeOptionalString(workspace.title),
298
+ normalizeOptionalString(workspace.manifestPath ? path.basename(workspace.manifestPath, path.extname(workspace.manifestPath)) : ""),
299
+ ].filter(Boolean),
300
+ updatedAt: normalizeOptionalString(workspace.updatedAt),
301
+ };
302
+ }
303
+
304
+ function extractHostedWorkspaceCandidate(workspace) {
305
+ if (!workspace || typeof workspace !== "object" || Array.isArray(workspace)) {
306
+ return null;
307
+ }
308
+ const workspaceId = normalizeOptionalString(workspace.workspace_id ?? workspace.id);
309
+ if (!workspaceId) {
310
+ return null;
311
+ }
312
+ const linkedIdea = getObjectValue(workspace, "linked_idea", "linkedIdea");
313
+ return {
314
+ kind: "hosted-workspace",
315
+ workspaceId,
316
+ title: normalizeOptionalString(workspace.title) || workspaceId,
317
+ hostedWorkspaceId: workspaceId,
318
+ selectorValues: [
319
+ workspaceId,
320
+ normalizeOptionalString(workspace.title),
321
+ getTextValue(linkedIdea, "idea_id", "ideaId"),
322
+ getTextValue(linkedIdea, "idea_title", "ideaTitle"),
323
+ ].filter(Boolean),
324
+ updatedAt: normalizeOptionalString(workspace.updated_at_utc ?? workspace.updatedAt),
325
+ sourceKind: normalizeOptionalString(workspace.source_kind ?? workspace.sourceKind) || "hosted",
326
+ };
327
+ }
328
+
329
+ function candidateMergeKey(candidate) {
330
+ const workspaceId = normalizeOptionalString(candidate?.workspaceId);
331
+ if (workspaceId) {
332
+ return `id:${workspaceId}`;
333
+ }
334
+ const titleSlug = slugify(candidate?.title);
335
+ if (titleSlug) {
336
+ return `title:${titleSlug}`;
337
+ }
338
+ const manifestPath = normalizeOptionalString(candidate?.manifestPath);
339
+ if (manifestPath) {
340
+ return `path:${path.resolve(manifestPath)}`;
341
+ }
342
+ const ideaId = normalizeOptionalString(candidate?.idea?.id);
343
+ if (ideaId) {
344
+ return `idea:${ideaId}`;
345
+ }
346
+ return null;
347
+ }
348
+
349
+ function looksLikeGeneratedCaptureName(value) {
350
+ const normalized = normalizeOptionalString(value);
351
+ if (!normalized) {
352
+ return false;
353
+ }
354
+ return /^captured-iterm-window-/i.test(normalized);
355
+ }
356
+
357
+ export function chooseImplicitMainCandidate(candidates = []) {
358
+ const rows = Array.isArray(candidates) ? candidates.filter(Boolean) : [];
359
+ if (rows.length === 1) {
360
+ return rows[0];
361
+ }
362
+
363
+ const hostedBacked = rows.filter((candidate) => candidate.kind === "hosted-workspace" || candidate.kind === "hosted-idea");
364
+ if (hostedBacked.length === 1) {
365
+ return hostedBacked[0];
366
+ }
367
+
368
+ const nonGenerated = rows.filter((candidate) => {
369
+ if (candidate.kind !== "workspace-file") {
370
+ return true;
371
+ }
372
+ return !looksLikeGeneratedCaptureName(candidate.workspaceId) && !looksLikeGeneratedCaptureName(candidate.title);
373
+ });
374
+ if (nonGenerated.length === 1) {
375
+ return nonGenerated[0];
376
+ }
377
+
378
+ return null;
379
+ }
380
+
381
+ function collectUniqueWorkspaceCandidates(collections = {}) {
382
+ const localCandidates = Array.isArray(collections.localWorkspaces)
383
+ ? collections.localWorkspaces.map((row) => extractLocalWorkspaceCandidate(row)).filter(Boolean)
384
+ : [];
385
+ const hostedCandidates = Array.isArray(collections.hostedWorkspaces)
386
+ ? collections.hostedWorkspaces.map((row) => extractHostedWorkspaceCandidate(row)).filter(Boolean)
387
+ : [];
388
+ const ideaCandidates = Array.isArray(collections.ideas)
389
+ ? collections.ideas.map((row) => extractIdeaWorkspaceCandidate(row)).filter(Boolean)
390
+ : [];
391
+
392
+ const merged = new Map();
393
+ for (const candidate of [...hostedCandidates, ...ideaCandidates, ...localCandidates]) {
394
+ const key = candidateMergeKey(candidate);
395
+ if (!key) {
396
+ continue;
397
+ }
398
+ const current = merged.get(key);
399
+ if (
400
+ !current ||
401
+ (current.kind === "workspace-file" && candidate.kind !== "workspace-file") ||
402
+ (current.kind === "hosted-idea" && candidate.kind === "hosted-workspace")
403
+ ) {
404
+ merged.set(key, candidate);
405
+ }
406
+ }
407
+ return [...merged.values()];
408
+ }
409
+
410
+ async function collectWorkspaceSelectorCollections(options = {}) {
411
+ const [ideasPayload, hostedWorkspacesPayload, localRegistry] = await Promise.all([
412
+ fetchIdeasPayload(options).catch(() => ({ ideas: [] })),
413
+ fetchHostedWorkspacesPayload(options).catch(() => ({ workspaces: [] })),
414
+ listTrackedWorkspaces(options).catch(() => ({ workspaces: [] })),
415
+ ]);
416
+ return {
417
+ ideas: ideasPayload.ideas,
418
+ hostedWorkspaces: hostedWorkspacesPayload.workspaces,
419
+ localWorkspaces: localRegistry.workspaces,
420
+ };
421
+ }
422
+
423
+ function buildWorkspaceSourceOptionsFromCandidate(candidate) {
424
+ if (!candidate || typeof candidate !== "object") {
425
+ return null;
426
+ }
427
+ if (candidate.kind === "workspace-file" && candidate.manifestPath) {
428
+ return {
429
+ workspaceFile: candidate.manifestPath,
430
+ };
431
+ }
432
+ if (candidate.kind === "hosted-idea" && candidate.idea?.id) {
433
+ return {
434
+ ideaId: candidate.idea.id,
435
+ };
436
+ }
437
+ if (candidate.kind === "hosted-workspace" && candidate.workspaceId) {
438
+ return {
439
+ hostedWorkspaceId: candidate.workspaceId,
440
+ };
441
+ }
442
+ return null;
443
+ }
444
+
445
+ function buildWorkspaceSourceOptionsFromSlot(slot) {
446
+ if (!slot || typeof slot !== "object") {
447
+ return null;
448
+ }
449
+ if (slot.kind === "workspace-file" && slot.manifestPath) {
450
+ return {
451
+ workspaceFile: slot.manifestPath,
452
+ };
453
+ }
454
+ if (slot.kind === "hosted-workspace") {
455
+ const hostedWorkspaceId = normalizeOptionalString(slot.hostedWorkspaceId) || normalizeOptionalString(slot.workspaceId);
456
+ if (hostedWorkspaceId) {
457
+ return {
458
+ hostedWorkspaceId,
459
+ };
460
+ }
461
+ }
462
+ if (slot.kind === "hosted-idea") {
463
+ const ideaId = normalizeOptionalString(slot.ideaId) || normalizeOptionalString(slot.workspaceId) || normalizeOptionalString(slot.selector);
464
+ if (ideaId) {
465
+ return {
466
+ ideaId,
467
+ };
468
+ }
469
+ }
470
+ return null;
471
+ }
472
+
473
+ export function buildWorkspaceSlotAssignment(candidate) {
474
+ if (!candidate || typeof candidate !== "object") {
475
+ throw new Error("workspace candidate is required");
476
+ }
477
+ if (candidate.kind === "workspace-file") {
478
+ return {
479
+ kind: "workspace-file",
480
+ selector: normalizeOptionalString(candidate.title) || normalizeOptionalString(candidate.workspaceId) || candidate.manifestPath,
481
+ workspaceId: normalizeOptionalString(candidate.workspaceId) || undefined,
482
+ title: normalizeOptionalString(candidate.title) || undefined,
483
+ manifestPath: normalizeOptionalString(candidate.manifestPath) || undefined,
484
+ };
485
+ }
486
+ if (candidate.kind === "hosted-idea") {
487
+ return {
488
+ kind: "hosted-idea",
489
+ selector:
490
+ normalizeOptionalString(candidate.title) ||
491
+ normalizeOptionalString(candidate.workspaceId) ||
492
+ normalizeOptionalString(candidate.idea?.id) ||
493
+ undefined,
494
+ workspaceId: normalizeOptionalString(candidate.workspaceId) || undefined,
495
+ title: normalizeOptionalString(candidate.title) || undefined,
496
+ ideaId: normalizeOptionalString(candidate.idea?.id) || undefined,
497
+ };
498
+ }
499
+ if (candidate.kind === "hosted-workspace") {
500
+ return {
501
+ kind: "hosted-workspace",
502
+ selector:
503
+ normalizeOptionalString(candidate.title) ||
504
+ normalizeOptionalString(candidate.workspaceId) ||
505
+ undefined,
506
+ workspaceId: normalizeOptionalString(candidate.workspaceId) || undefined,
507
+ title: normalizeOptionalString(candidate.title) || undefined,
508
+ hostedWorkspaceId: normalizeOptionalString(candidate.workspaceId) || undefined,
509
+ };
510
+ }
511
+ throw new Error(`unsupported workspace candidate kind: ${candidate.kind || "unknown"}`);
512
+ }
513
+
514
+ async function resolveWorkspaceSlotTarget(selector, options = {}) {
515
+ const slotName = normalizeWorkspaceSlotName(selector);
516
+ if (!slotName) {
517
+ return null;
518
+ }
519
+
520
+ const { slots } = await loadWorkspaceSlots(options).catch(() => ({ slots: {} }));
521
+ const explicitSlot = slots[slotName] || null;
522
+ if (explicitSlot) {
523
+ const target = buildWorkspaceSourceOptionsFromSlot(explicitSlot);
524
+ if (!target) {
525
+ throw new Error(`Workspace slot '${slotName}' is set but no longer points to a valid workspace target.`);
526
+ }
527
+ return {
528
+ slotName,
529
+ mode: "explicit",
530
+ target,
531
+ slot: explicitSlot,
532
+ };
533
+ }
534
+
535
+ if (slotName === "main") {
536
+ const collections = await collectWorkspaceSelectorCollections(options);
537
+ const candidates = collectUniqueWorkspaceCandidates(collections);
538
+ const implicitCandidate = chooseImplicitMainCandidate(candidates);
539
+ if (implicitCandidate) {
540
+ const target = buildWorkspaceSourceOptionsFromCandidate(implicitCandidate);
541
+ if (target) {
542
+ return {
543
+ slotName,
544
+ mode: "implicit",
545
+ target,
546
+ candidate: implicitCandidate,
547
+ };
548
+ }
549
+ }
550
+ }
551
+
552
+ return {
553
+ slotName,
554
+ mode: "unset",
555
+ target: null,
556
+ slot: explicitSlot,
557
+ };
558
+ }
559
+
560
+ export function resolveWorkspaceSelectorFromCollections(selector, collections = {}) {
561
+ const normalizedSelector = normalizeOptionalString(selector);
562
+ if (!normalizedSelector) {
563
+ return null;
564
+ }
565
+
566
+ const localCandidates = Array.isArray(collections.localWorkspaces)
567
+ ? collections.localWorkspaces.map((row) => extractLocalWorkspaceCandidate(row)).filter(Boolean)
568
+ : [];
569
+ const hostedCandidates = Array.isArray(collections.hostedWorkspaces)
570
+ ? collections.hostedWorkspaces.map((row) => extractHostedWorkspaceCandidate(row)).filter(Boolean)
571
+ : [];
572
+ const ideaCandidates = Array.isArray(collections.ideas)
573
+ ? collections.ideas.map((row) => extractIdeaWorkspaceCandidate(row)).filter(Boolean)
574
+ : [];
575
+
576
+ const ranked = [];
577
+ for (const candidate of [...hostedCandidates, ...ideaCandidates, ...localCandidates]) {
578
+ let score = matchQuality(normalizedSelector, candidate.selectorValues);
579
+ if (candidate.kind === "hosted-workspace") {
580
+ score += 10;
581
+ } else if (candidate.kind === "hosted-idea") {
582
+ score += 5;
583
+ }
584
+ if (score > 0) {
585
+ ranked.push({ candidate, score });
586
+ }
587
+ }
588
+
589
+ if (ranked.length === 0) {
590
+ return null;
591
+ }
592
+
593
+ ranked.sort((left, right) => right.score - left.score);
594
+ const bestScore = ranked[0].score;
595
+ const best = ranked.filter((row) => row.score === bestScore);
596
+ if (best.length > 1) {
597
+ const choices = best
598
+ .map(({ candidate }) => {
599
+ if (candidate.kind === "hosted-idea") {
600
+ return `${candidate.title || candidate.workspaceId || candidate.idea.id} [idea ${candidate.idea.id}]`;
601
+ }
602
+ if (candidate.kind === "hosted-workspace") {
603
+ return `${candidate.title || candidate.workspaceId} [workspace ${candidate.workspaceId}]`;
604
+ }
605
+ return `${candidate.title || candidate.workspaceId || candidate.manifestPath} [${candidate.manifestPath}]`;
606
+ })
607
+ .join("; ");
608
+ throw new Error(`Workspace selector '${normalizedSelector}' is ambiguous. Matches: ${choices}`);
609
+ }
610
+ return best[0].candidate;
611
+ }
612
+
613
+ export function buildWorkspaceManifestFromHostedWorkspacePayload(payload) {
614
+ const workspace =
615
+ payload && typeof payload === "object" && !Array.isArray(payload) && payload.workspace && typeof payload.workspace === "object"
616
+ ? payload.workspace
617
+ : payload;
618
+ if (!workspace || typeof workspace !== "object" || Array.isArray(workspace)) {
619
+ throw new Error("Hosted ORP returned an unexpected workspace payload.");
620
+ }
621
+
622
+ const state = getObjectValue(workspace, "state") || {};
623
+ const captureContext = getObjectValue(state, "capture_context", "captureContext");
624
+ const tabs = getArrayValue(state, "tabs").filter((row) => row && typeof row === "object" && !Array.isArray(row));
625
+
626
+ const manifest = {
627
+ version: "1",
628
+ workspaceId: getTextValue(workspace, "workspace_id", "id"),
629
+ title: getTextValue(workspace, "title"),
630
+ capture: captureContext
631
+ ? Object.fromEntries(
632
+ Object.entries({
633
+ sourceApp: getTextValue(captureContext, "source_app", "sourceApp"),
634
+ mode: getTextValue(captureContext, "mode"),
635
+ host: getTextValue(captureContext, "host"),
636
+ windowId:
637
+ Number.isInteger(captureContext.window_id) && captureContext.window_id > 0
638
+ ? captureContext.window_id
639
+ : Number.isInteger(captureContext.windowId) && captureContext.windowId > 0
640
+ ? captureContext.windowId
641
+ : undefined,
642
+ windowIndex:
643
+ Number.isInteger(captureContext.window_index) && captureContext.window_index > 0
644
+ ? captureContext.window_index
645
+ : Number.isInteger(captureContext.windowIndex) && captureContext.windowIndex > 0
646
+ ? captureContext.windowIndex
647
+ : undefined,
648
+ pollSeconds:
649
+ typeof captureContext.poll_seconds === "number" && captureContext.poll_seconds > 0
650
+ ? captureContext.poll_seconds
651
+ : typeof captureContext.pollSeconds === "number" && captureContext.pollSeconds > 0
652
+ ? captureContext.pollSeconds
653
+ : undefined,
654
+ capturedAt: getTextValue(state, "captured_at_utc", "capturedAtUtc"),
655
+ trackingStartedAt: getTextValue(captureContext, "tracking_started_at_utc", "trackingStartedAtUtc"),
656
+ tabCount:
657
+ Number.isInteger(state.tab_count) && state.tab_count >= 0
658
+ ? state.tab_count
659
+ : Number.isInteger(state.tabCount) && state.tabCount >= 0
660
+ ? state.tabCount
661
+ : tabs.length,
662
+ }).filter(([, value]) => value !== undefined && value !== null),
663
+ )
664
+ : undefined,
665
+ tabs: tabs.map((tab) =>
666
+ Object.fromEntries(
667
+ Object.entries({
668
+ title:
669
+ getTextValue(tab, "title") ||
670
+ getTextValue(tab, "repo_label", "repoLabel") ||
671
+ path.basename(String(getTextValue(tab, "project_root", "projectRoot") || "").replace(/\/+$/, "")) ||
672
+ undefined,
673
+ path: getTextValue(tab, "project_root", "projectRoot"),
674
+ resumeCommand: getTextValue(tab, "resume_command", "resumeCommand"),
675
+ resumeTool: getTextValue(tab, "resume_tool", "resumeTool"),
676
+ resumeSessionId: getTextValue(tab, "resume_session_id", "resumeSessionId"),
677
+ codexSessionId: getTextValue(tab, "codex_session_id", "codexSessionId"),
678
+ tmuxSessionName: getTextValue(tab, "tmux_session_name", "tmuxSessionName"),
679
+ }).filter(([, value]) => value !== undefined && value !== null),
680
+ ),
681
+ ),
682
+ };
683
+
684
+ if (!manifest.workspaceId || manifest.tabs.length === 0) {
685
+ throw new Error("Hosted ORP workspace payload is missing a workspace id or saved tabs.");
686
+ }
687
+
688
+ return manifest;
689
+ }
690
+
691
+ export async function fetchHostedWorkspacePayload(workspaceId, options = {}) {
692
+ const invocation = resolveOrpInvocation(options);
693
+ const args = [...invocation.prefixArgs, "workspaces", "show", workspaceId, "--json"];
694
+ if (options.baseUrl) {
695
+ args.push("--base-url", options.baseUrl);
696
+ }
697
+
698
+ const result = await runCommand(invocation.command, args, options);
699
+ const payload = parseOrpJsonResult(result, "Failed to fetch ORP hosted workspace.");
700
+ if (!payload || payload.ok !== true || !payload.workspace) {
701
+ throw new Error("ORP returned an unexpected hosted workspace payload.");
702
+ }
703
+ return payload;
704
+ }
705
+
706
+ export async function updateIdeaPayload(ideaId, fields, options = {}) {
707
+ const invocation = resolveOrpInvocation(options);
708
+ const args = [...invocation.prefixArgs, "idea", "update"];
709
+ if (options.baseUrl) {
710
+ args.push("--base-url", options.baseUrl);
711
+ }
712
+ args.push(ideaId);
713
+
714
+ if (fields.title != null) {
715
+ args.push("--title", String(fields.title));
716
+ }
717
+ if (fields.notes != null) {
718
+ args.push("--notes", String(fields.notes));
719
+ }
720
+
721
+ args.push("--json");
722
+
723
+ const result = await runCommand(invocation.command, args, options);
724
+ const payload = parseOrpJsonResult(result, "Failed to update ORP idea.");
725
+
726
+ if (!payload || payload.ok !== true || !payload.idea?.id) {
727
+ throw new Error("ORP returned an unexpected update payload.");
728
+ }
729
+
730
+ return payload.idea;
731
+ }
732
+
733
+ export async function pushHostedWorkspaceState(workspaceId, state, options = {}) {
734
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "orp-hosted-workspace-state-"));
735
+ const statePath = path.join(tempDir, "state.json");
736
+ try {
737
+ await fs.writeFile(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
738
+ const invocation = resolveOrpInvocation(options);
739
+ const args = [...invocation.prefixArgs, "workspaces", "push-state", workspaceId, "--state-file", statePath, "--json"];
740
+ if (options.baseUrl) {
741
+ args.push("--base-url", options.baseUrl);
742
+ }
743
+ const result = await runCommand(invocation.command, args, options);
744
+ return parseOrpJsonResult(result, "Failed to push hosted workspace state.");
745
+ } finally {
746
+ await fs.rm(tempDir, { recursive: true, force: true });
747
+ }
748
+ }
749
+
750
+ export function resolveWorkspaceWatchTargets(source, options = {}) {
751
+ if (!source || typeof source !== "object") {
752
+ return {
753
+ hostedWorkspaceId: null,
754
+ syncIdeaSelector: null,
755
+ };
756
+ }
757
+
758
+ if (source.sourceType === "hosted-idea") {
759
+ return {
760
+ hostedWorkspaceId: null,
761
+ syncIdeaSelector: source.idea?.id || options.ideaId || null,
762
+ };
763
+ }
764
+
765
+ if (source.sourceType === "hosted-workspace") {
766
+ const hostedWorkspace = source.hostedWorkspace || {};
767
+ const sourceKind = normalizeOptionalString(hostedWorkspace.source_kind ?? hostedWorkspace.sourceKind) || "hosted";
768
+ const linkedIdea = getObjectValue(hostedWorkspace, "linked_idea", "linkedIdea");
769
+ const linkedIdeaId = getTextValue(linkedIdea, "idea_id", "ideaId");
770
+ if (sourceKind === "idea_bridge" && linkedIdeaId) {
771
+ return {
772
+ hostedWorkspaceId: null,
773
+ syncIdeaSelector: linkedIdeaId,
774
+ };
775
+ }
776
+ return {
777
+ hostedWorkspaceId:
778
+ normalizeOptionalString(hostedWorkspace.workspace_id ?? hostedWorkspace.id) ||
779
+ normalizeOptionalString(options.hostedWorkspaceId),
780
+ syncIdeaSelector: null,
781
+ };
782
+ }
783
+
784
+ return {
785
+ hostedWorkspaceId: null,
786
+ syncIdeaSelector: null,
787
+ };
788
+ }
789
+
790
+ export async function loadWorkspaceSource(options = {}) {
791
+ if (options.workspaceFile) {
792
+ const workspacePath = path.resolve(options.workspaceFile);
793
+ const raw = await fs.readFile(workspacePath, "utf8");
794
+ let workspaceManifest;
795
+ try {
796
+ workspaceManifest = JSON.parse(raw);
797
+ } catch (error) {
798
+ throw new Error(
799
+ `Failed to parse workspace JSON at ${workspacePath}: ${error instanceof Error ? error.message : String(error)}`,
800
+ );
801
+ }
802
+
803
+ return {
804
+ sourceType: "workspace-file",
805
+ sourceLabel: workspacePath,
806
+ sourcePath: workspacePath,
807
+ title: path.basename(workspacePath),
808
+ workspaceManifest,
809
+ notes: "",
810
+ };
811
+ }
812
+
813
+ if (options.notesFile) {
814
+ const notesPath = path.resolve(options.notesFile);
815
+ const notes = await fs.readFile(notesPath, "utf8");
816
+ return {
817
+ sourceType: "file",
818
+ sourceLabel: notesPath,
819
+ sourcePath: notesPath,
820
+ title: path.basename(notesPath),
821
+ notes,
822
+ };
823
+ }
824
+
825
+ if (options.hostedWorkspaceId) {
826
+ const payload = await fetchHostedWorkspacePayload(options.hostedWorkspaceId, options);
827
+ return {
828
+ sourceType: "hosted-workspace",
829
+ sourceLabel: payload.workspace.title || options.hostedWorkspaceId,
830
+ title: payload.workspace.title || options.hostedWorkspaceId,
831
+ workspaceManifest: buildWorkspaceManifestFromHostedWorkspacePayload(payload),
832
+ notes: "",
833
+ hostedWorkspace: payload.workspace,
834
+ payload,
835
+ };
836
+ }
837
+
838
+ if (!options.ideaId) {
839
+ throw new Error(
840
+ "Provide a workspace name or id, use --hosted-workspace-id <id>, --workspace-file <path>, or --notes-file <path>.",
841
+ );
842
+ }
843
+
844
+ const selector = options.ideaId;
845
+ const slotTarget = await resolveWorkspaceSlotTarget(selector, options);
846
+ if (slotTarget?.target) {
847
+ return loadWorkspaceSource({
848
+ ...options,
849
+ ideaId: slotTarget.target.ideaId,
850
+ workspaceFile: slotTarget.target.workspaceFile,
851
+ hostedWorkspaceId: slotTarget.target.hostedWorkspaceId,
852
+ });
853
+ }
854
+ if (slotTarget?.slotName && slotTarget.mode === "unset") {
855
+ throw new Error(
856
+ slotTarget.slotName === "main"
857
+ ? "Workspace slot 'main' is not set. If this Mac only has one workspace it becomes main automatically; otherwise run `orp workspace slot set main <name-or-id>`."
858
+ : "Workspace slot 'offhand' is not set. Run `orp workspace slot set offhand <name-or-id>`.",
859
+ );
860
+ }
861
+
862
+ const collections = await collectWorkspaceSelectorCollections(options);
863
+ const resolved = resolveWorkspaceSelectorFromCollections(selector, collections);
864
+
865
+ if (resolved?.kind === "hosted-workspace" && resolved.workspaceId) {
866
+ const payload = await fetchHostedWorkspacePayload(resolved.workspaceId, options);
867
+ return {
868
+ sourceType: "hosted-workspace",
869
+ sourceLabel: payload.workspace.title || resolved.workspaceId,
870
+ title: payload.workspace.title || resolved.workspaceId,
871
+ workspaceManifest: buildWorkspaceManifestFromHostedWorkspacePayload(payload),
872
+ notes: "",
873
+ hostedWorkspace: payload.workspace,
874
+ payload,
875
+ };
876
+ }
877
+
878
+ if (resolved?.kind === "workspace-file" && resolved.manifestPath) {
879
+ const workspacePath = path.resolve(resolved.manifestPath);
880
+ const raw = await fs.readFile(workspacePath, "utf8");
881
+ const workspaceManifest = JSON.parse(raw);
882
+ return {
883
+ sourceType: "workspace-file",
884
+ sourceLabel: resolved.title || workspacePath,
885
+ sourcePath: workspacePath,
886
+ title: resolved.title || path.basename(workspacePath),
887
+ workspaceManifest,
888
+ notes: "",
889
+ };
890
+ }
891
+
892
+ if (resolved?.kind === "hosted-idea") {
893
+ return {
894
+ sourceType: "hosted-idea",
895
+ sourceLabel: resolved.title || resolved.idea.id,
896
+ title: resolved.title || resolved.idea.id,
897
+ notes: resolved.idea.notes || "",
898
+ idea: resolved.idea,
899
+ payload: { ok: true, idea: resolved.idea },
900
+ };
901
+ }
902
+
903
+ const payload = await fetchIdeaPayload(selector, options);
904
+ return {
905
+ sourceType: "hosted-idea",
906
+ sourceLabel: payload.idea.title || selector,
907
+ title: payload.idea.title || selector,
908
+ notes: payload.idea.notes || "",
909
+ idea: payload.idea,
910
+ payload,
911
+ };
912
+ }