open-research-protocol 0.4.24 → 0.4.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,68 @@
1
+ # Agent Runtime Borrowing Notes
2
+
3
+ ORP should watch fast-moving personal-agent runtimes such as Hermes Agent and
4
+ OpenClaw as design references, not as replacements for the ORP state model.
5
+ Their strongest lesson is that users want one reachable assistant surface across
6
+ CLI, mobile, messaging, schedulers, and remote machines. ORP's role is to make
7
+ that assistant surface durable, governable, and recoverable.
8
+
9
+ The architectural boundary is:
10
+
11
+ ```text
12
+ Clawdad = entry point and operator surface
13
+ ORP = durable workspace state, routing ledger, agenda, governance, packets, and checkpoints
14
+ Agent runtimes = execution backends, gateways, sandboxes, schedulers, or transports
15
+ Project artifacts = evidence
16
+ ```
17
+
18
+ Hermes/OpenClaw-style systems can inspire ORP features, but they should not
19
+ become parallel ledgers. ORP remains the canonical place for workspace tabs,
20
+ linked sessions, runner state, operating agendas, opportunities, connections,
21
+ repo governance, checkpoints, and research packets.
22
+
23
+ ## Ideas Worth Borrowing
24
+
25
+ - Gateway ergonomics: simplify setup for phone, chat, and always-on entrypoints
26
+ while preserving ORP's local-first and hosted-linked records.
27
+ - Skills and capability packs: expose small, auditable ORP command groups that
28
+ agents can load for specific jobs instead of handing them the whole machine.
29
+ - Background process signals: let long-running builds, scans, and research jobs
30
+ notify the current agent/session when they finish or hit watched output.
31
+ - Model and provider routing: study runtime-level provider switching while
32
+ keeping ORP's routing records independent of any one model vendor.
33
+ - Subagent isolation: borrow fresh-context worker patterns, but record only the
34
+ resulting task state, evidence paths, and handoff summaries in ORP.
35
+ - Local dashboards: use dashboards as visibility layers over ORP state, not as
36
+ a second source of workspace truth.
37
+ - Backup/import flows: make ORP's machine state, linked sessions, and local
38
+ workspace ledgers easier to inspect, export, and restore.
39
+ - Security hardening: preserve strict boundaries for remote control, including
40
+ allowlists, sandboxed command execution, explicit secret scoping, and clear
41
+ approval points.
42
+
43
+ ## Design Guardrails
44
+
45
+ - ORP files are process-only and remain separate from evidence.
46
+ - Messaging platforms must not own the durable agenda or project ledger.
47
+ - Agent memories may summarize preferences or conversation context, but ORP owns
48
+ project routing, governance, and operational state.
49
+ - Any borrowed gateway or scheduler behavior should write back to ORP through
50
+ explicit commands, not mutate hidden state.
51
+ - A new surface is acceptable only if an operator can still recover the work
52
+ from ORP without knowing which agent runtime handled it.
53
+
54
+ ## First Useful Adapter
55
+
56
+ A good borrowing experiment is an ORP skill or bridge for an external agent
57
+ runtime with read-first commands:
58
+
59
+ - `orp home --json`
60
+ - `orp agenda focus`
61
+ - `orp workspace tabs main`
62
+ - `orp runner status --json`
63
+ - `orp link status --json`
64
+ - `orp youtube inspect <url> --json`
65
+
66
+ The next layer can add carefully scoped writes such as registering a session,
67
+ emitting a checkpoint, or dispatching through Clawdad, but only after the read
68
+ surface proves useful and safe.
@@ -0,0 +1,123 @@
1
+ # ORP Research Council
2
+
3
+ ORP research council runs turn one question into a durable, tool-callable research artifact. The default profile is OpenAI-only right now, so one saved ORP key can power the full loop:
4
+
5
+ ```bash
6
+ orp research ask "Where should this system live?" --json
7
+ ```
8
+
9
+ By default this is a dry run. ORP writes the decomposition, profile, lane plan, lane JSON files, synthesized planning answer, and summary under:
10
+
11
+ ```text
12
+ orp/research/<run_id>/
13
+ ```
14
+
15
+ Live provider calls require an explicit flag:
16
+
17
+ ```bash
18
+ orp research ask "Where should this system live?" --execute --json
19
+ ```
20
+
21
+ ## Lanes
22
+
23
+ The built-in `openai-council` profile defines three OpenAI API lanes:
24
+
25
+ - `openai_reasoning_high`: `gpt-5.4` with `reasoning.effort=high` for the deliberate thinking pass.
26
+ - `openai_web_synthesis`: `gpt-5.4` with high reasoning plus Responses API web search for current public evidence and citations.
27
+ - `openai_deep_research`: `o3-deep-research-2025-06-26` with background execution and web search preview for Pro/Deep Research style investigation.
28
+
29
+ This follows OpenAI's current model guidance: `gpt-5.4` is the default for general-purpose, coding, reasoning, and agentic workflows; web search is enabled through the Responses API `tools` array when current information is needed; and Deep Research is available through the Responses endpoint with `o3-deep-research-2025-06-26`.
30
+
31
+ ## API Call Moments
32
+
33
+ ORP records when API keys are intended to be used:
34
+
35
+ - `plan`: local decomposition only. No API key is resolved.
36
+ - `thinking_reasoning_high`: resolve `openai-primary` immediately before the `openai_reasoning_high` lane.
37
+ - `web_synthesis`: resolve `openai-primary` immediately before the `openai_web_synthesis` lane.
38
+ - `pro_deep_research`: resolve `openai-primary` immediately before the `openai_deep_research` lane.
39
+
40
+ Dry runs write every lane with `api_call.called=false`. Live runs require `--execute`; even then, secret values are read only at the lane call moment and are not written to artifacts.
41
+
42
+ Secret values are read from environment variables first. If an env var is missing and a matching ORP Keychain secret is available, ORP can use it at execution time. Secret values are not persisted in artifacts.
43
+
44
+ The default live profile expects this ORP secret alias or env var:
45
+
46
+ - `openai-primary` / `OPENAI_API_KEY`
47
+
48
+ Store a local machine copy without the hosted secret API like this:
49
+
50
+ ```bash
51
+ printf '%s' '<openai-key>' | orp secrets keychain-add \
52
+ --alias openai-primary \
53
+ --label "OpenAI Primary" \
54
+ --provider openai \
55
+ --value-stdin \
56
+ --json
57
+ ```
58
+
59
+ ## Fixtures
60
+
61
+ Provider outputs can be attached without spending live calls:
62
+
63
+ ```bash
64
+ orp research ask "Where should this live?" \
65
+ --lane-fixture openai_reasoning_high=reports/reasoning.json \
66
+ --lane-fixture openai_web_synthesis=reports/web.txt \
67
+ --json
68
+ ```
69
+
70
+ Fixtures are useful when an OpenAI run happened outside ORP, when you are comparing model settings manually, or when tests need deterministic lane outputs.
71
+
72
+ ## OpenAI API Notes
73
+
74
+ ORP uses the Responses API for these lanes. Useful knobs in profile JSON:
75
+
76
+ - `model`: for example `gpt-5.4` or `o3-deep-research-2025-06-26`.
77
+ - `call_moment`: the named research-loop moment when this lane may resolve a key.
78
+ - `reasoning_effort`: `none`, `low`, `medium`, `high`, or `xhigh` for supported models.
79
+ - `reasoning_summary`: `auto` or `detailed` for Deep Research reasoning summaries.
80
+ - `text_verbosity`: `low`, `medium`, or `high`.
81
+ - `web_search`: `true` to add the Responses API web-search tool.
82
+ - `search_context_size`: `low`, `medium`, or `high` for web search.
83
+ - `background`: `true` for long-running Deep Research calls.
84
+ - `max_output_tokens`: hard cap for a lane response.
85
+
86
+ The default profile deliberately avoids Anthropic, xAI, and local-model lanes so a single OpenAI key is enough.
87
+
88
+ ## Project Context Timing
89
+
90
+ `orp init` creates `orp/project.json`, a process-only project context lens for the current directory. It records the authority surfaces ORP can see, the directory signals it should route on, and the default research timing policy:
91
+
92
+ - decompose locally first
93
+ - use high-reasoning API calls when a decision gate or ambiguous next action needs outside reasoning
94
+ - use web synthesis when current public facts, docs, papers, project status, or citations matter
95
+ - use Deep Research only after reasoning/web lanes expose a research-heavy gap, disagreement, source-quality issue, or literature-scale synthesis need
96
+
97
+ Run `orp project refresh --json` after adding or changing roadmap, spec, agent-guidance, docs, manifest, or command-surface files. Refreshing project context does not call a provider; live provider calls remain explicit through `orp research ask --execute`.
98
+
99
+ ## Follow-Up Commands
100
+
101
+ ```bash
102
+ orp project show --json
103
+ orp project refresh --json
104
+ orp research status latest --json
105
+ orp research show latest --json
106
+ ```
107
+
108
+ ## Codex MCP Tool
109
+
110
+ ORP also ships a tiny stdio MCP wrapper for the research commands:
111
+
112
+ ```toml
113
+ [mcp_servers.orp-research]
114
+ command = "/path/to/orp/scripts/orp-mcp"
115
+ ```
116
+
117
+ It exposes:
118
+
119
+ - `orp_research_ask`
120
+ - `orp_research_status`
121
+ - `orp_research_show`
122
+
123
+ Research council files are ORP process artifacts. They record decomposition, provider lane outputs, and synthesis. Canonical evidence still belongs in source repositories, linked reports, cited URLs, datasets, papers, or other primary artifacts.
@@ -43,6 +43,7 @@ orp home
43
43
  orp agents root set /absolute/path/to/projects
