open-research-protocol 0.4.34 → 0.4.36

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 CHANGED
@@ -6,6 +6,40 @@ There was no prior in-repo changelog file, so the first formal entry starts
6
6
  with the currently shipped `v0.4.4` release and summarizes the full release
7
7
  delta reflected in this repo.
8
8
 
9
+ ## v0.4.36 - 2026-04-30
10
+
11
+ ### Changed
12
+
13
+ - Clarified `orp workspace sync --dry-run` output for hosted workspace pushes
14
+ so compatibility notes are not described as stored idea notes when the hosted
15
+ workspace state is the authoritative payload.
16
+
17
+ ## v0.4.35 - 2026-04-30
18
+
19
+ This release makes the hosted workspace sync contract canonical from the ORP
20
+ CLI side, so hosted ORP can render the same projects, resume sessions, plans,
21
+ and tasks that local workspace tooling sees.
22
+
23
+ ### Added
24
+
25
+ - Added local workspace inventory reconciliation for `orp workspace sync`,
26
+ starting from the selected workspace ledger and refreshing known projects
27
+ from ORP startup state, Clawdad state, and recent Codex session metadata.
28
+ - Added hosted workspace state push support that carries per-project sync
29
+ metadata, linked ORP idea/feature ids, plan summaries, and task lists.
30
+ - Added a hosted workspace sync contract document covering the canonical
31
+ source order and required hosted payload fields.
32
+
33
+ ### Changed
34
+
35
+ - `orp workspace sync main` now bridges a local managed workspace file to the
36
+ matching hosted workspace by durable `workspaceId` before pushing state.
37
+ - Hosted workspace sync skips the idea-note compatibility mirror when the
38
+ hosted workspace state can be written directly, avoiding stale or truncated
39
+ plan/task payloads.
40
+ - Workspace manifests now round-trip activity timestamps, sync timestamps,
41
+ sync source labels, plan data, tasks, and linked ORP project references.
42
+
9
43
  ## v0.4.34 - 2026-04-30
10
44
 
11
45
  This release connects ORP frontier plans to hosted ORP ideas/features and lets
@@ -27,6 +27,45 @@ The canonical hosted source of truth should be one hosted workspace record:
27
27
 
28
28
  Idea notes are a compatibility bridge only after this contract lands.
29
29
 
