open-research-protocol 0.4.33 → 0.4.35
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 +50 -0
- package/cli/__pycache__/orp.cpython-311.pyc +0 -0
- package/cli/orp.py +430 -11
- package/docs/ORP_HOSTED_WORKSPACE_CONTRACT.md +39 -0
- package/package.json +1 -1
- package/packages/orp-workspace-launcher/src/core-plan.js +62 -0
- package/packages/orp-workspace-launcher/src/hosted-state.js +328 -4
- package/packages/orp-workspace-launcher/src/index.js +11 -1
- package/packages/orp-workspace-launcher/src/local-inventory.js +681 -0
- package/packages/orp-workspace-launcher/src/orp.js +23 -0
- package/packages/orp-workspace-launcher/src/registry.js +24 -11
- package/packages/orp-workspace-launcher/src/sync.js +241 -14
- package/packages/orp-workspace-launcher/test/core-plan.test.js +48 -0
- package/packages/orp-workspace-launcher/test/hosted-state.test.js +187 -0
- package/packages/orp-workspace-launcher/test/local-inventory.test.js +126 -0
- package/packages/orp-workspace-launcher/test/orp.test.js +19 -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
|
@@ -265,6 +265,14 @@ function normalizeStructuredTab(rawTab, index) {
|
|
|
265
265
|
const title = normalizeOptionalString(rawTab.title);
|
|
266
266
|
const resume = resolveResumeMetadata(rawTab);
|
|
267
267
|
const tmuxSessionName = normalizeOptionalString(rawTab.tmuxSessionName);
|
|
268
|
+
const plan = rawTab.plan && typeof rawTab.plan === "object" && !Array.isArray(rawTab.plan) ? rawTab.plan : null;
|
|
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
|
+
);
|
|
268
276
|
|
|
269
277
|
return {
|
|
270
278
|
lineNumber: index + 1,
|
|
@@ -279,6 +287,13 @@ function normalizeStructuredTab(rawTab, index) {
|
|
|
279
287
|
remoteUrl: normalizeOptionalUrl(rawTab.remoteUrl, `workspace tab ${index + 1} remoteUrl`),
|
|
280
288
|
remoteBranch: normalizeOptionalString(rawTab.remoteBranch),
|
|
281
289
|
bootstrapCommand: normalizeOptionalCommand(rawTab.bootstrapCommand),
|
|
290
|
+
linkedIdeaId: normalizeOptionalString(rawTab.linkedIdeaId ?? rawTab.linked_idea_id),
|
|
291
|
+
linkedFeatureId: normalizeOptionalString(rawTab.linkedFeatureId ?? rawTab.linked_feature_id),
|
|
292
|
+
plan,
|
|
293
|
+
tasks,
|
|
294
|
+
lastActivityAt,
|
|
295
|
+
lastSyncedAt,
|
|
296
|
+
syncSource: normalizeOptionalString(rawTab.syncSource ?? rawTab.sync_source),
|
|
282
297
|
};
|
|
283
298
|
}
|
|
284
299
|
|
|
@@ -307,6 +322,29 @@ function normalizeStructuredProject(rawProject, projectIndex) {
|
|
|
307
322
|
codexSessionId: rawSession.codexSessionId,
|
|
308
323
|
claudeSessionId: rawSession.claudeSessionId,
|
|
309
324
|
tmuxSessionName: rawSession.tmuxSessionName,
|
|
325
|
+
linkedIdeaId: rawSession.linkedIdeaId ?? rawSession.linked_idea_id ?? rawProject.linkedIdeaId ?? rawProject.linked_idea_id,
|
|
326
|
+
linkedFeatureId:
|
|
327
|
+
rawSession.linkedFeatureId ??
|
|
328
|
+
rawSession.linked_feature_id ??
|
|
329
|
+
rawProject.linkedFeatureId ??
|
|
330
|
+
rawProject.linked_feature_id,
|
|
331
|
+
plan: rawSession.plan ?? rawProject.plan,
|
|
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,
|
|
310
348
|
},
|
|
311
349
|
sessionIndex,
|
|
312
350
|
);
|
|
@@ -381,6 +419,12 @@ export function buildWorkspaceProjectGroups(entries = []) {
|
|
|
381
419
|
remoteUrl: normalizeOptionalUrl(entry.remoteUrl, "workspace project remoteUrl"),
|
|
382
420
|
remoteBranch: normalizeOptionalString(entry.remoteBranch),
|
|
383
421
|
bootstrapCommand: normalizeOptionalCommand(entry.bootstrapCommand),
|
|
422
|
+
linkedIdeaId: normalizeOptionalString(entry.linkedIdeaId),
|
|
423
|
+
linkedFeatureId: normalizeOptionalString(entry.linkedFeatureId),
|
|
424
|
+
plan: entry.plan && typeof entry.plan === "object" && !Array.isArray(entry.plan) ? entry.plan : null,
|
|
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),
|
|
384
428
|
sessions: [],
|
|
385
429
|
});
|
|
386
430
|
}
|
|
@@ -389,6 +433,16 @@ export function buildWorkspaceProjectGroups(entries = []) {
|
|
|
389
433
|
project.remoteUrl = project.remoteUrl || normalizeOptionalUrl(entry.remoteUrl, "workspace project remoteUrl");
|
|
390
434
|
project.remoteBranch = project.remoteBranch || normalizeOptionalString(entry.remoteBranch);
|
|
391
435
|
project.bootstrapCommand = project.bootstrapCommand || normalizeOptionalCommand(entry.bootstrapCommand);
|
|
436
|
+
project.linkedIdeaId = project.linkedIdeaId || normalizeOptionalString(entry.linkedIdeaId);
|
|
437
|
+
project.linkedFeatureId = project.linkedFeatureId || normalizeOptionalString(entry.linkedFeatureId);
|
|
438
|
+
project.plan =
|
|
439
|
+
project.plan ||
|
|
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);
|
|
443
|
+
if ((!Array.isArray(project.tasks) || project.tasks.length === 0) && Array.isArray(entry.tasks) && entry.tasks.length > 0) {
|
|
444
|
+
project.tasks = entry.tasks;
|
|
445
|
+
}
|
|
392
446
|
|
|
393
447
|
const resume = resolveResumeMetadata(entry);
|
|
394
448
|
project.sessions.push(
|
|
@@ -400,6 +454,8 @@ export function buildWorkspaceProjectGroups(entries = []) {
|
|
|
400
454
|
resumeSessionId: resume.resumeSessionId || undefined,
|
|
401
455
|
codexSessionId: resume.resumeTool === "codex" ? resume.resumeSessionId || undefined : undefined,
|
|
402
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,
|
|
403
459
|
}).filter(([, value]) => value !== undefined),
|
|
404
460
|
),
|
|
405
461
|
);
|
|
@@ -413,6 +469,12 @@ export function buildWorkspaceProjectGroups(entries = []) {
|
|
|
413
469
|
remoteUrl: project.remoteUrl || undefined,
|
|
414
470
|
remoteBranch: project.remoteBranch || undefined,
|
|
415
471
|
bootstrapCommand: project.bootstrapCommand || undefined,
|
|
472
|
+
linkedIdeaId: project.linkedIdeaId || undefined,
|
|
473
|
+
linkedFeatureId: project.linkedFeatureId || undefined,
|
|
474
|
+
plan: project.plan || undefined,
|
|
475
|
+
tasks: Array.isArray(project.tasks) && project.tasks.length > 0 ? project.tasks : undefined,
|
|
476
|
+
lastActivityAt: project.lastActivityAt || undefined,
|
|
477
|
+
lastSyncedAt: project.lastSyncedAt || undefined,
|
|
416
478
|
sessionCount: project.sessions.length,
|
|
417
479
|
sessions: project.sessions,
|
|
418
480
|
}).filter(([, value]) => value !== undefined),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
|
+
import fs from "node:fs";
|
|
4
5
|
|
|
5
6
|
import { resolveResumeMetadata } from "./core-plan.js";
|
|
6
7
|
|
|
@@ -71,8 +72,11 @@ function normalizePreviousHostedTabs(workspace) {
|
|
|
71
72
|
focusSummary: normalizeOptionalString(tab.focus_summary ?? tab.focusSummary),
|
|
72
73
|
trajectorySummary: normalizeOptionalString(tab.trajectory_summary ?? tab.trajectorySummary),
|
|
73
74
|
lastActivityAt: normalizeOptionalString(tab.last_activity_at_utc ?? tab.lastActivityAtUtc),
|
|
75
|
+
lastSyncedAt: normalizeOptionalString(tab.last_synced_at_utc ?? tab.lastSyncedAtUtc),
|
|
74
76
|
linkedIdeaId: normalizeOptionalString(tab.linked_idea_id ?? tab.linkedIdeaId),
|
|
75
77
|
linkedFeatureId: normalizeOptionalString(tab.linked_feature_id ?? tab.linkedFeatureId),
|
|
78
|
+
plan: tab.plan && typeof tab.plan === "object" && !Array.isArray(tab.plan) ? tab.plan : null,
|
|
79
|
+
tasks: Array.isArray(tab.tasks) ? tab.tasks : [],
|
|
76
80
|
used: false,
|
|
77
81
|
};
|
|
78
82
|
});
|
|
@@ -111,6 +115,12 @@ function buildHostedProjectGroups(tabs) {
|
|
|
111
115
|
remote_url: normalizeOptionalString(tab.remote_url),
|
|
112
116
|
remote_branch: normalizeOptionalString(tab.remote_branch),
|
|
113
117
|
bootstrap_command: normalizeOptionalString(tab.bootstrap_command),
|
|
118
|
+
linked_idea_id: normalizeOptionalString(tab.linked_idea_id),
|
|
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),
|
|
122
|
+
plan: tab.plan && typeof tab.plan === "object" && !Array.isArray(tab.plan) ? tab.plan : undefined,
|
|
123
|
+
tasks: Array.isArray(tab.tasks) ? tab.tasks : undefined,
|
|
114
124
|
sessions: [],
|
|
115
125
|
});
|
|
116
126
|
}
|
|
@@ -118,6 +128,14 @@ function buildHostedProjectGroups(tabs) {
|
|
|
118
128
|
project.remote_url = project.remote_url || normalizeOptionalString(tab.remote_url);
|
|
119
129
|
project.remote_branch = project.remote_branch || normalizeOptionalString(tab.remote_branch);
|
|
120
130
|
project.bootstrap_command = project.bootstrap_command || normalizeOptionalString(tab.bootstrap_command);
|
|
131
|
+
project.linked_idea_id = project.linked_idea_id || normalizeOptionalString(tab.linked_idea_id);
|
|
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);
|
|
135
|
+
project.plan = project.plan || (tab.plan && typeof tab.plan === "object" && !Array.isArray(tab.plan) ? tab.plan : undefined);
|
|
136
|
+
if ((!Array.isArray(project.tasks) || project.tasks.length === 0) && Array.isArray(tab.tasks) && tab.tasks.length > 0) {
|
|
137
|
+
project.tasks = tab.tasks;
|
|
138
|
+
}
|
|
121
139
|
project.sessions.push(
|
|
122
140
|
Object.fromEntries(
|
|
123
141
|
Object.entries({
|
|
@@ -130,6 +148,8 @@ function buildHostedProjectGroups(tabs) {
|
|
|
130
148
|
claude_session_id: normalizeOptionalString(tab.claude_session_id),
|
|
131
149
|
status: normalizeOptionalString(tab.status),
|
|
132
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),
|
|
133
153
|
}).filter(([, value]) => value !== undefined && value !== null),
|
|
134
154
|
),
|
|
135
155
|
);
|
|
@@ -143,6 +163,12 @@ function buildHostedProjectGroups(tabs) {
|
|
|
143
163
|
remote_url: project.remote_url || undefined,
|
|
144
164
|
remote_branch: project.remote_branch || undefined,
|
|
145
165
|
bootstrap_command: project.bootstrap_command || undefined,
|
|
166
|
+
linked_idea_id: project.linked_idea_id || undefined,
|
|
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,
|
|
170
|
+
plan: project.plan || undefined,
|
|
171
|
+
tasks: Array.isArray(project.tasks) && project.tasks.length > 0 ? project.tasks : undefined,
|
|
146
172
|
session_count: project.sessions.length,
|
|
147
173
|
sessions: project.sessions,
|
|
148
174
|
}).filter(([, value]) => value !== undefined && value !== null),
|
|
@@ -150,6 +176,264 @@ function buildHostedProjectGroups(tabs) {
|
|
|
150
176
|
);
|
|
151
177
|
}
|
|
152
178
|
|
|
179
|
+
function readJsonIfExists(filePath) {
|
|
180
|
+
try {
|
|
181
|
+
if (!fs.existsSync(filePath)) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
185
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function readTextIfExists(filePath) {
|
|
192
|
+
try {
|
|
193
|
+
if (!fs.existsSync(filePath)) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
const text = fs.readFileSync(filePath, "utf8").trim();
|
|
197
|
+
return text.length > 0 ? text : null;
|
|
198
|
+
} catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function resolveGitDir(projectRoot) {
|
|
204
|
+
const dotGit = path.join(projectRoot, ".git");
|
|
205
|
+
try {
|
|
206
|
+
const stat = fs.statSync(dotGit);
|
|
207
|
+
if (stat.isDirectory()) {
|
|
208
|
+
return dotGit;
|
|
209
|
+
}
|
|
210
|
+
if (stat.isFile()) {
|
|
211
|
+
const text = fs.readFileSync(dotGit, "utf8").trim();
|
|
212
|
+
const match = text.match(/^gitdir:\s*(.+)$/i);
|
|
213
|
+
if (match) {
|
|
214
|
+
const gitDir = match[1].trim();
|
|
215
|
+
return path.isAbsolute(gitDir) ? gitDir : path.resolve(projectRoot, gitDir);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function readProjectLink(projectRoot, cache) {
|
|
225
|
+
const normalizedRoot = normalizeOptionalString(projectRoot);
|
|
226
|
+
if (!normalizedRoot) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
const cacheKey = `link:${normalizedRoot}`;
|
|
230
|
+
if (cache.has(cacheKey)) {
|
|
231
|
+
return cache.get(cacheKey);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const gitDir = resolveGitDir(normalizedRoot);
|
|
235
|
+
const linkPath = gitDir ? path.join(gitDir, "orp", "link", "project.json") : null;
|
|
236
|
+
const link = linkPath ? readJsonIfExists(linkPath) : null;
|
|
237
|
+
const ideaId = normalizeOptionalString(link?.idea_id ?? link?.ideaId);
|
|
238
|
+
if (!ideaId) {
|
|
239
|
+
cache.set(cacheKey, null);
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const frontierFeatureIds =
|
|
243
|
+
link?.frontier_feature_ids && typeof link.frontier_feature_ids === "object" && !Array.isArray(link.frontier_feature_ids)
|
|
244
|
+
? link.frontier_feature_ids
|
|
245
|
+
: link?.frontierFeatureIds && typeof link.frontierFeatureIds === "object" && !Array.isArray(link.frontierFeatureIds)
|
|
246
|
+
? link.frontierFeatureIds
|
|
247
|
+
: {};
|
|
248
|
+
const normalizedLink = {
|
|
249
|
+
ideaId,
|
|
250
|
+
ideaTitle: normalizeOptionalString(link?.idea_title ?? link?.ideaTitle),
|
|
251
|
+
activeFeatureId: normalizeOptionalString(
|
|
252
|
+
link?.active_feature_id ?? link?.activeFeatureId ?? link?.linked_feature_id ?? link?.linkedFeatureId,
|
|
253
|
+
),
|
|
254
|
+
frontierFeatureIds,
|
|
255
|
+
};
|
|
256
|
+
cache.set(cacheKey, normalizedLink);
|
|
257
|
+
return normalizedLink;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function findFrontierVersion(stack, versionId) {
|
|
261
|
+
const versions = Array.isArray(stack?.versions) ? stack.versions : [];
|
|
262
|
+
return versions.find((version) => normalizeOptionalString(version?.id) === versionId) || null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function findFrontierMilestone(stack, milestoneId) {
|
|
266
|
+
const versions = Array.isArray(stack?.versions) ? stack.versions : [];
|
|
267
|
+
for (const version of versions) {
|
|
268
|
+
const milestones = Array.isArray(version?.milestones) ? version.milestones : [];
|
|
269
|
+
const milestone = milestones.find((row) => normalizeOptionalString(row?.id) === milestoneId);
|
|
270
|
+
if (milestone) {
|
|
271
|
+
return { version, milestone };
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return { version: null, milestone: null };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function findFrontierPhase(milestone, phaseId) {
|
|
278
|
+
const phases = Array.isArray(milestone?.phases) ? milestone.phases : [];
|
|
279
|
+
return phases.find((phase) => normalizeOptionalString(phase?.id) === phaseId) || null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function taskStatusFromFrontierStatus(value) {
|
|
283
|
+
const text = normalizeOptionalString(value)?.toLowerCase().replace(/[-\s]+/g, "_") || "";
|
|
284
|
+
if (["complete", "completed", "done", "terminal"].includes(text)) {
|
|
285
|
+
return "done";
|
|
286
|
+
}
|
|
287
|
+
if (["active", "in_progress", "running"].includes(text)) {
|
|
288
|
+
return "in_progress";
|
|
289
|
+
}
|
|
290
|
+
if (["blocked", "stuck"].includes(text)) {
|
|
291
|
+
return "blocked";
|
|
292
|
+
}
|
|
293
|
+
if (["skipped", "canceled", "cancelled"].includes(text)) {
|
|
294
|
+
return "skipped";
|
|
295
|
+
}
|
|
296
|
+
return "todo";
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function titleFromTas(tasText) {
|
|
300
|
+
if (!tasText) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
const line = tasText
|
|
304
|
+
.split(/\r?\n/)
|
|
305
|
+
.map((row) => row.trim())
|
|
306
|
+
.find((row) => row.startsWith("# "));
|
|
307
|
+
return line ? line.replace(/^#\s+/, "").trim() || null : null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function buildFrontierPlan({ projectRoot, tasText, state, stack }) {
|
|
311
|
+
const activeVersionId = normalizeOptionalString(state?.active_version ?? stack?.current_frontier?.active_version);
|
|
312
|
+
const activeMilestoneId = normalizeOptionalString(state?.active_milestone ?? stack?.current_frontier?.active_milestone);
|
|
313
|
+
const activePhaseId = normalizeOptionalString(state?.active_phase ?? stack?.current_frontier?.active_phase);
|
|
314
|
+
const nextAction = normalizeOptionalString(state?.next_action ?? stack?.current_frontier?.next_action);
|
|
315
|
+
const version = activeVersionId ? findFrontierVersion(stack, activeVersionId) : null;
|
|
316
|
+
const { milestone } = activeMilestoneId ? findFrontierMilestone(stack, activeMilestoneId) : { milestone: null };
|
|
317
|
+
const phase = activePhaseId ? findFrontierPhase(milestone, activePhaseId) : null;
|
|
318
|
+
const tasTitle = titleFromTas(tasText);
|
|
319
|
+
const summary =
|
|
320
|
+
tasTitle ||
|
|
321
|
+
normalizeOptionalString(milestone?.label) ||
|
|
322
|
+
normalizeOptionalString(phase?.label) ||
|
|
323
|
+
normalizeOptionalString(version?.label) ||
|
|
324
|
+
nextAction;
|
|
325
|
+
|
|
326
|
+
const bodyParts = [
|
|
327
|
+
nextAction ? `Current next action: ${nextAction}` : "",
|
|
328
|
+
activeVersionId || activeMilestoneId || activePhaseId
|
|
329
|
+
? `Active frontier: ${[activeVersionId, activeMilestoneId, activePhaseId].filter(Boolean).join(" / ")}`
|
|
330
|
+
: "",
|
|
331
|
+
tasText || "",
|
|
332
|
+
].filter(Boolean);
|
|
333
|
+
|
|
334
|
+
if (!summary && bodyParts.length === 0) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
summary: summary || null,
|
|
340
|
+
body: bodyParts.join("\n\n"),
|
|
341
|
+
source: tasText ? "orp/frontier/TAS.md" : "orp/frontier/state.json",
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function buildFrontierTasks({ state, stack }) {
|
|
346
|
+
const activeMilestoneId = normalizeOptionalString(state?.active_milestone ?? stack?.current_frontier?.active_milestone);
|
|
347
|
+
const activePhaseId = normalizeOptionalString(state?.active_phase ?? stack?.current_frontier?.active_phase);
|
|
348
|
+
const { milestone } = activeMilestoneId ? findFrontierMilestone(stack, activeMilestoneId) : { milestone: null };
|
|
349
|
+
const phases = Array.isArray(milestone?.phases) ? milestone.phases : [];
|
|
350
|
+
|
|
351
|
+
return phases
|
|
352
|
+
.map((phase, index) => {
|
|
353
|
+
const id = normalizeOptionalString(phase?.id) || `frontier-task-${index + 1}`;
|
|
354
|
+
const status = id === activePhaseId ? "in_progress" : taskStatusFromFrontierStatus(phase?.status);
|
|
355
|
+
return {
|
|
356
|
+
id,
|
|
357
|
+
title:
|
|
358
|
+
normalizeOptionalString(phase?.label) ||
|
|
359
|
+
normalizeOptionalString(phase?.goal) ||
|
|
360
|
+
id,
|
|
361
|
+
status,
|
|
362
|
+
completed: status === "done",
|
|
363
|
+
};
|
|
364
|
+
})
|
|
365
|
+
.filter((task) => task.title);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function readProjectFrontierContext(projectRoot, cache) {
|
|
369
|
+
const normalizedRoot = normalizeOptionalString(projectRoot);
|
|
370
|
+
if (!normalizedRoot) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
if (cache.has(normalizedRoot)) {
|
|
374
|
+
return cache.get(normalizedRoot);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const frontierRoot = path.join(normalizedRoot, "orp", "frontier");
|
|
378
|
+
const state = readJsonIfExists(path.join(frontierRoot, "state.json"));
|
|
379
|
+
const stack = readJsonIfExists(path.join(frontierRoot, "version-stack.json"));
|
|
380
|
+
const tasText = readTextIfExists(path.join(frontierRoot, "TAS.md"));
|
|
381
|
+
const projectLink = readProjectLink(normalizedRoot, cache);
|
|
382
|
+
if (!state && !stack && !tasText && !projectLink) {
|
|
383
|
+
cache.set(normalizedRoot, null);
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const context = {
|
|
388
|
+
plan: buildFrontierPlan({ projectRoot: normalizedRoot, tasText, state, stack }),
|
|
389
|
+
tasks: stack ? buildFrontierTasks({ state, stack }) : [],
|
|
390
|
+
link: projectLink,
|
|
391
|
+
};
|
|
392
|
+
cache.set(normalizedRoot, context);
|
|
393
|
+
return context;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function enrichWorkspaceTabsWithProjectContext(tabs = []) {
|
|
397
|
+
const projectContextCache = new Map();
|
|
398
|
+
return tabs.map((tab) => {
|
|
399
|
+
const projectRoot = normalizeOptionalString(tab?.path ?? tab?.project_root ?? tab?.projectRoot);
|
|
400
|
+
const projectContext = readProjectFrontierContext(projectRoot, projectContextCache);
|
|
401
|
+
if (!projectContext) {
|
|
402
|
+
return tab;
|
|
403
|
+
}
|
|
404
|
+
const plan =
|
|
405
|
+
tab?.plan && typeof tab.plan === "object" && !Array.isArray(tab.plan)
|
|
406
|
+
? tab.plan
|
|
407
|
+
: projectContext.plan || undefined;
|
|
408
|
+
const tasks =
|
|
409
|
+
Array.isArray(tab?.tasks) && tab.tasks.length > 0
|
|
410
|
+
? tab.tasks
|
|
411
|
+
: Array.isArray(projectContext.tasks) && projectContext.tasks.length > 0
|
|
412
|
+
? projectContext.tasks
|
|
413
|
+
: undefined;
|
|
414
|
+
|
|
415
|
+
return Object.fromEntries(
|
|
416
|
+
Object.entries({
|
|
417
|
+
...tab,
|
|
418
|
+
linkedIdeaId: tab?.linkedIdeaId ?? tab?.linked_idea_id ?? projectContext.link?.ideaId,
|
|
419
|
+
linkedFeatureId: tab?.linkedFeatureId ?? tab?.linked_feature_id ?? projectContext.link?.activeFeatureId,
|
|
420
|
+
plan,
|
|
421
|
+
tasks,
|
|
422
|
+
}).filter(([, value]) => value !== undefined && value !== null),
|
|
423
|
+
);
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function enrichWorkspaceManifestWithProjectContext(manifest) {
|
|
428
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
429
|
+
return manifest;
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
...manifest,
|
|
433
|
+
tabs: enrichWorkspaceTabsWithProjectContext(Array.isArray(manifest.tabs) ? manifest.tabs : []),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
153
437
|
export function buildHostedWorkspaceState(manifest, options = {}) {
|
|
154
438
|
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
155
439
|
throw new Error("workspace manifest is required to build a hosted workspace state payload");
|
|
@@ -166,12 +450,14 @@ export function buildHostedWorkspaceState(manifest, options = {}) {
|
|
|
166
450
|
const capturedAt = normalizeOptionalString(options.capturedAt) || new Date().toISOString();
|
|
167
451
|
const updatedAt = normalizeOptionalString(options.updatedAt) || capturedAt;
|
|
168
452
|
const previousTabs = normalizePreviousHostedTabs(previousWorkspace);
|
|
453
|
+
const projectContextCache = new Map();
|
|
169
454
|
|
|
170
455
|
const tabs = manifest.tabs.map((tab, index) => {
|
|
171
456
|
const previous = matchPreviousHostedTab(tab, previousTabs);
|
|
172
457
|
const title = normalizeOptionalString(tab.title) || previous?.title || null;
|
|
173
458
|
const projectRoot = normalizeOptionalString(tab.path);
|
|
174
|
-
const
|
|
459
|
+
const projectContext = readProjectFrontierContext(projectRoot, projectContextCache);
|
|
460
|
+
const repoLabel = title || previous?.repoLabel || path.basename(String(projectRoot).replace(/\/+$/, "")) || projectRoot;
|
|
175
461
|
const terminalTitle = previous?.terminalTitle || title || repoLabel;
|
|
176
462
|
const resume = resolveResumeMetadata({
|
|
177
463
|
resumeCommand: tab.resumeCommand,
|
|
@@ -198,6 +484,27 @@ export function buildHostedWorkspaceState(manifest, options = {}) {
|
|
|
198
484
|
title,
|
|
199
485
|
orderIndex: index,
|
|
200
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;
|
|
201
508
|
|
|
202
509
|
return Object.fromEntries(
|
|
203
510
|
Object.entries({
|
|
@@ -219,9 +526,21 @@ export function buildHostedWorkspaceState(manifest, options = {}) {
|
|
|
219
526
|
current_task: previous?.currentTask || undefined,
|
|
220
527
|
focus_summary: previous?.focusSummary || undefined,
|
|
221
528
|
trajectory_summary: previous?.trajectorySummary || undefined,
|
|
222
|
-
last_activity_at_utc:
|
|
223
|
-
|
|
224
|
-
|
|
529
|
+
last_activity_at_utc: lastActivityAt,
|
|
530
|
+
last_synced_at_utc: lastSyncedAt,
|
|
531
|
+
sync_source: normalizeOptionalString(tab.syncSource ?? tab.sync_source) || undefined,
|
|
532
|
+
linked_feature_id:
|
|
533
|
+
normalizeOptionalString(tab.linkedFeatureId ?? tab.linked_feature_id) ||
|
|
534
|
+
projectContext?.link?.activeFeatureId ||
|
|
535
|
+
previous?.linkedFeatureId ||
|
|
536
|
+
undefined,
|
|
537
|
+
linked_idea_id:
|
|
538
|
+
normalizeOptionalString(tab.linkedIdeaId ?? tab.linked_idea_id) ||
|
|
539
|
+
projectContext?.link?.ideaId ||
|
|
540
|
+
previous?.linkedIdeaId ||
|
|
541
|
+
undefined,
|
|
542
|
+
plan,
|
|
543
|
+
tasks,
|
|
225
544
|
}).filter(([, value]) => value !== undefined && value !== null),
|
|
226
545
|
);
|
|
227
546
|
});
|
|
@@ -239,6 +558,10 @@ export function buildHostedWorkspaceState(manifest, options = {}) {
|
|
|
239
558
|
durable_backend: options.durableBackend || "manual-ledger",
|
|
240
559
|
}).filter(([, value]) => value !== undefined && value !== null),
|
|
241
560
|
);
|
|
561
|
+
const sourceContract =
|
|
562
|
+
options.localInventory?.contract && typeof options.localInventory.contract === "object" && !Array.isArray(options.localInventory.contract)
|
|
563
|
+
? options.localInventory.contract
|
|
564
|
+
: undefined;
|
|
242
565
|
|
|
243
566
|
const stateVersion = Math.max(1, (getHostedIntegerValue(previousState, "state_version", "stateVersion") || 0) + 1);
|
|
244
567
|
const snapshotSeed = JSON.stringify({
|
|
@@ -266,6 +589,7 @@ export function buildHostedWorkspaceState(manifest, options = {}) {
|
|
|
266
589
|
tab_count: tabs.length,
|
|
267
590
|
project_count: projects.length,
|
|
268
591
|
capture_context: Object.keys(captureContext).length > 0 ? captureContext : undefined,
|
|
592
|
+
source_contract: sourceContract,
|
|
269
593
|
projects,
|
|
270
594
|
tabs,
|
|
271
595
|
}).filter(([, value]) => value !== undefined && value !== null),
|
|
@@ -41,7 +41,11 @@ export {
|
|
|
41
41
|
runWorkspaceAddTab,
|
|
42
42
|
runWorkspaceRemoveTab,
|
|
43
43
|
} from "./ledger.js";
|
|
44
|
-
export {
|
|
44
|
+
export {
|
|
45
|
+
buildHostedWorkspaceState,
|
|
46
|
+
enrichWorkspaceManifestWithProjectContext,
|
|
47
|
+
enrichWorkspaceTabsWithProjectContext,
|
|
48
|
+
} from "./hosted-state.js";
|
|
45
49
|
export {
|
|
46
50
|
applyWorkspaceSlotsToInventory,
|
|
47
51
|
buildWorkspaceInventory,
|
|
@@ -59,6 +63,7 @@ export {
|
|
|
59
63
|
fetchIdeaPayload,
|
|
60
64
|
fetchIdeasPayload,
|
|
61
65
|
fetchHostedWorkspacesPayload,
|
|
66
|
+
findHostedWorkspaceByWorkspaceId,
|
|
62
67
|
findHostedWorkspaceByLinkedIdea,
|
|
63
68
|
findHostedWorkspaceLinkedToIdea,
|
|
64
69
|
loadWorkspaceSource,
|
|
@@ -68,6 +73,11 @@ export {
|
|
|
68
73
|
resolveWorkspaceSelectorFromCollections,
|
|
69
74
|
updateIdeaPayload,
|
|
70
75
|
} from "./orp.js";
|
|
76
|
+
export {
|
|
77
|
+
buildLocalProjectInventory,
|
|
78
|
+
inferLocalProjectRoots,
|
|
79
|
+
mergeLocalProjectInventoryIntoManifest,
|
|
80
|
+
} from "./local-inventory.js";
|
|
71
81
|
export {
|
|
72
82
|
cacheManagedWorkspaceManifest,
|
|
73
83
|
clearWorkspaceSlot,
|