open-research-protocol 0.4.19 → 0.4.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  # orp-workspace-launcher
2
2
 
3
- Manage a durable ORP workspace ledger of project paths plus saved `codex resume ...` or `claude --resume ...` commands.
3
+ Manage a durable ORP workspace ledger of project paths plus saved `codex resume ...` or `claude --resume ...` commands, optional remote git URLs, bootstrap commands, and per-machine workspace identity.
4
4
 
5
5
  The package no longer automates iTerm or Terminal.app. The workspace ledger is the source of truth, and you use Terminal however you want.
6
6
 
@@ -11,6 +11,7 @@ Create a local workspace ledger with no hosted account required:
11
11
  ```bash
12
12
  orp workspace create main-cody-1
13
13
  orp workspace create research-lab --path /absolute/path/to/research-lab --resume-tool claude --resume-session-id 469d99b2-2997-42bf-a8f5-3812c808ef29
14
+ orp workspace create mac-main --machine-label "Mac Studio" --path /absolute/path/to/orp --remote-url git@github.com:SproutSeeds/orp.git --bootstrap-command "npm install"
14
15
  ```
15
16
 
16
17
  Inspect the saved ledger:
@@ -32,6 +33,7 @@ Add a new saved tab manually:
32
33
  ```bash
33
34
  orp workspace ledger add main --path /absolute/path/to/frg-site --resume-command "codex resume 019d348d-5031-78e1-9840-a66deaac33ae"
34
35
  orp workspace add-tab main --path /absolute/path/to/anthropic-lab --resume-tool claude --resume-session-id claude-456
36
+ orp workspace add-tab main --path /absolute/path/to/orp-web-app --remote-url git@github.com:SproutSeeds/orp-web-app.git --bootstrap-command "pnpm install"
35
37
  ```
36
38
 
37
39
  Remove a saved tab manually:
@@ -75,8 +77,24 @@ orp workspace slot set offhand research-lab
75
77
  - `--orp-command <path-or-command>`: override the ORP CLI binary used to fetch hosted idea JSON
76
78
  - `--path <absolute-path>`: add or match a saved project path
77
79
  - `--title <text>`: set or match a saved tab title
80
+ - `--remote-url <git-url>`: remember where to clone the repo on another machine
81
+ - `--remote-branch <branch>`: remember the branch to clone by default
82
+ - `--bootstrap-command <text>`: remember the setup command for another machine, like `npm install`, `pnpm install`, or `uv sync`
78
83
  - `--resume-command <text>`: save or match an exact `codex resume ...` or `claude --resume ...` command
79
84
  - `--resume-tool <codex|claude>`: build or narrow the resume command by tool
80
85
  - `--resume-session-id <id>`: build or match a specific session id
81
86
  - `--index <n>`: remove a saved tab by 1-based index
82
87
  - `--all`: remove every matching saved tab
88
+
89
+ ## Cross-machine idea
90
+
91
+ The local `path` is still the machine-specific working directory. The optional `remoteUrl`, `remoteBranch`, and `bootstrapCommand` fields are the portable part that an agent or user can carry to another rig.
92
+
93
+ That means one workspace can remember:
94
+
95
+ - where the repo lives on this machine
96
+ - which git remote should be cloned on the next machine
97
+ - which setup command should be run after clone
98
+ - which `codex resume ...` or `claude --resume ...` line is valid on this machine
99
+
100
+ Resume session ids are still machine-local/runtime-local. The remote and bootstrap metadata are what make the ledger portable.
@@ -64,6 +64,41 @@ function normalizeOptionalString(value) {
64
64
  return trimmed.length > 0 ? trimmed : null;
65
65
  }
66
66
 
67
+ function normalizeOptionalCommand(value) {
68
+ return normalizeOptionalString(value);
69
+ }
70
+
71
+ function normalizeOptionalUrl(value, label) {
72
+ const normalized = normalizeOptionalString(value);
73
+ if (!normalized) {
74
+ return null;
75
+ }
76
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(normalized) || /^[^@\s]+@[^:\s]+:[^\s]+$/i.test(normalized)) {
77
+ return normalized;
78
+ }
79
+ throw new Error(`${label} must look like a git remote URL`);
80
+ }
81
+
82
+ function normalizeMachineMetadata(rawMachine) {
83
+ if (rawMachine == null) {
84
+ return null;
85
+ }
86
+ if (!rawMachine || typeof rawMachine !== "object" || Array.isArray(rawMachine)) {
87
+ throw new Error("workspace manifest machine metadata must be an object");
88
+ }
89
+
90
+ const machine = Object.fromEntries(
91
+ Object.entries({
92
+ machineId: normalizeOptionalString(rawMachine.machineId),
93
+ machineLabel: normalizeOptionalString(rawMachine.machineLabel),
94
+ platform: normalizeOptionalString(rawMachine.platform),
95
+ host: normalizeOptionalString(rawMachine.host),
96
+ }).filter(([, value]) => value != null),
97
+ );
98
+
99
+ return Object.keys(machine).length > 0 ? machine : null;
100
+ }
101
+
67
102
  function normalizeResumeTool(value) {
68
103
  const trimmed = normalizeOptionalString(value)?.toLowerCase() || null;
69
104
  return trimmed && SUPPORTED_RESUME_TOOLS.has(trimmed) ? trimmed : null;
@@ -241,6 +276,9 @@ function normalizeStructuredTab(rawTab, index) {
241
276
  resumeTool: resume.resumeTool,
242
277
  title,
243
278
  tmuxSessionName,
279
+ remoteUrl: normalizeOptionalUrl(rawTab.remoteUrl, `workspace tab ${index + 1} remoteUrl`),
280
+ remoteBranch: normalizeOptionalString(rawTab.remoteBranch),
281
+ bootstrapCommand: normalizeOptionalCommand(rawTab.bootstrapCommand),
244
282
  };
245
283
  }