30
+ ## CLI Sync Contract
31
+
32
+ `orp workspace sync <selector>` is the canonical local-to-hosted sync path.
33
+ The web app should render the hosted workspace state produced by this command,
34
+ not reconstruct a competing workspace model.
35
+
36
+ The local canonical input is a reconciled workspace snapshot:
37
+
38
+ 1. Start with the selected ORP workspace ledger, usually
39
+ `orp workspace tabs main`.
40
+ 2. If that selector resolves to a local workspace file, match a hosted
41
+ workspace by the same `workspaceId` before writing hosted state.
42
+ 3. Reconcile known local projects from ORP project startup state
43
+ (`orp/state.json`, including historical startup result manifests).
44
+ 4. Use Clawdad state only to refresh projects already known from the ledger or
45
+ ORP startup manifests, unless a caller explicitly opts into Clawdad-only
46
+ projects.
47
+ 5. Use recent Codex session metadata to refresh known project paths with the
48
+ newest local `codex resume ...` session. Codex-only paths are not added by
49
+ default.
50
+
51
+ The pushed hosted state must include, for every project/tab where available:
52
+
53
+ - `title`
54
+ - `project_root` / local `path`
55
+ - resume command plus structured `resume_tool` and `resume_session_id`
56
+ - `last_activity_at_utc`
57
+ - `last_synced_at_utc`
58
+ - `linked_idea_id`
59
+ - `linked_feature_id`
60
+ - `plan`
61
+ - `tasks`
62
+
63
+ Idea notes are only a compatibility mirror. When sync can write the matched
64
+ hosted workspace state directly, the hosted workspace `state` is authoritative
65
+ and the idea-note mirror may be skipped to avoid truncating or conflicting with
66
+ the full payload. If no hosted workspace target exists, sync may still write a
67
+ compact ` ```orp-workspace ` block to the linked idea.
68
+
30
69
  ## Resource Model
31
70
 
32
71
  ### Hosted Workspace
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-research-protocol",
3
- "version": "0.4.34",
3
+ "version": "0.4.36",
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>",
@@ -267,6 +267,12 @@ function normalizeStructuredTab(rawTab, index) {
267
267
  const tmuxSessionName = normalizeOptionalString(rawTab.tmuxSessionName);
268
268
  const plan = rawTab.plan && typeof rawTab.plan === "object" && !Array.isArray(rawTab.plan) ? rawTab.plan : null;
269
269
  const tasks = Array.isArray(rawTab.tasks) ? rawTab.tasks : [];
270
+ const lastActivityAt = normalizeOptionalString(
271
+ rawTab.lastActivityAt ?? rawTab.last_activity_at_utc ?? rawTab.lastActivityAtUtc,
272
+ );
273
+ const lastSyncedAt = normalizeOptionalString(
274
+ rawTab.lastSyncedAt ?? rawTab.last_synced_at_utc ?? rawTab.lastSyncedAtUtc,
275
+ );
270
276
 
271
277
  return {
272
278
  lineNumber: index + 1,
@@ -285,6 +291,9 @@ function normalizeStructuredTab(rawTab, index) {
285
291
  linkedFeatureId: normalizeOptionalString(rawTab.linkedFeatureId ?? rawTab.linked_feature_id),
286
292
  plan,
287
293
  tasks,
294
+ lastActivityAt,
295
+ lastSyncedAt,
296
+ syncSource: normalizeOptionalString(rawTab.syncSource ?? rawTab.sync_source),
288
297
  };
289
298
  }
290
299
 
@@ -321,6 +330,21 @@ function normalizeStructuredProject(rawProject, projectIndex) {
321
330
  rawProject.linked_feature_id,
322
331
  plan: rawSession.plan ?? rawProject.plan,
323
332
  tasks: rawSession.tasks ?? rawProject.tasks,
333
+ lastActivityAt:
334
+ rawSession.lastActivityAt ??
335
+ rawSession.last_activity_at_utc ??
336
+ rawSession.lastActivityAtUtc ??
337
+ rawProject.lastActivityAt ??
338
+ rawProject.last_activity_at_utc ??
339
+ rawProject.lastActivityAtUtc,
340
+ lastSyncedAt:
341
+ rawSession.lastSyncedAt ??
342
+ rawSession.last_synced_at_utc ??
343
+ rawSession.lastSyncedAtUtc ??
344
+ rawProject.lastSyncedAt ??
345
+ rawProject.last_synced_at_utc ??
346
+ rawProject.lastSyncedAtUtc,
347
+ syncSource: rawSession.syncSource ?? rawSession.sync_source ?? rawProject.syncSource ?? rawProject.sync_source,
324
348
  },
325
349
  sessionIndex,
326
350
  );
@@ -399,6 +423,8 @@ export function buildWorkspaceProjectGroups(entries = []) {
399
423
  linkedFeatureId: normalizeOptionalString(entry.linkedFeatureId),
400
424
  plan: entry.plan && typeof entry.plan === "object" && !Array.isArray(entry.plan) ? entry.plan : null,
401
425
  tasks: Array.isArray(entry.tasks) && entry.tasks.length > 0 ? entry.tasks : [],
426
+ lastActivityAt: normalizeOptionalString(entry.lastActivityAt ?? entry.last_activity_at_utc),
427
+ lastSyncedAt: normalizeOptionalString(entry.lastSyncedAt ?? entry.last_synced_at_utc),
402
428
  sessions: [],
403
429
  });
404
430
  }
@@ -412,6 +438,8 @@ export function buildWorkspaceProjectGroups(entries = []) {
412
438
  project.plan =
413
439
  project.plan ||
414
440
  (entry.plan && typeof entry.plan === "object" && !Array.isArray(entry.plan) ? entry.plan : null);
441
+ project.lastActivityAt = project.lastActivityAt || normalizeOptionalString(entry.lastActivityAt ?? entry.last_activity_at_utc);
442
+ project.lastSyncedAt = project.lastSyncedAt || normalizeOptionalString(entry.lastSyncedAt ?? entry.last_synced_at_utc);
415
443
  if ((!Array.isArray(project.tasks) || project.tasks.length === 0) && Array.isArray(entry.tasks) && entry.tasks.length > 0) {
416
444
  project.tasks = entry.tasks;
417
445
  }
@@ -426,6 +454,8 @@ export function buildWorkspaceProjectGroups(entries = []) {
426
454
  resumeSessionId: resume.resumeSessionId || undefined,
427
455
  codexSessionId: resume.resumeTool === "codex" ? resume.resumeSessionId || undefined : undefined,
428
456
  claudeSessionId: resume.resumeTool === "claude" ? resume.resumeSessionId || undefined : undefined,
457
+ lastActivityAt: normalizeOptionalString(entry.lastActivityAt ?? entry.last_activity_at_utc) || undefined,
458
+ lastSyncedAt: normalizeOptionalString(entry.lastSyncedAt ?? entry.last_synced_at_utc) || undefined,
429
459
  }).filter(([, value]) => value !== undefined),
430
460
  ),
431
461
  );
@@ -443,6 +473,8 @@ export function buildWorkspaceProjectGroups(entries = []) {
443
473
  linkedFeatureId: project.linkedFeatureId || undefined,
444
474
  plan: project.plan || undefined,
445
475
  tasks: Array.isArray(project.tasks) && project.tasks.length > 0 ? project.tasks : undefined,
476
+ lastActivityAt: project.lastActivityAt || undefined,
477
+ lastSyncedAt: project.lastSyncedAt || undefined,
446
478
  sessionCount: project.sessions.length,
447
479
  sessions: project.sessions,
448
480
  }).filter(([, value]) => value !== undefined),
@@ -72,6 +72,7 @@ function normalizePreviousHostedTabs(workspace) {
72
72
  focusSummary: normalizeOptionalString(tab.focus_summary ?? tab.focusSummary),
73
73
  trajectorySummary: normalizeOptionalString(tab.trajectory_summary ?? tab.trajectorySummary),
74
74
  lastActivityAt: normalizeOptionalString(tab.last_activity_at_utc ?? tab.lastActivityAtUtc),
75
+ lastSyncedAt: normalizeOptionalString(tab.last_synced_at_utc ?? tab.lastSyncedAtUtc),
75
76
  linkedIdeaId: normalizeOptionalString(tab.linked_idea_id ?? tab.linkedIdeaId),
76
77
  linkedFeatureId: normalizeOptionalString(tab.linked_feature_id ?? tab.linkedFeatureId),
77
78
  plan: tab.plan && typeof tab.plan === "object" && !Array.isArray(tab.plan) ? tab.plan : null,
@@ -116,6 +117,8 @@ function buildHostedProjectGroups(tabs) {
116
117
  bootstrap_command: normalizeOptionalString(tab.bootstrap_command),
117
118
  linked_idea_id: normalizeOptionalString(tab.linked_idea_id),
118
119
  linked_feature_id: normalizeOptionalString(tab.linked_feature_id),
120
+ last_activity_at_utc: normalizeOptionalString(tab.last_activity_at_utc),
121
+ last_synced_at_utc: normalizeOptionalString(tab.last_synced_at_utc),
119
122
  plan: tab.plan && typeof tab.plan === "object" && !Array.isArray(tab.plan) ? tab.plan : undefined,
120
123
  tasks: Array.isArray(tab.tasks) ? tab.tasks : undefined,
121
124
  sessions: [],
@@ -127,6 +130,8 @@ function buildHostedProjectGroups(tabs) {
127
130
  project.bootstrap_command = project.bootstrap_command || normalizeOptionalString(tab.bootstrap_command);
128
131
  project.linked_idea_id = project.linked_idea_id || normalizeOptionalString(tab.linked_idea_id);
129
132
  project.linked_feature_id = project.linked_feature_id || normalizeOptionalString(tab.linked_feature_id);
133
+ project.last_activity_at_utc = project.last_activity_at_utc || normalizeOptionalString(tab.last_activity_at_utc);
134
+ project.last_synced_at_utc = project.last_synced_at_utc || normalizeOptionalString(tab.last_synced_at_utc);
130
135
  project.plan = project.plan || (tab.plan && typeof tab.plan === "object" && !Array.isArray(tab.plan) ? tab.plan : undefined);
131
136
  if ((!Array.isArray(project.tasks) || project.tasks.length === 0) && Array.isArray(tab.tasks) && tab.tasks.length > 0) {
132
137
  project.tasks = tab.tasks;
@@ -143,6 +148,8 @@ function buildHostedProjectGroups(tabs) {
143
148
  claude_session_id: normalizeOptionalString(tab.claude_session_id),
144
149
  status: normalizeOptionalString(tab.status),
145
150
  current_task: normalizeOptionalString(tab.current_task),
151
+ last_activity_at_utc: normalizeOptionalString(tab.last_activity_at_utc),
152
+ last_synced_at_utc: normalizeOptionalString(tab.last_synced_at_utc),
146
153
  }).filter(([, value]) => value !== undefined && value !== null),
147
154
  ),
148
155
  );
@@ -158,6 +165,8 @@ function buildHostedProjectGroups(tabs) {
158
165
  bootstrap_command: project.bootstrap_command || undefined,
159
166
  linked_idea_id: project.linked_idea_id || undefined,
160
167
  linked_feature_id: project.linked_feature_id || undefined,
168
+ last_activity_at_utc: project.last_activity_at_utc || undefined,
169
+ last_synced_at_utc: project.last_synced_at_utc || undefined,
161
170
  plan: project.plan || undefined,
162
171
  tasks: Array.isArray(project.tasks) && project.tasks.length > 0 ? project.tasks : undefined,
163
172
  session_count: project.sessions.length,
@@ -448,7 +457,7 @@ export function buildHostedWorkspaceState(manifest, options = {}) {
448
457
  const title = normalizeOptionalString(tab.title) || previous?.title || null;
449
458
  const projectRoot = normalizeOptionalString(tab.path);
450
459
  const projectContext = readProjectFrontierContext(projectRoot, projectContextCache);
451
- const repoLabel = previous?.repoLabel || path.basename(String(projectRoot).replace(/\/+$/, "")) || projectRoot;
460
+ const repoLabel = title || previous?.repoLabel || path.basename(String(projectRoot).replace(/\/+$/, "")) || projectRoot;
452
461
  const terminalTitle = previous?.terminalTitle || title || repoLabel;
453
462
  const resume = resolveResumeMetadata({
454
463
  resumeCommand: tab.resumeCommand,
@@ -475,6 +484,27 @@ export function buildHostedWorkspaceState(manifest, options = {}) {
475
484
  title,
476
485
  orderIndex: index,
477
486
  });
487
+ const plan =
488
+ tab.plan && typeof tab.plan === "object" && !Array.isArray(tab.plan)
489
+ ? tab.plan
490
+ : projectContext?.plan || previous?.plan || undefined;
491
+ const tasks =
492
+ Array.isArray(tab.tasks) && tab.tasks.length > 0
493
+ ? tab.tasks
494
+ : Array.isArray(projectContext?.tasks) && projectContext.tasks.length > 0
495
+ ? projectContext.tasks
496
+ : Array.isArray(previous?.tasks) && previous.tasks.length > 0
497
+ ? previous.tasks
498
+ : undefined;
499
+ const lastActivityAt =
500
+ normalizeOptionalString(tab.lastActivityAt ?? tab.last_activity_at_utc ?? tab.lastActivityAtUtc) ||
501
+ previous?.lastActivityAt ||
502
+ undefined;
503
+ const lastSyncedAt =
504
+ normalizeOptionalString(tab.lastSyncedAt ?? tab.last_synced_at_utc ?? tab.lastSyncedAtUtc) ||
505
+ previous?.lastSyncedAt ||
506
+ updatedAt ||
507
+ undefined;
478
508
 
479
509
  return Object.fromEntries(
480
510
  Object.entries({
@@ -496,7 +526,9 @@ export function buildHostedWorkspaceState(manifest, options = {}) {
496
526
  current_task: previous?.currentTask || undefined,
497
527
  focus_summary: previous?.focusSummary || undefined,
498
528
  trajectory_summary: previous?.trajectorySummary || undefined,
499
- last_activity_at_utc: previous?.lastActivityAt || undefined,
529
+ last_activity_at_utc: lastActivityAt,
530
+ last_synced_at_utc: lastSyncedAt,
531
+ sync_source: normalizeOptionalString(tab.syncSource ?? tab.sync_source) || undefined,
500
532
  linked_feature_id:
501
533
  normalizeOptionalString(tab.linkedFeatureId ?? tab.linked_feature_id) ||
502
534
  projectContext?.link?.activeFeatureId ||
@@ -507,13 +539,8 @@ export function buildHostedWorkspaceState(manifest, options = {}) {
507
539
  projectContext?.link?.ideaId ||
508
540
  previous?.linkedIdeaId ||
509
541
  undefined,
510
- plan: projectContext?.plan || previous?.plan || undefined,
511
- tasks:
512
- Array.isArray(projectContext?.tasks) && projectContext.tasks.length > 0
513
- ? projectContext.tasks
514
- : Array.isArray(previous?.tasks) && previous.tasks.length > 0
515
- ? previous.tasks
516
- : undefined,
542
+ plan,
543
+ tasks,
517
544
  }).filter(([, value]) => value !== undefined && value !== null),
518
545
  );
519
546
  });
@@ -531,6 +558,10 @@ export function buildHostedWorkspaceState(manifest, options = {}) {
531
558
  durable_backend: options.durableBackend || "manual-ledger",
532
559
  }).filter(([, value]) => value !== undefined && value !== null),
533
560
  );
561
+ const sourceContract =
562
+ options.localInventory?.contract && typeof options.localInventory.contract === "object" && !Array.isArray(options.localInventory.contract)
563
+ ? options.localInventory.contract
564
+ : undefined;
534
565
 
535
566
  const stateVersion = Math.max(1, (getHostedIntegerValue(previousState, "state_version", "stateVersion") || 0) + 1);
536
567
  const snapshotSeed = JSON.stringify({
@@ -558,6 +589,7 @@ export function buildHostedWorkspaceState(manifest, options = {}) {
558
589
  tab_count: tabs.length,
559
590
  project_count: projects.length,
560
591
  capture_context: Object.keys(captureContext).length > 0 ? captureContext : undefined,
592
+ source_contract: sourceContract,
561
593
  projects,
562
594
  tabs,
563
595
  }).filter(([, value]) => value !== undefined && value !== null),
@@ -63,6 +63,7 @@ export {
63
63
  fetchIdeaPayload,
64
64
  fetchIdeasPayload,
65
65
  fetchHostedWorkspacesPayload,
66
+ findHostedWorkspaceByWorkspaceId,
66
67
  findHostedWorkspaceByLinkedIdea,
67
68
  findHostedWorkspaceLinkedToIdea,
68
69
  loadWorkspaceSource,
@@ -72,6 +73,11 @@ export {
72
73
  resolveWorkspaceSelectorFromCollections,
73
74
  updateIdeaPayload,
74
75
  } from "./orp.js";
76
+ export {
77
+ buildLocalProjectInventory,
78
+ inferLocalProjectRoots,
79
+ mergeLocalProjectInventoryIntoManifest,
80
+ } from "./local-inventory.js";
75
81
  export {
76
82
  cacheManagedWorkspaceManifest,
77
83
  clearWorkspaceSlot,