open-research-protocol 0.4.14 → 0.4.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_INTEGRATION.md +50 -0
- package/README.md +273 -144
- package/bin/orp.js +14 -1
- package/cli/orp.py +14846 -9925
- package/docs/AGENT_LOOP.md +13 -0
- package/docs/AGENT_MODES.md +79 -0
- package/docs/CANONICAL_CLI_BOUNDARY.md +15 -0
- package/docs/EXCHANGE.md +94 -0
- package/docs/LAUNCH_KIT.md +107 -0
- package/docs/ORP_HOSTED_WORKSPACE_CONTRACT.md +295 -0
- package/docs/ORP_PUBLIC_LAUNCH_CHECKLIST.md +5 -0
- package/docs/START_HERE.md +567 -0
- package/package.json +4 -2
- package/packages/lifeops-orp/README.md +67 -0
- package/packages/lifeops-orp/package.json +48 -0
- package/packages/lifeops-orp/src/index.d.ts +106 -0
- package/packages/lifeops-orp/src/index.js +7 -0
- package/packages/lifeops-orp/src/mapping.js +309 -0
- package/packages/lifeops-orp/src/workspace.js +108 -0
- package/packages/lifeops-orp/test/orp.test.js +187 -0
- package/packages/orp-workspace-launcher/README.md +82 -0
- package/packages/orp-workspace-launcher/package.json +39 -0
- package/packages/orp-workspace-launcher/src/commands.js +77 -0
- package/packages/orp-workspace-launcher/src/core-plan.js +506 -0
- package/packages/orp-workspace-launcher/src/hosted-state.js +208 -0
- package/packages/orp-workspace-launcher/src/index.js +82 -0
- package/packages/orp-workspace-launcher/src/ledger.js +745 -0
- package/packages/orp-workspace-launcher/src/list.js +488 -0
- package/packages/orp-workspace-launcher/src/orp-command.js +126 -0
- package/packages/orp-workspace-launcher/src/orp.js +912 -0
- package/packages/orp-workspace-launcher/src/registry.js +558 -0
- package/packages/orp-workspace-launcher/src/slot.js +188 -0
- package/packages/orp-workspace-launcher/src/sync.js +363 -0
- package/packages/orp-workspace-launcher/src/tabs.js +166 -0
- package/packages/orp-workspace-launcher/test/commands.test.js +164 -0
- package/packages/orp-workspace-launcher/test/core-plan.test.js +253 -0
- package/packages/orp-workspace-launcher/test/fixtures/smoke-notes.txt +2 -0
- package/packages/orp-workspace-launcher/test/fixtures/workspace-manifest.json +17 -0
- package/packages/orp-workspace-launcher/test/ledger.test.js +244 -0
- package/packages/orp-workspace-launcher/test/list.test.js +299 -0
- package/packages/orp-workspace-launcher/test/orp-command.test.js +44 -0
- package/packages/orp-workspace-launcher/test/orp.test.js +224 -0
- package/packages/orp-workspace-launcher/test/tabs.test.js +168 -0
- package/scripts/orp-kernel-agent-pilot.py +10 -1
- package/scripts/orp-kernel-agent-replication.py +10 -1
- package/scripts/orp-kernel-canonical-continuation.py +10 -1
- package/scripts/orp-kernel-continuation-pilot.py +10 -1
- package/scripts/render-terminal-demo.py +416 -0
- package/spec/v1/exchange-report.schema.json +105 -0
- package/spec/v1/hosted-workspace-event.schema.json +102 -0
- package/spec/v1/hosted-workspace.schema.json +332 -0
- package/spec/v1/workspace.schema.json +108 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import math
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
12
|
+
ASSETS = ROOT / "assets"
|
|
13
|
+
GIF_PATH = ASSETS / "terminal-demo.gif"
|
|
14
|
+
POSTER_PATH = ASSETS / "terminal-demo-poster.png"
|
|
15
|
+
STORYBOARD_PATH = ASSETS / "terminal-demo-storyboard.png"
|
|
16
|
+
|
|
17
|
+
WIDTH = 1360
|
|
18
|
+
HEIGHT = 840
|
|
19
|
+
SIDE_MARGIN = 72
|
|
20
|
+
SHELL_TOP = 132
|
|
21
|
+
BOTTOM_MARGIN = 72
|
|
22
|
+
WINDOW_PADDING = 28
|
|
23
|
+
|
|
24
|
+
OUTER_BG = "#07131F"
|
|
25
|
+
WINDOW_BG = "#0A1728"
|
|
26
|
+
WINDOW_EDGE = "#16314D"
|
|
27
|
+
TITLEBAR_BG = "#10243A"
|
|
28
|
+
TERMINAL_BG = "#0C1B2E"
|
|
29
|
+
INK = "#E7F4F1"
|
|
30
|
+
MUTED = "#8FA7BF"
|
|
31
|
+
DIM = "#6C819A"
|
|
32
|
+
ACCENT = "#6EE7D8"
|
|
33
|
+
SKY = "#8DBEFF"
|
|
34
|
+
SOFT = "#F1D6B8"
|
|
35
|
+
CORAL = "#FF9A86"
|
|
36
|
+
LIME = "#BDEB9B"
|
|
37
|
+
GOLD = "#F6D98D"
|
|
38
|
+
|
|
39
|
+
FONT_MONO = "/System/Library/Fonts/Menlo.ttc"
|
|
40
|
+
FONT_SANS_BOLD = "/System/Library/Fonts/Supplemental/Arial Bold.ttf"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def load_font(path: str, size: int) -> ImageFont.ImageFont:
|
|
44
|
+
try:
|
|
45
|
+
return ImageFont.truetype(path, size)
|
|
46
|
+
except OSError:
|
|
47
|
+
return ImageFont.load_default()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
MONO_18 = load_font(FONT_MONO, 18)
|
|
51
|
+
MONO_20 = load_font(FONT_MONO, 20)
|
|
52
|
+
MONO_24 = load_font(FONT_MONO, 24)
|
|
53
|
+
MONO_28 = load_font(FONT_MONO, 28)
|
|
54
|
+
MONO_32 = load_font(FONT_MONO, 32)
|
|
55
|
+
SANS_22 = load_font(FONT_SANS_BOLD, 22)
|
|
56
|
+
SANS_26 = load_font(FONT_SANS_BOLD, 26)
|
|
57
|
+
SANS_34 = load_font(FONT_SANS_BOLD, 34)
|
|
58
|
+
SANS_52 = load_font(FONT_SANS_BOLD, 52)
|
|
59
|
+
|
|
60
|
+
CONTENT_X = SIDE_MARGIN + WINDOW_PADDING + 46
|
|
61
|
+
CONTENT_Y = SHELL_TOP + 138
|
|
62
|
+
CONTENT_WIDTH = (WIDTH - SIDE_MARGIN - WINDOW_PADDING) - CONTENT_X - 20
|
|
63
|
+
LINE_HEIGHT = 34
|
|
64
|
+
TYPING_MS = 75
|
|
65
|
+
COMMAND_SETTLE_MS = 320
|
|
66
|
+
OUTPUT_STEP_MS = 240
|
|
67
|
+
SCENE_HOLD_MS = 33000
|
|
68
|
+
|
|
69
|
+
SCENES = [
|
|
70
|
+
{
|
|
71
|
+
"id": "home",
|
|
72
|
+
"label": "home",
|
|
73
|
+
"headline": "discover the surface",
|
|
74
|
+
"command": "orp home",
|
|
75
|
+
"output_font": "small",
|
|
76
|
+
"line_height": 30,
|
|
77
|
+
"output": [
|
|
78
|
+
("ORP 0.4.13", ACCENT),
|
|
79
|
+
("Agent-first CLI for workspace ledgers, secrets, scheduling, and research workflows.", INK),
|
|
80
|
+
("Repo", SKY),
|
|
81
|
+
(" root: /Volumes/Code_2TB/code/orp", INK),
|
|
82
|
+
(" config: orp.yml (missing)", ACCENT),
|
|
83
|
+
(" git: yes, branch=main, commit=4cde66c", SOFT),
|
|
84
|
+
("Daily Loop", SKY),
|
|
85
|
+
(" orp workspace tabs main", ACCENT),
|
|
86
|
+
(' orp secrets add --alias <alias> --label "<label>" --provider <provider>', INK),
|
|
87
|
+
(' orp checkpoint create -m "capture loop state"', SOFT),
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"id": "hosted",
|
|
92
|
+
"label": "hosted",
|
|
93
|
+
"headline": "see the control plane",
|
|
94
|
+
"command": "orp workspaces list",
|
|
95
|
+
"output": [
|
|
96
|
+
("workspaces.count=2", ACCENT),
|
|
97
|
+
("cursor=", INK),
|
|
98
|
+
("has_more=false", SKY),
|
|
99
|
+
("source=idea_bridge", ACCENT),
|
|
100
|
+
("---", DIM),
|
|
101
|
+
("workspace.id=main-cody-1", INK),
|
|
102
|
+
("workspace.title=main-cody-1", SKY),
|
|
103
|
+
("workspace.tab_count=18", SOFT),
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"id": "secrets",
|
|
108
|
+
"label": "secrets",
|
|
109
|
+
"headline": "save the key once, reuse it later",
|
|
110
|
+
"command": 'orp secrets add --alias openai-primary --label "OpenAI Primary" --provider openai',
|
|
111
|
+
"output": [
|
|
112
|
+
("Secret value:", ACCENT),
|
|
113
|
+
(" sk-...", INK),
|
|
114
|
+
("secret.alias=openai-primary", SKY),
|
|
115
|
+
("secret.provider=openai", ACCENT),
|
|
116
|
+
("secret.kind=api_key", INK),
|
|
117
|
+
("secret.status=active", SKY),
|
|
118
|
+
("next: orp secrets resolve openai-primary --reveal", SOFT),
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"id": "workspace",
|
|
123
|
+
"label": "workspace",
|
|
124
|
+
"headline": "keep the workspace ledger",
|
|
125
|
+
"command": "orp workspace tabs main",
|
|
126
|
+
"output": [
|
|
127
|
+
("saved tabs: 11", ACCENT),
|
|
128
|
+
("ledger: hosted canonical + local cache", INK),
|
|
129
|
+
("recovery: copy the exact cd && resume command you need", SKY),
|
|
130
|
+
("tools: codex resume and claude --resume are both tracked", ACCENT),
|
|
131
|
+
("tail tabs: orp · orp-web-app · RigidityCore · frg-site", SOFT),
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"id": "schedule",
|
|
136
|
+
"label": "schedule",
|
|
137
|
+
"headline": "automate the next loop",
|
|
138
|
+
"command": 'orp schedule add codex --name morning-summary --prompt "Summarize this repo"',
|
|
139
|
+
"output": [
|
|
140
|
+
("ORP Scheduled Job Created", ACCENT),
|
|
141
|
+
("Name: morning-summary", INK),
|
|
142
|
+
("Kind: codex", SKY),
|
|
143
|
+
("Schedule: daily at 09:00", ACCENT),
|
|
144
|
+
("Prompt source: inline", INK),
|
|
145
|
+
("Codex session id: none", SKY),
|
|
146
|
+
("Next steps:", ACCENT),
|
|
147
|
+
(" orp schedule run morning-summary", SOFT),
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"id": "governance",
|
|
152
|
+
"label": "governance",
|
|
153
|
+
"headline": "checkpoint the repo safely",
|
|
154
|
+
"command": 'orp checkpoint create -m "capture loop state"',
|
|
155
|
+
"output": [
|
|
156
|
+
("commit=7f3c2a1", ACCENT),
|
|
157
|
+
("branch=work/release-hardening", INK),
|
|
158
|
+
("message=checkpoint: capture loop state", SKY),
|
|
159
|
+
("checkpoint_log=orp/checkpoints/CHECKPOINT_LOG.md", ACCENT),
|
|
160
|
+
("git_runtime=orp/git/runtime.json", SOFT),
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"id": "planning",
|
|
165
|
+
"label": "planning",
|
|
166
|
+
"headline": "track the live point",
|
|
167
|
+
"command": "orp frontier state",
|
|
168
|
+
"output": [
|
|
169
|
+
("program_id=sunflower-coda", ACCENT),
|
|
170
|
+
("active_version=v10", INK),
|
|
171
|
+
("active_milestone=v10.3", SKY),
|
|
172
|
+
("active_phase=395", ACCENT),
|
|
173
|
+
("band=verification", INK),
|
|
174
|
+
("next_action=Execute Phase 395", SKY),
|
|
175
|
+
("blocked_by=(none)", SOFT),
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"id": "synthesis",
|
|
180
|
+
"label": "synthesis",
|
|
181
|
+
"headline": "scan, synthesize, collaborate",
|
|
182
|
+
"command": "orp exchange repo synthesize /path/to/source",
|
|
183
|
+
"output": [
|
|
184
|
+
("exchange_id=exc_20260331_001", ACCENT),
|
|
185
|
+
("source.mode=local_path", INK),
|
|
186
|
+
("source.local_path=/path/to/source", SKY),
|
|
187
|
+
("source.git_present=true", ACCENT),
|
|
188
|
+
("artifacts.exchange_json=orp/exchange/exc_20260331_001/EXCHANGE.json", INK),
|
|
189
|
+
("artifacts.summary_md=orp/exchange/exc_20260331_001/SUMMARY.md", SKY),
|
|
190
|
+
("artifacts.transfer_map_md=orp/exchange/exc_20260331_001/TRANSFER_MAP.md", SOFT),
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"id": "mode",
|
|
195
|
+
"label": "mode",
|
|
196
|
+
"headline": "change the lens",
|
|
197
|
+
"command": "orp mode nudge sleek-minimal-progressive",
|
|
198
|
+
"output": [
|
|
199
|
+
("mode.id=sleek-minimal-progressive", ACCENT),
|
|
200
|
+
("mode.label=Sleek Minimal Progressive", INK),
|
|
201
|
+
("nudge.title=Subtractive Spark", SKY),
|
|
202
|
+
("nudge.prompt=remove one thing before adding one surprising move", ACCENT),
|
|
203
|
+
("nudge.twist=keep the architecture cleaner than the idea feels", INK),
|
|
204
|
+
("nudge.release=drop the first obvious framing", SOFT),
|
|
205
|
+
("nudge.micro_loop:", ACCENT),
|
|
206
|
+
("- zoom out, rotate, re-enter deliberately", SKY),
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def wrap_line(text: str, font: ImageFont.ImageFont, max_width: int) -> list[str]:
|
|
213
|
+
words = text.split(" ")
|
|
214
|
+
lines: list[str] = []
|
|
215
|
+
current = ""
|
|
216
|
+
for word in words:
|
|
217
|
+
trial = word if not current else f"{current} {word}"
|
|
218
|
+
trial_box = font.getbbox(trial)
|
|
219
|
+
trial_width = trial_box[2] - trial_box[0]
|
|
220
|
+
if trial_width <= max_width or not current:
|
|
221
|
+
current = trial
|
|
222
|
+
else:
|
|
223
|
+
lines.append(current)
|
|
224
|
+
current = word
|
|
225
|
+
if current:
|
|
226
|
+
lines.append(current)
|
|
227
|
+
return lines
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def wrap_command(command: str, font: ImageFont.ImageFont, max_width: int) -> list[str]:
|
|
231
|
+
words = command.split(" ")
|
|
232
|
+
lines: list[str] = []
|
|
233
|
+
current = "$"
|
|
234
|
+
for word in words:
|
|
235
|
+
trial = f"{current} {word}".strip()
|
|
236
|
+
trial_box = font.getbbox(trial)
|
|
237
|
+
trial_width = trial_box[2] - trial_box[0]
|
|
238
|
+
if trial_width <= max_width or current == "$":
|
|
239
|
+
current = trial
|
|
240
|
+
else:
|
|
241
|
+
lines.append(current)
|
|
242
|
+
current = f" {word}"
|
|
243
|
+
if current:
|
|
244
|
+
lines.append(current)
|
|
245
|
+
return lines
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def draw_background(draw: ImageDraw.ImageDraw) -> None:
|
|
249
|
+
draw.rectangle((0, 0, WIDTH, HEIGHT), fill=OUTER_BG)
|
|
250
|
+
orbit_colors = [ACCENT, SKY, SOFT, ACCENT, SKY]
|
|
251
|
+
orbit_points = [
|
|
252
|
+
(84, 70),
|
|
253
|
+
(WIDTH - 88, 66),
|
|
254
|
+
(106, HEIGHT - 118),
|
|
255
|
+
(232, HEIGHT - 176),
|
|
256
|
+
(WIDTH - 210, HEIGHT - 162),
|
|
257
|
+
(WIDTH - 116, HEIGHT - 110),
|
|
258
|
+
]
|
|
259
|
+
for idx, (x, y) in enumerate(orbit_points):
|
|
260
|
+
r = 8 if idx % 2 == 0 else 6
|
|
261
|
+
color = orbit_colors[idx % len(orbit_colors)]
|
|
262
|
+
draw.ellipse((x - r, y - r, x + r, y + r), fill=color)
|
|
263
|
+
draw.line((106, HEIGHT - 118, 232, HEIGHT - 176), fill="#17324D", width=2)
|
|
264
|
+
draw.line((WIDTH - 210, HEIGHT - 162, WIDTH - 116, HEIGHT - 110), fill="#17324D", width=2)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def draw_terminal_shell(draw: ImageDraw.ImageDraw) -> tuple[int, int, int, int]:
|
|
268
|
+
left = SIDE_MARGIN
|
|
269
|
+
top = SHELL_TOP
|
|
270
|
+
right = WIDTH - SIDE_MARGIN
|
|
271
|
+
bottom = HEIGHT - BOTTOM_MARGIN
|
|
272
|
+
draw.rounded_rectangle((left, top, right, bottom), radius=36, fill=WINDOW_BG, outline=WINDOW_EDGE, width=2)
|
|
273
|
+
draw.rounded_rectangle(
|
|
274
|
+
(left + WINDOW_PADDING, top + WINDOW_PADDING, right - WINDOW_PADDING, bottom - WINDOW_PADDING),
|
|
275
|
+
radius=24,
|
|
276
|
+
fill=TERMINAL_BG,
|
|
277
|
+
outline="#1A3A5A",
|
|
278
|
+
width=2,
|
|
279
|
+
)
|
|
280
|
+
title_bottom = top + WINDOW_PADDING + 62
|
|
281
|
+
draw.rounded_rectangle(
|
|
282
|
+
(left + WINDOW_PADDING, top + WINDOW_PADDING, right - WINDOW_PADDING, title_bottom),
|
|
283
|
+
radius=24,
|
|
284
|
+
fill=TITLEBAR_BG,
|
|
285
|
+
)
|
|
286
|
+
draw.rectangle((left + WINDOW_PADDING, title_bottom - 24, right - WINDOW_PADDING, title_bottom), fill=TITLEBAR_BG)
|
|
287
|
+
for idx, color in enumerate((CORAL, GOLD, LIME)):
|
|
288
|
+
x = left + WINDOW_PADDING + 22 + (idx * 28)
|
|
289
|
+
y = top + WINDOW_PADDING + 20
|
|
290
|
+
draw.ellipse((x, y, x + 14, y + 14), fill=color)
|
|
291
|
+
draw.text((left + WINDOW_PADDING + 112, top + WINDOW_PADDING + 12), "open-research-protocol demo", font=SANS_22, fill=INK)
|
|
292
|
+
right_label = "agent-first research loop"
|
|
293
|
+
right_box = draw.textbbox((0, 0), right_label, font=MONO_18)
|
|
294
|
+
right_x = right - WINDOW_PADDING - 22 - (right_box[2] - right_box[0])
|
|
295
|
+
draw.text((right_x, top + WINDOW_PADDING + 18), right_label, font=MONO_18, fill=DIM)
|
|
296
|
+
return (left, top, right, bottom)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def draw_scene_header(draw: ImageDraw.ImageDraw, scene: dict) -> None:
|
|
300
|
+
label = scene["label"].upper()
|
|
301
|
+
label_box = draw.textbbox((0, 0), label, font=MONO_20)
|
|
302
|
+
label_x = (WIDTH - (label_box[2] - label_box[0])) // 2
|
|
303
|
+
draw.text((label_x, 18), label, font=MONO_20, fill=DIM)
|
|
304
|
+
headline = scene["headline"]
|
|
305
|
+
headline_box = draw.textbbox((0, 0), headline, font=SANS_52)
|
|
306
|
+
headline_x = (WIDTH - (headline_box[2] - headline_box[0])) // 2
|
|
307
|
+
draw.text((headline_x, 42), headline, font=SANS_52, fill=ACCENT)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def render_scene(scene: dict, typed_chars: Optional[int] = None, shown_lines: int = 0, cursor: bool = False) -> Image.Image:
|
|
311
|
+
image = Image.new("RGBA", (WIDTH, HEIGHT), OUTER_BG)
|
|
312
|
+
draw = ImageDraw.Draw(image)
|
|
313
|
+
draw_background(draw)
|
|
314
|
+
draw_scene_header(draw, scene)
|
|
315
|
+
draw_terminal_shell(draw)
|
|
316
|
+
|
|
317
|
+
command = scene["command"] if typed_chars is None else scene["command"][:typed_chars]
|
|
318
|
+
command_lines = wrap_command(command, MONO_32, CONTENT_WIDTH)
|
|
319
|
+
if cursor and command_lines:
|
|
320
|
+
command_lines[-1] = f"{command_lines[-1]}_"
|
|
321
|
+
command_y = CONTENT_Y + 14
|
|
322
|
+
for line in command_lines:
|
|
323
|
+
draw.text((CONTENT_X, command_y), line, font=MONO_32, fill=ACCENT)
|
|
324
|
+
command_y += 40
|
|
325
|
+
|
|
326
|
+
output_font = MONO_24 if scene.get("output_font") != "small" else MONO_20
|
|
327
|
+
line_height = int(scene.get("line_height", LINE_HEIGHT))
|
|
328
|
+
content_max_y = HEIGHT - BOTTOM_MARGIN - WINDOW_PADDING - 20
|
|
329
|
+
y = command_y + 38
|
|
330
|
+
for text, color in scene["output"][:shown_lines]:
|
|
331
|
+
if y > content_max_y:
|
|
332
|
+
break
|
|
333
|
+
for line in wrap_line(text, output_font, CONTENT_WIDTH):
|
|
334
|
+
if y > content_max_y:
|
|
335
|
+
break
|
|
336
|
+
draw.text((CONTENT_X, y), line, font=output_font, fill=color)
|
|
337
|
+
y += line_height
|
|
338
|
+
|
|
339
|
+
footer = "npm install -g open-research-protocol"
|
|
340
|
+
footer_box = draw.textbbox((0, 0), footer, font=MONO_24)
|
|
341
|
+
footer_width = (footer_box[2] - footer_box[0]) + 46
|
|
342
|
+
footer_x = (WIDTH - footer_width) // 2
|
|
343
|
+
footer_y = HEIGHT - 118
|
|
344
|
+
draw.rounded_rectangle((footer_x, footer_y, footer_x + footer_width, footer_y + 54), radius=15, fill=ACCENT)
|
|
345
|
+
draw.text((footer_x + 23, footer_y + 14), footer, font=MONO_24, fill=WINDOW_BG)
|
|
346
|
+
return image
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def build_storyboard(final_frames: list[Image.Image]) -> Image.Image:
|
|
350
|
+
thumb_width = 560
|
|
351
|
+
thumb_height = int(thumb_width * HEIGHT / WIDTH)
|
|
352
|
+
cols = 3 if len(final_frames) > 6 else 2
|
|
353
|
+
rows = max(1, math.ceil(len(final_frames) / cols))
|
|
354
|
+
gutter = 24
|
|
355
|
+
board_width = thumb_width * cols + gutter * (cols + 1)
|
|
356
|
+
board_height = thumb_height * rows + gutter * (rows + 1) + 72
|
|
357
|
+
storyboard = Image.new("RGBA", (board_width, board_height), OUTER_BG)
|
|
358
|
+
draw = ImageDraw.Draw(storyboard)
|
|
359
|
+
draw.text((gutter, 20), "ORP terminal walkthrough storyboard", font=SANS_34, fill=INK)
|
|
360
|
+
for idx, frame in enumerate(final_frames):
|
|
361
|
+
thumb = frame.copy()
|
|
362
|
+
thumb.thumbnail((thumb_width, thumb_height))
|
|
363
|
+
row = idx // cols
|
|
364
|
+
col = idx % cols
|
|
365
|
+
x = gutter + col * (thumb_width + gutter)
|
|
366
|
+
y = 72 + gutter + row * (thumb_height + gutter)
|
|
367
|
+
storyboard.alpha_composite(thumb, (x, y))
|
|
368
|
+
return storyboard
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def main() -> None:
|
|
372
|
+
ASSETS.mkdir(parents=True, exist_ok=True)
|
|
373
|
+
frames: list[Image.Image] = []
|
|
374
|
+
durations: list[int] = []
|
|
375
|
+
final_scene_frames: list[Image.Image] = []
|
|
376
|
+
|
|
377
|
+
for index, scene in enumerate(SCENES, start=1):
|
|
378
|
+
command = scene["command"]
|
|
379
|
+
step = 2 if len(command) < 28 else 4
|
|
380
|
+
for typed in range(1, len(command) + 1, step):
|
|
381
|
+
frames.append(render_scene(scene, typed_chars=typed, shown_lines=0, cursor=True))
|
|
382
|
+
durations.append(TYPING_MS)
|
|
383
|
+
frames.append(render_scene(scene, typed_chars=len(command), shown_lines=0, cursor=False))
|
|
384
|
+
durations.append(COMMAND_SETTLE_MS)
|
|
385
|
+
for shown_lines in range(1, len(scene["output"]) + 1):
|
|
386
|
+
frame = render_scene(scene, typed_chars=len(command), shown_lines=shown_lines, cursor=False)
|
|
387
|
+
frames.append(frame)
|
|
388
|
+
durations.append(OUTPUT_STEP_MS if shown_lines < len(scene["output"]) else SCENE_HOLD_MS)
|
|
389
|
+
if shown_lines == len(scene["output"]):
|
|
390
|
+
final_scene_frames.append(frame.copy())
|
|
391
|
+
scene_path = ASSETS / f"terminal-scene-{index:02d}-{scene['id']}.png"
|
|
392
|
+
frame.save(scene_path)
|
|
393
|
+
|
|
394
|
+
poster = final_scene_frames[2] if len(final_scene_frames) >= 3 else final_scene_frames[0]
|
|
395
|
+
poster.save(POSTER_PATH)
|
|
396
|
+
storyboard = build_storyboard(final_scene_frames)
|
|
397
|
+
storyboard.save(STORYBOARD_PATH)
|
|
398
|
+
frames[0].save(
|
|
399
|
+
GIF_PATH,
|
|
400
|
+
save_all=True,
|
|
401
|
+
append_images=frames[1:],
|
|
402
|
+
duration=durations,
|
|
403
|
+
loop=0,
|
|
404
|
+
optimize=True,
|
|
405
|
+
disposal=2,
|
|
406
|
+
)
|
|
407
|
+
print(f"WROTE {GIF_PATH}")
|
|
408
|
+
print(f"WROTE {POSTER_PATH}")
|
|
409
|
+
print(f"WROTE {STORYBOARD_PATH}")
|
|
410
|
+
for index, scene in enumerate(SCENES, start=1):
|
|
411
|
+
scene_name = f"terminal-scene-{index:02d}-{scene['id']}.png"
|
|
412
|
+
print(f"WROTE {ASSETS / scene_name}")
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
if __name__ == "__main__":
|
|
416
|
+
main()
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://openresearchprotocol.com/spec/v1/exchange-report.schema.json",
|
|
4
|
+
"title": "ORP Exchange Report",
|
|
5
|
+
"description": "Machine-readable ORP knowledge exchange artifact for structured synthesis of another repository or project directory.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": true,
|
|
8
|
+
"required": [
|
|
9
|
+
"schema_version",
|
|
10
|
+
"kind",
|
|
11
|
+
"exchange_id",
|
|
12
|
+
"generated_at_utc",
|
|
13
|
+
"current_project_root",
|
|
14
|
+
"source",
|
|
15
|
+
"inventory",
|
|
16
|
+
"relation",
|
|
17
|
+
"suggested_focus",
|
|
18
|
+
"artifacts",
|
|
19
|
+
"notes"
|
|
20
|
+
],
|
|
21
|
+
"properties": {
|
|
22
|
+
"schema_version": {
|
|
23
|
+
"const": "1.0.0"
|
|
24
|
+
},
|
|
25
|
+
"kind": {
|
|
26
|
+
"const": "exchange_report"
|
|
27
|
+
},
|
|
28
|
+
"exchange_id": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"minLength": 1
|
|
31
|
+
},
|
|
32
|
+
"generated_at_utc": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"minLength": 1
|
|
35
|
+
},
|
|
36
|
+
"current_project_root": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"minLength": 1
|
|
39
|
+
},
|
|
40
|
+
"source": {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"required": [
|
|
43
|
+
"requested",
|
|
44
|
+
"mode",
|
|
45
|
+
"label",
|
|
46
|
+
"local_path",
|
|
47
|
+
"remote_url",
|
|
48
|
+
"git_present",
|
|
49
|
+
"git_initialized_by_orp",
|
|
50
|
+
"cloned_by_orp",
|
|
51
|
+
"git"
|
|
52
|
+
],
|
|
53
|
+
"properties": {
|
|
54
|
+
"requested": { "type": "string" },
|
|
55
|
+
"mode": { "type": "string" },
|
|
56
|
+
"label": { "type": "string" },
|
|
57
|
+
"local_path": { "type": "string" },
|
|
58
|
+
"remote_url": { "type": "string" },
|
|
59
|
+
"git_present": { "type": "boolean" },
|
|
60
|
+
"git_initialized_by_orp": { "type": "boolean" },
|
|
61
|
+
"cloned_by_orp": { "type": "boolean" },
|
|
62
|
+
"git": { "type": "object" }
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"inventory": {
|
|
66
|
+
"type": "object",
|
|
67
|
+
"required": [
|
|
68
|
+
"top_level_entries",
|
|
69
|
+
"manifest_files",
|
|
70
|
+
"docs_paths",
|
|
71
|
+
"test_paths",
|
|
72
|
+
"languages",
|
|
73
|
+
"files_scanned",
|
|
74
|
+
"dirs_scanned"
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
"relation": {
|
|
78
|
+
"type": "object",
|
|
79
|
+
"required": [
|
|
80
|
+
"host_repo_name",
|
|
81
|
+
"source_repo_name",
|
|
82
|
+
"same_root",
|
|
83
|
+
"shared_languages",
|
|
84
|
+
"shared_manifest_types",
|
|
85
|
+
"shared_top_level_entries"
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
"suggested_focus": {
|
|
89
|
+
"type": "array",
|
|
90
|
+
"items": { "type": "string" }
|
|
91
|
+
},
|
|
92
|
+
"artifacts": {
|
|
93
|
+
"type": "object",
|
|
94
|
+
"required": [
|
|
95
|
+
"exchange_json",
|
|
96
|
+
"summary_md",
|
|
97
|
+
"transfer_map_md"
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
"notes": {
|
|
101
|
+
"type": "array",
|
|
102
|
+
"items": { "type": "string" }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://openresearchprotocol.com/spec/v1/hosted-workspace-event.schema.json",
|
|
4
|
+
"title": "ORP Hosted Workspace Event",
|
|
5
|
+
"description": "Timeline event for one hosted ORP workspace, used to build the detail-page activity stream and state history.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": [
|
|
9
|
+
"schema_version",
|
|
10
|
+
"event_id",
|
|
11
|
+
"workspace_id",
|
|
12
|
+
"event_type",
|
|
13
|
+
"actor",
|
|
14
|
+
"created_at_utc"
|
|
15
|
+
],
|
|
16
|
+
"properties": {
|
|
17
|
+
"schema_version": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"const": "1.0.0"
|
|
20
|
+
},
|
|
21
|
+
"event_id": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"minLength": 1
|
|
24
|
+
},
|
|
25
|
+
"workspace_id": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"minLength": 1
|
|
28
|
+
},
|
|
29
|
+
"event_type": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"enum": [
|
|
32
|
+
"workspace.created",
|
|
33
|
+
"workspace.updated",
|
|
34
|
+
"workspace.opened",
|
|
35
|
+
"snapshot.created",
|
|
36
|
+
"tab.added",
|
|
37
|
+
"tab.removed",
|
|
38
|
+
"tab.updated",
|
|
39
|
+
"focus.updated",
|
|
40
|
+
"trajectory.updated",
|
|
41
|
+
"summary.updated",
|
|
42
|
+
"bridge.synced"
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
"snapshot_id": {
|
|
46
|
+
"type": "string"
|
|
47
|
+
},
|
|
48
|
+
"actor": {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"additionalProperties": false,
|
|
51
|
+
"required": [
|
|
52
|
+
"type"
|
|
53
|
+
],
|
|
54
|
+
"properties": {
|
|
55
|
+
"type": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"enum": [
|
|
58
|
+
"user",
|
|
59
|
+
"agent",
|
|
60
|
+
"system"
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
"id": {
|
|
64
|
+
"type": "string"
|
|
65
|
+
},
|
|
66
|
+
"label": {
|
|
67
|
+
"type": "string"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"summary": {
|
|
72
|
+
"type": "string"
|
|
73
|
+
},
|
|
74
|
+
"tab_ids": {
|
|
75
|
+
"type": "array",
|
|
76
|
+
"items": {
|
|
77
|
+
"type": "string",
|
|
78
|
+
"minLength": 1
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"state_excerpt": {
|
|
82
|
+
"type": "object",
|
|
83
|
+
"additionalProperties": false,
|
|
84
|
+
"properties": {
|
|
85
|
+
"tab_count": {
|
|
86
|
+
"type": "integer",
|
|
87
|
+
"minimum": 0
|
|
88
|
+
},
|
|
89
|
+
"current_focus": {
|
|
90
|
+
"type": "string"
|
|
91
|
+
},
|
|
92
|
+
"trajectory": {
|
|
93
|
+
"type": "string"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"created_at_utc": {
|
|
98
|
+
"type": "string",
|
|
99
|
+
"format": "date-time"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|