44
44
  orp init
45
45
  orp agents audit
46
+ orp project show --json
46
47
  orp workspace create main-cody-1
47
48
  orp workspace tabs main
48
49
  orp agenda refresh --json
@@ -64,6 +65,7 @@ That gets you:
64
65
  - an optional umbrella projects root for parent/child agent guidance
65
66
  - repo governance initialized
66
67
  - repo-level AGENTS.md and CLAUDE.md scaffolded or refreshed
68
+ - `orp/project.json` created as the local project context lens
67
69
  - a local workspace ledger
68
70
  - the main recovery surface
69
71
  - a local operating agenda
@@ -73,6 +75,8 @@ That gets you:
73
75
  - a clean repo-governance read
74
76
  - a first intentional checkpoint
75
77
 
78
+ `orp/project.json` records the current directory's authority surfaces, directory signals, and default research call timing. It is refreshed by `orp init` and can evolve with the directory through `orp project refresh --json`, especially after adding or changing roadmap, spec, agent-guidance, docs, manifest, or command-surface files.
79
+
76
80
  ## Beginner Flow
77
81
 
78
82
  This is the zero-assumption path for a new user.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-research-protocol",
3
- "version": "0.4.24",
3
+ "version": "0.4.26",
4
4
  "description": "ORP CLI (Open Research Protocol): workspace ledgers, secrets, scheduling, governed execution, and agent-friendly research workflows.",