246
284
 
@@ -254,8 +292,8 @@ export function normalizeWorkspaceManifest(rawManifest) {
254
292
  throw new Error(`unsupported workspace manifest version: ${version}`);
255
293
  }
256
294
 
257
- if (!Array.isArray(rawManifest.tabs) || rawManifest.tabs.length === 0) {
258
- throw new Error("workspace manifest must include a non-empty tabs array");
295
+ if (!Array.isArray(rawManifest.tabs)) {
296
+ throw new Error("workspace manifest must include a tabs array");
259
297
  }
260
298
 
261
299
  const tabs = rawManifest.tabs.map((tab, index) => normalizeStructuredTab(tab, index));
@@ -264,6 +302,7 @@ export function normalizeWorkspaceManifest(rawManifest) {
264
302
  workspaceId: normalizeOptionalString(rawManifest.workspaceId),
265
303
  title: normalizeOptionalString(rawManifest.title),
266
304
  tmuxPrefix: normalizeOptionalString(rawManifest.tmuxPrefix),
305
+ machine: normalizeMachineMetadata(rawManifest.machine),
267
306
  capture: normalizeCaptureMetadata(rawManifest.capture),
268
307
  tabs,
269
308
  };
@@ -401,6 +440,37 @@ export function buildDirectCommand(entry, options = {}) {
401
440
  return commands.join(" && ");
402
441
  }
403
442
 
443
+ export function buildCloneCommand(entry, options = {}) {
444
+ const remoteUrl = normalizeOptionalUrl(entry?.remoteUrl, "workspace tab remoteUrl");
445
+ if (!remoteUrl) {
446
+ return null;
447
+ }
448
+ const repoDir = options.repoDir || path.basename(normalizeDisplayPath(entry.path));
449
+ const branch = normalizeOptionalString(entry?.remoteBranch);
450
+ const parts = ["git", "clone"];
451
+ if (branch) {
452
+ parts.push("--branch", shellQuote(branch));
453
+ }
454
+ parts.push(shellQuote(remoteUrl));
455
+ if (repoDir) {
456
+ parts.push(shellQuote(repoDir));
457
+ }
458
+ return parts.join(" ");
459
+ }
460
+
461
+ export function buildSetupCommand(entry, options = {}) {
462
+ const cloneCommand = buildCloneCommand(entry, options);
463
+ const bootstrapCommand = normalizeOptionalCommand(entry?.bootstrapCommand);
464
+ if (!cloneCommand && !bootstrapCommand) {
465
+ return null;
466
+ }
467
+ if (cloneCommand && bootstrapCommand) {
468
+ const repoDir = options.repoDir || path.basename(normalizeDisplayPath(entry.path));
469
+ return `${cloneCommand} && cd ${shellQuote(repoDir)} && ${bootstrapCommand}`;
470
+ }
471
+ return cloneCommand || bootstrapCommand;
472
+ }
473
+
404
474
  export function buildTmuxPresentationCommands(entry, options = {}) {
405
475
  const sessionName = options.sessionName || deriveTmuxSessionName(entry, options);
406
476
  const quotedSession = shellQuote(sessionName);
@@ -59,6 +59,9 @@ function normalizePreviousHostedTabs(workspace) {
59
59
  tabId: normalizeOptionalString(tab.tab_id ?? tab.tabId),
60
60
  title: normalizeOptionalString(tab.title),
61
61
  path: normalizeOptionalString(tab.project_root ?? tab.projectRoot),
62
+ remoteUrl: normalizeOptionalString(tab.remote_url ?? tab.remoteUrl),
63
+ remoteBranch: normalizeOptionalString(tab.remote_branch ?? tab.remoteBranch),
64
+ bootstrapCommand: normalizeOptionalString(tab.bootstrap_command ?? tab.bootstrapCommand),
62
65
  repoLabel: normalizeOptionalString(tab.repo_label ?? tab.repoLabel),
63
66
  terminalTitle: normalizeOptionalString(tab.terminal_title ?? tab.terminalTitle),
64
67
  resumeCommand: resume.resumeCommand,
@@ -149,6 +152,9 @@ export function buildHostedWorkspaceState(manifest, options = {}) {
149
152
  order_index: index,
150
153
  title: title || undefined,
151
154
  project_root: projectRoot,
155
+ remote_url: normalizeOptionalString(tab.remoteUrl) || previous?.remoteUrl || undefined,
156
+ remote_branch: normalizeOptionalString(tab.remoteBranch) || previous?.remoteBranch || undefined,
157
+ bootstrap_command: normalizeOptionalString(tab.bootstrapCommand) || previous?.bootstrapCommand || undefined,
152
158
  repo_label: repoLabel || undefined,
153
159
  resume_command: resumeCommand,
154
160
  resume_tool: resumeTool,
@@ -172,6 +178,9 @@ export function buildHostedWorkspaceState(manifest, options = {}) {
172
178
  source_app: "terminal-ledger",
173
179
  mode: "manual",
174
180
  host: os.hostname(),
181
+ machine_id: normalizeOptionalString(manifest.machine?.machineId) || undefined,
182
+ machine_label: normalizeOptionalString(manifest.machine?.machineLabel) || undefined,
183
+ platform: normalizeOptionalString(manifest.machine?.platform) || process.platform,
175
184
  terminal_frontend: "terminal",
176
185
  durable_backend: options.durableBackend || "manual-ledger",
177
186
  }).filter(([, value]) => value !== undefined && value !== null),
@@ -1,6 +1,8 @@
1
1
  export {
2
+ buildCloneCommand,
2
3
  buildDirectCommand,
3
4
  buildLaunchPlan,
5
+ buildSetupCommand,
4
6
  getResumeCommand,
5
7
  deriveBaseTitle,
6
8
  deriveWorkspaceId,
@@ -1,4 +1,5 @@
1
1
  import fs from "node:fs/promises";
2
+ import os from "node:os";
2
3
  import path from "node:path";
3
4
  import process from "node:process";
4
5
 
@@ -36,6 +37,16 @@ function normalizeOptionalString(value) {
36
37
  return trimmed.length > 0 ? trimmed : null;
37
38
  }
38
39
 
40
+ function buildCurrentMachineMetadata(options = {}) {
41
+ const machineId = normalizeOptionalString(options.machineId) || `${os.hostname().trim() || "machine"}:${process.platform}`;
42
+ return {
43
+ machineId,
44
+ machineLabel: normalizeOptionalString(options.machineLabel) || os.hostname().trim() || "This Machine",
45
+ platform: normalizeOptionalString(options.platform) || process.platform,
46
+ host: os.hostname().trim() || undefined,
47
+ };
48
+ }
49
+
39
50
  function validateAbsolutePath(value, label) {
40
51
  const normalized = normalizeOptionalString(value);
41
52
  if (!normalized || !normalized.startsWith("/")) {
@@ -65,6 +76,9 @@ function materializeWorkspaceTab(tab) {
65
76
  Object.entries({
66
77
  title: normalizeOptionalString(tab.title) || undefined,
67
78
  path: tab.path,
79
+ remoteUrl: normalizeOptionalString(tab.remoteUrl) || undefined,
80
+ remoteBranch: normalizeOptionalString(tab.remoteBranch) || undefined,
81
+ bootstrapCommand: normalizeOptionalString(tab.bootstrapCommand) || undefined,
68
82
  resumeCommand: resume.resumeCommand || undefined,
69
83
  resumeTool: resume.resumeTool || undefined,
70
84
  resumeSessionId: resume.resumeSessionId || undefined,
@@ -101,6 +115,7 @@ function materializeWorkspaceManifest(manifest) {
101
115
  version: normalized.version,
102
116
  workspaceId: normalized.workspaceId || undefined,
103
117
  title: normalized.title || undefined,
118
+ machine: normalized.machine || undefined,
104
119
  capture: normalized.capture || undefined,
105
120
  tabs: normalized.tabs.map((tab) => materializeWorkspaceTab(tab)),
106
121
  }).filter(([, value]) => value !== undefined),
@@ -113,6 +128,7 @@ function normalizeEditableManifest(source, parsed) {
113
128
  version: parsed.manifest.version,
114
129
  workspaceId: parsed.manifest.workspaceId,
115
130
  title: parsed.manifest.title,
131
+ machine: parsed.manifest.machine,
116
132
  capture: parsed.manifest.capture,
117
133
  tabs: parsed.manifest.tabs.map((entry) => {
118
134
  const resume = resolveResumeMetadata(entry);
@@ -120,6 +136,9 @@ function normalizeEditableManifest(source, parsed) {
120
136
  Object.entries({
121
137
  title: normalizeOptionalString(entry.title) || undefined,
122
138
  path: entry.path,
139
+ remoteUrl: normalizeOptionalString(entry.remoteUrl) || undefined,
140
+ remoteBranch: normalizeOptionalString(entry.remoteBranch) || undefined,
141
+ bootstrapCommand: normalizeOptionalString(entry.bootstrapCommand) || undefined,
123
142
  resumeCommand: resume.resumeCommand || undefined,
124
143
  resumeTool: resume.resumeTool || undefined,
125
144
  resumeSessionId: resume.resumeSessionId || undefined,
@@ -133,6 +152,7 @@ function normalizeEditableManifest(source, parsed) {
133
152
  version: "1",
134
153
  workspaceId: source.workspaceManifest?.workspaceId || source.title || "workspace",
135
154
  title: source.workspaceManifest?.title || source.title || null,
155
+ machine: source.workspaceManifest?.machine || null,
136
156
  capture: source.workspaceManifest?.capture || null,
137
157
  tabs: parsed.entries.map((entry) => {
138
158
  const resume = resolveResumeMetadata(entry);
@@ -140,6 +160,9 @@ function normalizeEditableManifest(source, parsed) {
140
160
  Object.entries({
141
161
  title: normalizeOptionalString(entry.title) || deriveBaseTitle(entry),
142
162
  path: entry.path,
163
+ remoteUrl: normalizeOptionalString(entry.remoteUrl) || undefined,
164
+ remoteBranch: normalizeOptionalString(entry.remoteBranch) || undefined,
165
+ bootstrapCommand: normalizeOptionalString(entry.bootstrapCommand) || undefined,
143
166
  resumeCommand: resume.resumeCommand || undefined,
144
167
  resumeTool: resume.resumeTool || undefined,
145
168
  resumeSessionId: resume.resumeSessionId || undefined,
@@ -216,6 +239,12 @@ function parseLedgerSelectorArgs(
216
239
  options.resumeTool = next;
217
240
  } else if (arg === "--resume-session-id") {
218
241
  options.resumeSessionId = next;
242
+ } else if (arg === "--remote-url") {
243
+ options.remoteUrl = next;
244
+ } else if (arg === "--remote-branch") {
245
+ options.remoteBranch = next;
246
+ } else if (arg === "--bootstrap-command") {
247
+ options.bootstrapCommand = next;
219
248
  } else if (arg === "--index") {
220
249
  options.index = next;
221
250
  } else {
@@ -312,6 +341,18 @@ export function parseWorkspaceCreateArgs(argv = []) {
312
341
  options.resumeTool = next;
313
342
  } else if (arg === "--resume-session-id") {
314
343
  options.resumeSessionId = next;
344
+ } else if (arg === "--remote-url") {
345
+ options.remoteUrl = next;
346
+ } else if (arg === "--remote-branch") {
347
+ options.remoteBranch = next;
348
+ } else if (arg === "--bootstrap-command") {
349
+ options.bootstrapCommand = next;
350
+ } else if (arg === "--machine-id") {
351
+ options.machineId = next;
352
+ } else if (arg === "--machine-label") {
353
+ options.machineLabel = next;
354
+ } else if (arg === "--platform") {
355
+ options.platform = next;
315
356
  } else {
316
357
  throw new Error(`unknown option: ${arg}`);
317
358
  }
@@ -392,6 +433,13 @@ export function addTabToManifest(manifest, options = {}) {
392
433
  Object.entries({
393
434
  title: normalizedTitle || normalizeOptionalString(existingTab?.title) || undefined,
394
435
  path: normalizedPath,
436
+ remoteUrl: normalizeOptionalString(options.remoteUrl) || normalizeOptionalString(existingTab?.remoteUrl) || undefined,
437
+ remoteBranch:
438
+ normalizeOptionalString(options.remoteBranch) || normalizeOptionalString(existingTab?.remoteBranch) || undefined,
439
+ bootstrapCommand:
440
+ normalizeOptionalString(options.bootstrapCommand) ||
441
+ normalizeOptionalString(existingTab?.bootstrapCommand) ||
442
+ undefined,
395
443
  resumeCommand: chosenResume.resumeCommand || undefined,
396
444
  resumeTool: chosenResume.resumeTool || undefined,
397
445
  resumeSessionId: chosenResume.resumeSessionId || undefined,
@@ -607,7 +655,7 @@ function printWorkspaceAddTabHelp() {
607
655
  console.log(`ORP workspace add-tab
608
656
 
609
657
  Usage:
610
- orp workspace add-tab <name-or-id> (--path <absolute-path> | --here) [--title <title>] [--resume-command <text> | --resume-tool <codex|claude> --resume-session-id <id> | --current-codex] [--append] [--json]
658
+ orp workspace add-tab <name-or-id> (--path <absolute-path> | --here) [--title <title>] [--remote-url <git-url>] [--remote-branch <branch>] [--bootstrap-command <text>] [--resume-command <text> | --resume-tool <codex|claude> --resume-session-id <id> | --current-codex] [--append] [--json]
611
659
  orp workspace add-tab --hosted-workspace-id <workspace-id> (--path <absolute-path> | --here) [--json]
612
660
  orp workspace add-tab --workspace-file <path> (--path <absolute-path> | --here) [--json]
613
661
 
@@ -615,6 +663,9 @@ Options:
615
663
  --path <absolute-path> Add this local project path to the saved workspace
616
664
  --here Use the current working directory as the saved path
617
665
  --title <title> Optional saved tab title
666
+ --remote-url <git-url> Optional git remote URL for cross-machine setup
667
+ --remote-branch <branch> Optional default branch to clone on another machine
668
+ --bootstrap-command <text> Optional setup command like \`npm install\` or \`uv sync\`
618
669
  --resume-command <text> Exact saved resume command, like \`codex resume ...\` or \`claude --resume ...\`
619
670
  --resume-tool <tool> Build the resume command from \`codex\` or \`claude\`
620
671
  --resume-session-id <id> Resume session id to save with the tab
@@ -630,6 +681,7 @@ Examples:
630
681
  orp workspace add-tab main --here --current-codex
631
682
  orp workspace add-tab main --path /absolute/path/to/new-project --resume-command "codex resume 019d..."
632
683
  orp workspace add-tab main --path /absolute/path/to/new-project --resume-tool claude --resume-session-id claude-456
684
+ orp workspace add-tab main --path /absolute/path/to/new-project --remote-url git@github.com:org/new-project.git --bootstrap-command "npm install"
633
685
  `);
634
686
  }
635
687
 
@@ -637,13 +689,19 @@ function printWorkspaceCreateHelp() {
637
689
  console.log(`ORP workspace create
638
690
 
639
691
  Usage:
640
- orp workspace create <title-slug> [--workspace-file <path>] [--slot <main|offhand>] [--path <absolute-path>] [--resume-command <text> | --resume-tool <codex|claude> --resume-session-id <id>] [--json]
692
+ orp workspace create <title-slug> [--workspace-file <path>] [--slot <main|offhand>] [--machine-id <id>] [--machine-label <label>] [--platform <platform>] [--path <absolute-path>] [--remote-url <git-url>] [--remote-branch <branch>] [--bootstrap-command <text>] [--resume-command <text> | --resume-tool <codex|claude> --resume-session-id <id>] [--json]
641
693
 
642
694
  Options:
643
695
  <title-slug> Required local workspace title using lowercase letters, numbers, and dashes only
644
696
  --workspace-file <path> Create the workspace manifest at an explicit local path instead of the managed ORP workspace directory
645
697
  --slot <main|offhand> Optionally assign the created workspace to a named slot
698
+ --machine-id <id> Optional stable machine id for this workspace ledger (defaults to this machine)
699
+ --machine-label <label> Optional human label for the current machine
700
+ --platform <platform> Optional platform label like darwin, linux, or win32
646
701
  --path <absolute-path> Optionally seed the workspace with one saved path immediately
702
+ --remote-url <git-url> Optional git remote URL for the first saved tab
703
+ --remote-branch <branch> Optional default branch to clone on another machine
704
+ --bootstrap-command <text> Optional setup command like \`npm install\` or \`uv sync\`
647
705
  --resume-command <text> Exact saved resume command, like \`codex resume ...\` or \`claude --resume ...\`
648
706
  --resume-tool <tool> Build the resume command from \`codex\` or \`claude\`
649
707
  --resume-session-id <id> Resume session id to save with the first tab
@@ -655,6 +713,7 @@ Examples:
655
713
  orp workspace create main-cody-1 --slot main
656
714
  orp workspace create research-lab --path /absolute/path/to/research-lab
657
715
  orp workspace create research-lab --path /absolute/path/to/research-lab --resume-tool claude --resume-session-id 469d99b2-2997-42bf-a8f5-3812c808ef29
716
+ orp workspace create mac-main --machine-label "Mac Studio" --path /absolute/path/to/research-lab --remote-url git@github.com:org/research-lab.git --bootstrap-command "npm install"
658
717
  `);
659
718
  }
660
719
 
@@ -697,6 +756,12 @@ function summarizeWorkspaceLedgerMutation(result) {
697
756
  if (result.action === "add-tab") {
698
757
  lines.push(`Tab: ${result.tab?.title || path.basename(result.tab?.path || "") || result.tab?.path}`);
699
758
  lines.push(`Path: ${result.tab?.path}`);
759
+ if (result.tab?.remoteUrl) {
760
+ lines.push(`Remote: ${result.tab.remoteUrl}`);
761
+ }
762
+ if (result.tab?.bootstrapCommand) {
763
+ lines.push(`Bootstrap: ${result.tab.bootstrapCommand}`);
764
+ }
700
765
  if (result.tab?.resumeCommand && result.tab?.restartCommand) {
701
766
  lines.push(`Resume: ${result.tab.restartCommand}`);
702
767
  }
@@ -792,6 +857,9 @@ export async function runWorkspaceCreate(argv = process.argv.slice(2)) {
792
857
  Object.entries({
793
858
  title: deriveBaseTitle({ path: options.path }),
794
859
  path: options.path,
860
+ remoteUrl: normalizeOptionalString(options.remoteUrl) || undefined,
861
+ remoteBranch: normalizeOptionalString(options.remoteBranch) || undefined,
862
+ bootstrapCommand: normalizeOptionalString(options.bootstrapCommand) || undefined,
795
863
  resumeCommand: resume.resumeCommand || undefined,
796
864
  resumeTool: resume.resumeTool || undefined,
797
865
  resumeSessionId: resume.resumeSessionId || undefined,
@@ -806,6 +874,7 @@ export async function runWorkspaceCreate(argv = process.argv.slice(2)) {
806
874
  version: "1",
807
875
  workspaceId: options.title,
808
876
  title: options.title,
877
+ machine: buildCurrentMachineMetadata(options),
809
878
  tabs,
810
879
  });
811
880
 
@@ -869,6 +938,9 @@ export async function runWorkspaceCreate(argv = process.argv.slice(2)) {
869
938
  `Saved tabs: ${result.tabCount}`,
870
939
  `Saved file: ${result.manifestPath}`,
871
940
  ];
941
+ if (result.manifest?.machine?.machineLabel) {
942
+ lines.push(`Machine: ${result.manifest.machine.machineLabel}${result.manifest.machine.platform ? ` (${result.manifest.machine.platform})` : ""}`);
943
+ }
872
944
  if (result.slot?.slot) {
873
945
  lines.push(`Slot: ${result.slot.slot}`);
874
946
  }
@@ -52,6 +52,10 @@ function buildHostedWorkspaceSummary(workspace) {
52
52
  return {
53
53
  workspaceId: normalizeOptionalString(workspace?.workspace_id ?? workspace?.id),
54
54
  title: normalizeOptionalString(workspace?.title),
55
+ machineId: normalizeOptionalString(workspace?.state?.capture_context?.machine_id ?? workspace?.state?.captureContext?.machineId),
56
+ machineLabel:
57
+ normalizeOptionalString(workspace?.state?.capture_context?.machine_label ?? workspace?.state?.captureContext?.machineLabel),
58
+ platform: normalizeOptionalString(workspace?.state?.capture_context?.platform ?? workspace?.state?.captureContext?.platform),
55
59
  visibility: normalizeOptionalString(workspace?.visibility),
56
60
  ideaId: normalizeOptionalString(linkedIdea.idea_id ?? linkedIdea.ideaId),
57
61
  tabCount:
@@ -66,6 +70,9 @@ function buildLocalWorkspaceSummary(workspace) {
66
70
  return {
67
71
  workspaceId: normalizeOptionalString(workspace?.workspaceId),
68
72
  title: normalizeOptionalString(workspace?.title),
73
+ machineId: normalizeOptionalString(workspace?.machineId),
74
+ machineLabel: normalizeOptionalString(workspace?.machineLabel),
75
+ platform: normalizeOptionalString(workspace?.platform),
69
76
  status: normalizeOptionalString(workspace?.status) || "ok",
70
77
  manifestPath: normalizeOptionalString(workspace?.manifestPath),
71
78
  tabCount: Number.isInteger(workspace?.tabCount) ? workspace.tabCount : 0,
@@ -244,6 +251,9 @@ export function buildWorkspaceInventory({ localResult, hostedResult, hostedError
244
251
  availability:
245
252
  entry.hosted && entry.local ? "hosted+local" : entry.hosted ? "hosted" : "local",
246
253
  syncStatus: inferSyncStatus(entry),
254
+ machineId: entry.local?.machineId || entry.hosted?.machineId || null,
255
+ machineLabel: entry.local?.machineLabel || entry.hosted?.machineLabel || null,
256
+ platform: entry.local?.platform || entry.hosted?.platform || null,
247
257
  sources: entry.sources,
248
258
  hosted: entry.hosted || null,
249
259
  local: entry.local || null,
@@ -308,6 +318,11 @@ export function summarizeWorkspaceInventory(result) {
308
318
  `Slots: ${workspace.slots.join(", ")}${workspace.implicitMain ? " (main inferred because this is the only workspace)" : ""}`,
309
319
  );
310
320
  }
321
+ if (workspace.machineLabel || workspace.machineId) {
322
+ lines.push(
323
+ `Machine: ${workspace.machineLabel || workspace.machineId}${workspace.platform ? ` (${workspace.platform})` : ""}`,
324
+ );
325
+ }
311
326
  lines.push(`Availability: ${workspace.availability}`);
312
327
  lines.push(`Sync: ${workspace.syncStatus}`);
313
328
  if (workspace.hosted) {
@@ -400,6 +415,9 @@ export function summarizeTrackedWorkspaces(result) {
400
415
  lines.push(`Status: ${workspace.status}`);
401
416
  lines.push(`File: ${workspace.manifestPath}`);
402
417
  lines.push(`Saved tabs: ${workspace.tabCount || 0}`);
418
+ if (workspace.machineLabel || workspace.machineId) {
419
+ lines.push(`Machine: ${workspace.machineLabel || workspace.machineId}${workspace.platform ? ` (${workspace.platform})` : ""}`);
420
+ }
403
421
 
404
422
  if (workspace.captureMode) {
405
423
  lines.push(`Capture mode: ${workspace.captureMode}`);
@@ -8,18 +8,18 @@ function printWorkspaceHelp() {
8
8
  console.log(`ORP workspace
9
9
 
10
10
  Usage:
11
- orp workspace create <title-slug> [--workspace-file <path>] [--slot <main|offhand>] [--path <absolute-path>] [--resume-command <text> | --resume-tool <codex|claude> --resume-session-id <id>] [--json]
11
+ orp workspace create <title-slug> [--workspace-file <path>] [--slot <main|offhand>] [--machine-id <id>] [--machine-label <label>] [--platform <platform>] [--path <absolute-path>] [--remote-url <git-url>] [--remote-branch <branch>] [--bootstrap-command <text>] [--resume-command <text> | --resume-tool <codex|claude> --resume-session-id <id>] [--json]
12
12
  orp workspace list [--json]
13
13
  orp workspace tabs <name-or-id> [--json]
14
14
  orp workspace tabs --hosted-workspace-id <workspace-id> [--json]
15
15
  orp workspace tabs --notes-file <path> [--json]
16
16
  orp workspace tabs --workspace-file <path> [--json]
17
- orp workspace add-tab <name-or-id> (--path <absolute-path> | --here) [--title <title>] [--resume-command <text> | --resume-tool <codex|claude> --resume-session-id <id> | --current-codex] [--append] [--json]
17
+ orp workspace add-tab <name-or-id> (--path <absolute-path> | --here) [--title <title>] [--remote-url <git-url>] [--remote-branch <branch>] [--bootstrap-command <text>] [--resume-command <text> | --resume-tool <codex|claude> --resume-session-id <id> | --current-codex] [--append] [--json]
18
18
  orp workspace remove-tab <name-or-id> (--index <n> | --path <absolute-path> | --title <title> | --resume-session-id <id> | --resume-command <text>) [--all] [--json]
19
19
  orp workspace slot <list|set|clear> ...
20
20
  orp workspace sync <name-or-id> [--workspace-file <path> | --notes-file <path>] [--dry-run] [--json]
21
21
  orp workspace ledger <name-or-id> [--json]
22
- orp workspace ledger add <name-or-id> (--path <absolute-path> | --here) [--title <title>] [--resume-command <text> | --resume-tool <codex|claude> --resume-session-id <id> | --current-codex] [--append] [--json]
22
+ orp workspace ledger add <name-or-id> (--path <absolute-path> | --here) [--title <title>] [--remote-url <git-url>] [--remote-branch <branch>] [--bootstrap-command <text>] [--resume-command <text> | --resume-tool <codex|claude> --resume-session-id <id> | --current-codex] [--append] [--json]
23
23
  orp workspace ledger remove <name-or-id> (--index <n> | --path <absolute-path> | --title <title> | --resume-session-id <id> | --resume-command <text>) [--all] [--json]
24
24
  orp workspace -h
25
25
 
@@ -36,7 +36,9 @@ Commands:
36
36
  Notes:
37
37
  - Local-only usage works: create a workspace with \`orp workspace create <title-slug>\`, then use \`orp workspace tabs ...\`, \`orp workspace add-tab ...\`, and \`orp workspace remove-tab ...\` without authenticating.
38
38
  - Use \`orp workspace list\` for the combined hosted + local workspace inventory.
39
- - Use \`orp workspace tabs <workspace>\` when you want saved paths plus copyable \`cd ... && codex resume ...\` / \`claude --resume ...\` recovery lines.
39
+ - Use \`orp workspace tabs <workspace>\` when you want saved paths plus copyable \`cd ... && codex resume ...\` / \`claude --resume ...\` recovery lines, along with optional remote repo and bootstrap metadata for another machine.
40
+ - Use \`orp workspace create ... --machine-label ...\` when you want the workspace ledger to stay clearly tied to one rig.
41
+ - Use \`orp workspace add-tab ... --remote-url ... --bootstrap-command ...\` when you want ORP to remember how to recreate the repo on another machine.
40
42
  - Use \`orp workspace add-tab ...\` and \`orp workspace remove-tab ...\` when you want to edit the saved workspace ledger explicitly from Terminal.app or any other shell.
41
43
  - If you prefer the older ledger-prefixed wording, \`orp workspace ledger\`, \`orp workspace ledger add\`, and \`orp workspace ledger remove\` stay available as aliases.
42
44
  - \`main\` and \`offhand\` are reserved slot selectors; use \`orp workspace slot set ...\` to assign them.
@@ -46,10 +48,12 @@ Notes:
46
48
  Examples:
47
49
  orp workspace create main-cody-1
48
50
  orp workspace create main-cody-1 --slot main
51
+ orp workspace create mac-main --machine-label "Mac Studio" --path /absolute/path/to/orp --remote-url git@github.com:SproutSeeds/orp.git --bootstrap-command "npm install"
49
52
  orp workspace list
50
53
  orp workspace tabs main-cody-1
51
54
  orp workspace add-tab main --here --current-codex
52
55
  orp workspace add-tab main --path /absolute/path/to/new-project --resume-command "codex resume 019d..."
56
+ orp workspace add-tab main --path /absolute/path/to/new-project --remote-url git@github.com:org/new-project.git --bootstrap-command "npm install"
53
57
  orp workspace remove-tab main --path /absolute/path/to/frg-site --resume-session-id 019d348d-5031-78e1-9840-a66deaac33ae
54
58
  orp workspace slot set main main-cody-1
55
59
  orp workspace slot set offhand research-lab
@@ -627,6 +627,16 @@ export function buildWorkspaceManifestFromHostedWorkspacePayload(payload) {
627
627
  version: "1",
628
628
  workspaceId: getTextValue(workspace, "workspace_id", "id"),
629
629
  title: getTextValue(workspace, "title"),
630
+ machine: captureContext
631
+ ? Object.fromEntries(
632
+ Object.entries({
633
+ machineId: getTextValue(captureContext, "machine_id", "machineId"),
634
+ machineLabel: getTextValue(captureContext, "machine_label", "machineLabel"),
635
+ platform: getTextValue(captureContext, "platform"),
636
+ host: getTextValue(captureContext, "host"),
637
+ }).filter(([, value]) => value !== undefined && value !== null),
638
+ )
639
+ : undefined,
630
640
  capture: captureContext
631
641
  ? Object.fromEntries(
632
642
  Object.entries({
@@ -671,6 +681,9 @@ export function buildWorkspaceManifestFromHostedWorkspacePayload(payload) {
671
681
  path.basename(String(getTextValue(tab, "project_root", "projectRoot") || "").replace(/\/+$/, "")) ||
672
682
  undefined,
673
683
  path: getTextValue(tab, "project_root", "projectRoot"),
684
+ remoteUrl: getTextValue(tab, "remote_url", "remoteUrl"),
685
+ remoteBranch: getTextValue(tab, "remote_branch", "remoteBranch"),
686
+ bootstrapCommand: getTextValue(tab, "bootstrap_command", "bootstrapCommand"),
674
687
  resumeCommand: getTextValue(tab, "resume_command", "resumeCommand"),
675
688
  resumeTool: getTextValue(tab, "resume_tool", "resumeTool"),
676
689
  resumeSessionId: getTextValue(tab, "resume_session_id", "resumeSessionId"),
@@ -78,6 +78,9 @@ function normalizeRegistryEntry(rawEntry) {
78
78
  manifestPath,
79
79
  workspaceId: normalizeOptionalString(rawEntry.workspaceId) ?? undefined,
80
80
  title: normalizeOptionalString(rawEntry.title) ?? undefined,
81
+ machineId: normalizeOptionalString(rawEntry.machineId) ?? undefined,
82
+ machineLabel: normalizeOptionalString(rawEntry.machineLabel) ?? undefined,
83
+ platform: normalizeOptionalString(rawEntry.platform) ?? undefined,
81
84
  host: normalizeOptionalString(rawEntry.host) ?? undefined,
82
85
  captureMode: normalizeOptionalString(rawEntry.captureMode) ?? undefined,
83
86
  capturedAt: normalizeOptionalString(rawEntry.capturedAt) ?? undefined,
@@ -312,6 +315,9 @@ export function summarizeManifestForRegistry(manifestPath, manifest) {
312
315
  manifestPath: normalizedPath,
313
316
  workspaceId: normalizeOptionalString(manifest.workspaceId) ?? undefined,
314
317
  title: normalizeOptionalString(manifest.title) ?? undefined,
318
+ machineId: normalizeOptionalString(manifest.machine?.machineId) ?? undefined,
319
+ machineLabel: normalizeOptionalString(manifest.machine?.machineLabel) ?? undefined,
320
+ platform: normalizeOptionalString(manifest.machine?.platform) ?? undefined,
315
321
  host: normalizeOptionalString(manifest.capture?.host) ?? undefined,
316
322
  captureMode: normalizeOptionalString(manifest.capture?.mode) ?? undefined,
317
323
  capturedAt: normalizeOptionalString(manifest.capture?.capturedAt) ?? undefined,
@@ -337,6 +343,9 @@ function serializeManagedWorkspaceManifest(manifest) {
337
343
  Object.entries({
338
344
  title: normalizeOptionalString(tab.title) ?? undefined,
339
345
  path: normalizeOptionalString(tab.path) ?? undefined,
346
+ remoteUrl: normalizeOptionalString(tab.remoteUrl) ?? undefined,
347
+ remoteBranch: normalizeOptionalString(tab.remoteBranch) ?? undefined,
348
+ bootstrapCommand: normalizeOptionalString(tab.bootstrapCommand) ?? undefined,
340
349
  resumeCommand: resumeCommand ?? undefined,
341
350
  resumeTool: resumeTool ?? undefined,
342
351
  resumeSessionId: resumeSessionId ?? undefined,
@@ -353,6 +362,7 @@ function serializeManagedWorkspaceManifest(manifest) {
353
362
  version: normalized.version,
354
363
  workspaceId: normalizeOptionalString(normalized.workspaceId) ?? undefined,
355
364
  title: normalizeOptionalString(normalized.title) ?? undefined,
365
+ machine: normalized.machine || undefined,
356
366
  capture: normalized.capture || undefined,
357
367
  tabs,
358
368
  }).filter(([, value]) => value !== undefined),
@@ -191,6 +191,9 @@ function serializeWorkspaceManifest(manifest) {
191
191
  Object.entries({
192
192
  title: normalizeOptionalString(entry.title) ?? undefined,
193
193
  path: String(entry.path).trim(),
194
+ remoteUrl: normalizeOptionalString(entry.remoteUrl) ?? undefined,
195
+ remoteBranch: normalizeOptionalString(entry.remoteBranch) ?? undefined,
196
+ bootstrapCommand: normalizeOptionalString(entry.bootstrapCommand) ?? undefined,
194
197
  resumeCommand: normalizeOptionalString(entry.resumeCommand) ?? undefined,
195
198
  resumeTool: normalizeOptionalString(entry.resumeTool) ?? undefined,
196
199
  resumeSessionId: normalizeOptionalString(entry.resumeSessionId ?? entry.sessionId) ?? undefined,
@@ -211,6 +214,7 @@ function serializeWorkspaceManifest(manifest) {
211
214
  version: WORKSPACE_SCHEMA_VERSION,
212
215
  workspaceId: normalizeOptionalString(manifest.workspaceId) ?? undefined,
213
216
  title: normalizeOptionalString(manifest.title) ?? undefined,
217
+ machine: manifest.machine ?? undefined,
214
218
  tabs,
215
219
  }).filter(([, value]) => value !== undefined),
216
220
  );
@@ -231,9 +235,13 @@ export function buildWorkspaceSyncPreview({ source, parsed, targetIdea, workspac
231
235
  version: parsed.manifest.version || WORKSPACE_SCHEMA_VERSION,
232
236
  workspaceId: parsed.manifest.workspaceId || workspaceTitle || deriveWorkspaceId(source, parsed),
233
237
  title: workspaceTitle || parsed.manifest.title || null,
238
+ machine: parsed.manifest.machine || null,
234
239
  tabs: parsed.manifest.tabs.map((entry) => ({
235
240
  title: entry.title || deriveBaseTitle(entry),
236
241
  path: entry.path,
242
+ remoteUrl: entry.remoteUrl || null,
243
+ remoteBranch: entry.remoteBranch || null,
244
+ bootstrapCommand: entry.bootstrapCommand || null,
237
245
  resumeCommand: entry.resumeCommand || null,
238
246
  resumeTool: entry.resumeTool || null,
239
247
  resumeSessionId: entry.sessionId || null,
@@ -245,9 +253,13 @@ export function buildWorkspaceSyncPreview({ source, parsed, targetIdea, workspac
245
253
  version: WORKSPACE_SCHEMA_VERSION,
246
254
  workspaceId: workspaceTitle || deriveWorkspaceId(source, parsed),
247
255
  title: workspaceTitle || null,
256
+ machine: null,
248
257
  tabs: parsed.entries.map((entry) => ({
249
258
  title: deriveBaseTitle(entry),
250
259
  path: entry.path,
260
+ remoteUrl: entry.remoteUrl || null,
261
+ remoteBranch: entry.remoteBranch || null,
262
+ bootstrapCommand: entry.bootstrapCommand || null,
251
263
  resumeCommand: getResumeCommand(entry),
252
264
  resumeTool: resolveResumeMetadata(entry).resumeTool,
253
265
  resumeSessionId: resolveResumeMetadata(entry).resumeSessionId,