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.
- package/CHANGELOG.md +456 -0
- package/README.md +47 -13
- package/cli/orp.py +2998 -70
- package/docs/AGENT_RUNTIME_BORROWING_NOTES.md +68 -0
- package/docs/RESEARCH_COUNCIL.md +123 -0
- package/docs/START_HERE.md +4 -0
- package/package.json +2 -1
- package/packages/orp-workspace-launcher/src/index.js +3 -0
- package/packages/orp-workspace-launcher/src/ledger.js +192 -33
- package/packages/orp-workspace-launcher/src/orp.js +61 -1
- package/packages/orp-workspace-launcher/src/tabs.js +147 -4
- package/packages/orp-workspace-launcher/test/ledger.test.js +226 -0
- package/packages/orp-workspace-launcher/test/tabs.test.js +60 -0
- package/scripts/orp-mcp +205 -0
- package/spec/v1/project-context.schema.json +223 -0
- package/spec/v1/research-run.schema.json +245 -0
- package/cli/__pycache__/orp.cpython-311.pyc +0 -0
- package/scripts/__pycache__/orp-kernel-agent-pilot.cpython-311.pyc +0 -0
- package/scripts/__pycache__/orp-kernel-agent-replication.cpython-311.pyc +0 -0
- package/scripts/__pycache__/orp-kernel-benchmark.cpython-311.pyc +0 -0
- package/scripts/__pycache__/orp-kernel-canonical-continuation.cpython-311.pyc +0 -0
- package/scripts/__pycache__/orp-kernel-continuation-pilot.cpython-311.pyc +0 -0
|
@@ -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.
|
package/docs/START_HERE.md
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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 {
|
|
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
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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
|
|
623
|
-
const
|
|
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-
|
|
626
|
-
ideaId
|
|
627
|
-
|
|
769
|
+
persistedTo: "hosted-workspace",
|
|
770
|
+
ideaId,
|
|
771
|
+
promotedFromIdeaId: ideaId,
|
|
772
|
+
createdHostedWorkspace: promoted.created,
|
|
773
|
+
workspaceId,
|
|
774
|
+
pushResult,
|
|
775
|
+
assignedSlots,
|
|
628
776
|
managedCache,
|
|
629
|
-
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
|
-
|
|
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(
|