5
5
  "license": "MIT",
6
6
  "author": "Fractal Research Group <cody@frg.earth>",
@@ -28,6 +28,7 @@
28
28
  "spec/",
29
29
  "templates/",
30
30
  "AGENT_INTEGRATION.md",
31
+ "CHANGELOG.md",
31
32
  "INSTALL.md",
32
33
  "LICENSE",
33
34
  "PROTOCOL.md",
@@ -44,10 +44,13 @@ export { runWorkspaceSlot } from "./slot.js";
44
44
  export { buildWorkspaceTabsReport, parseWorkspaceTabsArgs, runWorkspaceTabs, summarizeWorkspaceTabs } from "./tabs.js";
45
45
  export {
46
46
  buildWorkspaceManifestFromHostedWorkspacePayload,
47
+ createHostedWorkspaceForIdea,
47
48
  fetchHostedWorkspacePayload,
48
49
  fetchIdeaPayload,
49
50
  fetchIdeasPayload,
50
51
  fetchHostedWorkspacesPayload,
52
+ findHostedWorkspaceByLinkedIdea,
53
+ findHostedWorkspaceLinkedToIdea,
51
54
  loadWorkspaceSource,
52
55
  pushHostedWorkspaceState,
53
56
  chooseImplicitMainCandidate,
@@ -14,12 +14,12 @@ import {
14
14
  import { buildHostedWorkspaceState } from "./hosted-state.js";
15
15
  import {
16
16
  buildWorkspaceManifestFromHostedWorkspacePayload,
17
- fetchIdeaPayload,
17
+ createHostedWorkspaceForIdea,
18
18
  fetchHostedWorkspacePayload,
19
+ findHostedWorkspaceByLinkedIdea,
19
20
  loadWorkspaceSource,
20
21
  pushHostedWorkspaceState,
21
22
  resolveWorkspaceWatchTargets,
22
- updateIdeaPayload,
23
23
  } from "./orp.js";
24
24
  import {
25
25
  cacheManagedWorkspaceManifest,
@@ -28,7 +28,7 @@ import {
28
28
  registerWorkspaceManifest,
29
29
  setWorkspaceSlot,
30
30
  } from "./registry.js";
31
- import { buildWorkspaceSyncPreview, resolveWorkspaceSyncTargetIdeaId, validateWorkspaceTitle } from "./sync.js";
31
+ import { validateWorkspaceTitle } from "./sync.js";
32
32
 
33
33
  function normalizeOptionalString(value) {
34
34
  if (value == null) {
@@ -125,6 +125,103 @@ function materializeWorkspaceManifest(manifest) {
125
125
  );
126
126
  }
127
127
 
128
+ function getObjectValue(record, ...keys) {
129
+ for (const key of keys) {
130
+ const value = record?.[key];
131
+ if (value && typeof value === "object" && !Array.isArray(value)) {
132
+ return value;
133
+ }
134
+ }
135
+ return null;
136
+ }
137
+
138
+ function getHostedWorkspaceId(workspace) {
139
+ return normalizeOptionalString(workspace?.workspace_id ?? workspace?.workspaceId ?? workspace?.id);
140
+ }
141
+
142
+ function getHostedWorkspaceTitle(workspace) {
143
+ return normalizeOptionalString(workspace?.title) || getHostedWorkspaceId(workspace);
144
+ }
145
+
146
+ function buildHostedWorkspaceSlotAssignment(workspace) {
147
+ const workspaceId = getHostedWorkspaceId(workspace);
148
+ if (!workspaceId) {
149
+ return null;
150
+ }
151
+ const title = getHostedWorkspaceTitle(workspace);
152
+ return {
153
+ kind: "hosted-workspace",
154
+ selector: title || workspaceId,
155
+ workspaceId,
156
+ title: title || undefined,
157
+ hostedWorkspaceId: workspaceId,
158
+ };
159
+ }
160
+
161
+ function buildWorkspaceFileSlotAssignment(manifest, manifestPath) {
162
+ const workspaceId = normalizeOptionalString(manifest?.workspaceId);
163
+ const title = normalizeOptionalString(manifest?.title) || workspaceId || undefined;
164
+ return {
165
+ kind: "workspace-file",
166
+ selector: title || workspaceId || manifestPath,
167
+ workspaceId: workspaceId || undefined,
168
+ title,
169
+ manifestPath,
170
+ };
171
+ }
172
+
173
+ async function assignMatchingWorkspaceSlots(source, manifest, assignment, options = {}) {
174
+ if (!assignment) {
175
+ return {};
176
+ }
177
+
178
+ const slotNames = new Set();
179
+ if (source.resolvedSlotName) {
180
+ slotNames.add(source.resolvedSlotName);
181
+ }
182
+
183
+ const sourceWorkspaceIds = new Set(
184
+ [
185
+ manifest?.workspaceId,
186
+ source.workspaceManifest?.workspaceId,
187
+ getHostedWorkspaceId(source.hostedWorkspace),
188
+ source.hostedWorkspaceId,
189
+ ]
190
+ .map((value) => normalizeOptionalString(value))
191
+ .filter(Boolean),
192
+ );
193
+ const sourcePaths = new Set(
194
+ [source.sourcePath, assignment.manifestPath]
195
+ .map((value) => normalizeOptionalString(value))
196
+ .filter(Boolean)
197
+ .map((value) => path.resolve(value)),
198
+ );
199
+
200
+ const slotsResult = await loadWorkspaceSlots(options).catch(() => ({ slots: {} }));
201
+ for (const [slotName, slot] of Object.entries(slotsResult.slots || {})) {
202
+ if (!slot || typeof slot !== "object" || Array.isArray(slot)) {
203
+ continue;
204
+ }
205
+ const slotIds = [
206
+ slot.workspaceId,
207
+ slot.hostedWorkspaceId,
208
+ slot.selector,
209
+ ]
210
+ .map((value) => normalizeOptionalString(value))
211
+ .filter(Boolean);
212
+ const slotPath = normalizeOptionalString(slot.manifestPath);
213
+ if (slotIds.some((value) => sourceWorkspaceIds.has(value)) || (slotPath && sourcePaths.has(path.resolve(slotPath)))) {
214
+ slotNames.add(slotName);
215
+ }
216
+ }
217
+
218
+ const assignedSlots = {};
219
+ for (const slotName of slotNames) {
220
+ assignedSlots[slotName] = (await setWorkspaceSlot(slotName, assignment, options)).slot;
221
+ }
222
+ return assignedSlots;
223
+ }
224
+
128
225
  function normalizeEditableManifest(source, parsed) {
129
226
  const baseManifest = parsed.manifest
130
227
  ? {
@@ -179,6 +276,51 @@ function normalizeEditableManifest(source, parsed) {
179
276
  return normalizeWorkspaceManifest(baseManifest);
180
277
  }
181
278
 
279
+ async function findOrCreateHostedWorkspaceForIdea(ideaId, source, manifest, options = {}) {
280
+ const existingWorkspace = await findHostedWorkspaceByLinkedIdea(ideaId, options);
281
+ if (existingWorkspace) {
282
+ return {
283
+ workspace: existingWorkspace,
284
+ created: false,
285
+ };
286
+ }
287
+
288
+ const linkedIdea =
289
+ source.sourceType === "hosted-idea"
290
+ ? source.idea
291
+ : getObjectValue(source.hostedWorkspace, "linked_idea", "linkedIdea");
292
+ const title =
293
+ manifest.title ||
294
+ manifest.workspaceId ||
295
+ source.title ||
296
+ normalizeOptionalString(linkedIdea?.idea_title ?? linkedIdea?.ideaTitle) ||
297
+ normalizeOptionalString(linkedIdea?.title) ||
298
+ ideaId;
299
+ const created = await createHostedWorkspaceForIdea({ title, ideaId }, options);
300
+ return {
301
+ workspace: created.workspace,
302
+ created: true,
303
+ createdPayload: created,
304
+ };
305
+ }
306
+
307
+ async function persistIdeaBackedWorkspaceToLocalCache(ideaId, source, manifest, reason, options = {}) {
308
+ const managedCache = await cacheManagedWorkspaceManifest(manifest, options);
309
+ const assignment = buildWorkspaceFileSlotAssignment(manifest, managedCache.manifestPath);
310
+ const assignedSlots = await assignMatchingWorkspaceSlots(source, manifest, assignment, options);
311
+ return {
312
+ persistedTo: "workspace-file",
313
+ ideaId,
314
+ promotedFromIdeaId: ideaId,
315
+ hostedMigrationSkippedReason: reason instanceof Error ? reason.message : String(reason || "Hosted workspace API unavailable."),
316
+ manifestPath: managedCache.manifestPath,
317
+ registryPath: managedCache.registryPath,
318
+ assignedSlots,
319
+ managedCache,
320
+ manifest,
321
+ };
322
+ }
323
+
182
324
  function parseLedgerSelectorArgs(
183
325
  argv = [],
184
326
  { commandName, requirePath = false, requireSelector = true, allowAppend = false, allowHere = false, allowCurrentCodex = false } = {},
@@ -593,40 +735,46 @@ async function persistWorkspaceManifest(source, manifest, options = {}) {
593
735
  }
594
736
 
595
737
  if (watchTargets.syncIdeaSelector) {
596
- const targetSource = await loadWorkspaceSource({
597
- ...options,
598
- ideaId: watchTargets.syncIdeaSelector,
599
- });
600
- const targetIdeaId = resolveWorkspaceSyncTargetIdeaId(targetSource);
601
- if (!targetIdeaId) {
602
- throw new Error(`Workspace source does not resolve to a syncable hosted idea: ${watchTargets.syncIdeaSelector}`);
738
+ const ideaId = watchTargets.syncIdeaSelector;
739
+ let promoted;
740
+ try {
741
+ promoted = await findOrCreateHostedWorkspaceForIdea(ideaId, source, manifest, options);
742
+ } catch (error) {
743
+ return persistIdeaBackedWorkspaceToLocalCache(ideaId, source, manifest, error, options);
603
744
  }
604
- const targetPayload =
605
- targetSource.sourceType === "hosted-idea" && targetSource.idea?.id === targetIdeaId
606
- ? targetSource.payload
607
- : await fetchIdeaPayload(targetIdeaId, options);
608
- const liveSource = {
609
- sourceType: "workspace-file",
610
- sourceLabel: `edited-workspace:${watchTargets.syncIdeaSelector}`,
611
- title: manifest.title || manifest.workspaceId || source.title || watchTargets.syncIdeaSelector,
612
- workspaceManifest: manifest,
613
- notes: "",
614
- };
615
- const parsed = parseWorkspaceSource(liveSource);
616
- const preview = buildWorkspaceSyncPreview({
617
- source: liveSource,
618
- parsed,
619
- targetIdea: targetPayload.idea,
620
- workspaceTitle: manifest.title || manifest.workspaceId || undefined,
745
+ const workspaceId = getHostedWorkspaceId(promoted.workspace);
746
+ if (!workspaceId) {
747
+ throw new Error(`Hosted workspace for idea ${ideaId} did not include a workspace id.`);
748
+ }
749
+
750
+ const previousWorkspace = promoted.created
751
+ ? promoted.workspace
752
+ : (await fetchHostedWorkspacePayload(workspaceId, options)).workspace;
753
+ const state = buildHostedWorkspaceState(manifest, {
754
+ previousWorkspace,
755
+ capturedAt: manifest.capture?.capturedAt,
756
+ updatedAt: new Date().toISOString(),
621
757
  });
622
- const updatedIdea = await updateIdeaPayload(targetIdeaId, { notes: preview.nextNotes }, options);
623
- const managedCache = await cacheManagedWorkspaceManifest(preview.manifest, options);
758
+ const pushResult = await pushHostedWorkspaceState(workspaceId, state, options);
759
+ const cachedManifest = buildWorkspaceManifestFromHostedWorkspacePayload(pushResult);
760
+ const managedCache = await cacheManagedWorkspaceManifest(cachedManifest, options);
761
+ const workspaceForSlot = pushResult.workspace || promoted.workspace;
762
+ const assignedSlots = await assignMatchingWorkspaceSlots(
763
+ source,
764
+ cachedManifest,
765
+ buildHostedWorkspaceSlotAssignment(workspaceForSlot),
766
+ options,
767
+ );
624
768
  return {
625
- persistedTo: "hosted-idea",
626
- ideaId: targetIdeaId,
627
- updatedIdea,
769
+ persistedTo: "hosted-workspace",
770
+ ideaId,
771
+ promotedFromIdeaId: ideaId,
772
+ createdHostedWorkspace: promoted.created,
773
+ workspaceId,
774
+ pushResult,
775
+ assignedSlots,
628
776
  managedCache,
629
- manifest: preview.manifest,
777
+ manifest: cachedManifest,
630
778
  };
631
779
  }
632
780
 
@@ -780,8 +928,14 @@ function summarizeWorkspaceLedgerMutation(result) {
780
928
  lines.push(`Canonical source: ORP idea ${result.ideaId}`);
781
929
  } else if (result.persistedTo === "hosted-workspace") {
782
930
  lines.push(`Canonical source: hosted workspace ${result.workspaceId}`);
931
+ if (result.promotedFromIdeaId) {
932
+ lines.push(`Linked idea: ${result.promotedFromIdeaId}`);
933
+ }
783
934
  } else if (result.persistedTo === "workspace-file") {
784
935
  lines.push(`Saved file: ${result.manifestPath}`);
936
+ if (result.hostedMigrationSkippedReason) {
937
+ lines.push(`Hosted migration skipped: ${result.hostedMigrationSkippedReason}`);
938
+ }
785
939
  }
786
940
 
787
941
  if (result.managedCachePath) {
@@ -809,9 +963,14 @@ async function runWorkspaceLedgerMutation(options, mutate, action) {
809
963
  removedTabs: (mutated.removedTabs || []).map((tab) => buildWorkspaceResultTab(tab)),
810
964
  persistedTo: persisted.persistedTo,
811
965
  ideaId: persisted.ideaId || null,
966
+ promotedFromIdeaId: persisted.promotedFromIdeaId || null,
967
+ createdHostedWorkspace: persisted.createdHostedWorkspace || false,
968
+ hostedMigrationSkippedReason: persisted.hostedMigrationSkippedReason || null,
812
969
  workspaceSourceId: persisted.workspaceId || null,
813
970
  manifestPath: persisted.manifestPath || null,
814
971
  managedCachePath: persisted.managedCache?.manifestPath || null,
972
+ assignedSlot: persisted.assignedSlot || Object.values(persisted.assignedSlots || {})[0] || null,
973
+ assignedSlots: persisted.assignedSlots || null,
815
974
  manifest: finalManifest,
816
975
  };
817
976
 
@@ -197,6 +197,59 @@ export async function fetchHostedWorkspacesPayload(options = {}) {
197
197
  };
198
198
  }
199
199
 
200
+ export function findHostedWorkspaceLinkedToIdea(workspaces = [], ideaId) {
201
+ const targetIdeaId = normalizeOptionalString(ideaId);
202
+ if (!targetIdeaId || !Array.isArray(workspaces)) {
203
+ return null;
204
+ }
205
+
206
+ return (
207
+ workspaces.find((workspace) => {
208
+ if (!workspace || typeof workspace !== "object" || Array.isArray(workspace)) {
209
+ return false;
210
+ }
211
+ const sourceKind = normalizeOptionalString(workspace.source_kind ?? workspace.sourceKind) || "hosted";
212
+ if (sourceKind === "idea_bridge") {
213
+ return false;
214
+ }
215
+ const linkedIdea = getObjectValue(workspace, "linked_idea", "linkedIdea");
216
+ return getTextValue(linkedIdea, "idea_id", "ideaId") === targetIdeaId;
217
+ }) || null
218
+ );
219
+ }
220
+
221
+ export async function findHostedWorkspaceByLinkedIdea(ideaId, options = {}) {
222
+ const payload = await fetchHostedWorkspacesPayload(options);
223
+ return findHostedWorkspaceLinkedToIdea(payload.workspaces, ideaId);
224
+ }
225
+
226
+ export async function createHostedWorkspaceForIdea({ title, ideaId, description = null, visibility = null } = {}, options = {}) {
227
+ const workspaceTitle = slugify(title) || (ideaId ? `workspace-${String(ideaId).slice(0, 8).toLowerCase()}` : "workspace");
228
+ const invocation = resolveOrpInvocation(options);
229
+ const args = [...invocation.prefixArgs, "workspaces", "add", "--title", workspaceTitle];
230
+ if (description != null) {
231
+ args.push("--description", String(description));
232
+ }
233
+ if (visibility != null) {
234
+ args.push("--visibility", String(visibility));
235
+ }
236
+ const linkedIdeaId = normalizeOptionalString(ideaId);
237
+ if (linkedIdeaId) {
238
+ args.push("--idea-id", linkedIdeaId);
239
+ }
240
+ if (options.baseUrl) {
241
+ args.push("--base-url", options.baseUrl);
242
+ }
243
+ args.push("--json");
244
+
245
+ const result = await runCommand(invocation.command, args, options);
246
+ const payload = parseOrpJsonResult(result, "Failed to create ORP hosted workspace.");
247
+ if (!payload || payload.ok !== true || !payload.workspace) {
248
+ throw new Error("ORP returned an unexpected hosted workspace creation payload.");
249
+ }
250
+ return payload;
251
+ }
252
+
200
253
  function buildWorkspaceTitleFromIdea(idea, manifest) {
201
254
  return normalizeOptionalString(manifest?.title) || normalizeOptionalString(idea?.title) || null;
202
255
  }
@@ -857,12 +910,19 @@ export async function loadWorkspaceSource(options = {}) {
857
910
  const selector = options.ideaId;
858
911
  const slotTarget = await resolveWorkspaceSlotTarget(selector, options);
859
912
  if (slotTarget?.target) {
860
- return loadWorkspaceSource({
913
+ const resolvedSource = await loadWorkspaceSource({
861
914
  ...options,
862
915
  ideaId: slotTarget.target.ideaId,
863
916
  workspaceFile: slotTarget.target.workspaceFile,
864
917
  hostedWorkspaceId: slotTarget.target.hostedWorkspaceId,
865
918
  });
919
+ return {
920
+ ...resolvedSource,
921
+ resolvedSlotName: slotTarget.slotName,
922
+ resolvedSlotMode: slotTarget.mode,
923
+ resolvedSlot: slotTarget.slot || null,
924
+ resolvedSlotCandidate: slotTarget.candidate || null,
925
+ };
866
926
  }
867
927
  if (slotTarget?.slotName && slotTarget.mode === "unset") {
868
928
  throw new Error(