@openprd/cli 0.1.18 → 0.1.19
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/.openprd/changes/openprd-generated-change/.openprd.yaml +2 -0
- package/.openprd/changes/openprd-generated-change/design.md +50 -0
- package/.openprd/changes/openprd-generated-change/proposal.md +37 -0
- package/.openprd/changes/openprd-generated-change/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/openprd-generated-change/task-events.jsonl +21 -0
- package/.openprd/changes/openprd-generated-change/tasks.md +357 -0
- package/.openprd/engagements/active/prd.md +99 -77
- package/README.md +20 -0
- package/README_EN.md +25 -0
- package/package.json +1 -1
- package/src/agent-canonical-content.js +2 -0
- package/src/canvas-app-client-app.js +838 -0
- package/src/canvas-app-client-helpers.js +1138 -0
- package/src/canvas-app-styles.js +898 -0
- package/src/canvas-app.html.js +6 -2781
- package/src/canvas-binding.js +414 -0
- package/src/canvas-codex-relay.js +430 -0
- package/src/canvas-i18n.js +8 -0
- package/src/canvas-scene.js +377 -0
- package/src/canvas-session-store.js +148 -0
- package/src/canvas-workspace.js +59 -1066
- package/src/codex-hook-runner-template.mjs +22 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Core purpose
|
|
3
|
+
* Resolve which conversation (thread/session/workspace) a canvas belongs to
|
|
4
|
+
* and materialize the file-backed session directory for that binding.
|
|
5
|
+
*
|
|
6
|
+
* Inputs
|
|
7
|
+
* Project root plus CLI/env binding hints (--thread/--session/--workspace),
|
|
8
|
+
* with fallback to .openprd/harness/runtime-environment.json evidence.
|
|
9
|
+
*
|
|
10
|
+
* Outputs
|
|
11
|
+
* ensureCanvasSession() creates .openprd/harness/canvas-sessions/<key>/ with
|
|
12
|
+
* session.json, scene.json, ops.json, and library.json scaffolding.
|
|
13
|
+
*
|
|
14
|
+
* Positioning
|
|
15
|
+
* Used by canvas-workspace.js; keep it transport-agnostic so Codex, Claude
|
|
16
|
+
* Code, Cursor, and future clients can all bind by providing evidence.
|
|
17
|
+
*/
|
|
18
|
+
import fs from 'node:fs/promises';
|
|
19
|
+
import { cjoin, exists, readJson, writeJson } from './fs-utils.js';
|
|
20
|
+
import { timestamp } from './time.js';
|
|
21
|
+
import {
|
|
22
|
+
CANVAS_SURFACE_BACKGROUND,
|
|
23
|
+
buildSessionKey,
|
|
24
|
+
firstPresent,
|
|
25
|
+
isPlainObject,
|
|
26
|
+
normalizeBindingTitle,
|
|
27
|
+
} from './canvas-session-store.js';
|
|
28
|
+
|
|
29
|
+
const CANVAS_SCHEMA_VERSION = 1;
|
|
30
|
+
|
|
31
|
+
async function readRuntimeEnvironment(projectRoot) {
|
|
32
|
+
const runtimePath = cjoin(projectRoot, '.openprd', 'harness', 'runtime-environment.json');
|
|
33
|
+
if (!(await exists(runtimePath))) {
|
|
34
|
+
return { path: runtimePath, data: null };
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
return { path: runtimePath, data: await readJson(runtimePath) };
|
|
38
|
+
} catch {
|
|
39
|
+
return { path: runtimePath, data: null };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function flattenRuntimeEvidence(runtimeEnvironment) {
|
|
44
|
+
const runtime = runtimeEnvironment ?? {};
|
|
45
|
+
const active = runtime.active ?? runtime.activeClient ?? runtime.current ?? {};
|
|
46
|
+
const detection = runtime.runtimeDetection ?? {};
|
|
47
|
+
const detectionActive = detection.active ?? detection.current ?? {};
|
|
48
|
+
const session = runtime.session ?? runtime.conversation ?? detection.session ?? detectionActive.session ?? {};
|
|
49
|
+
const turn = runtime.turn ?? detection.turn ?? detectionActive.turn ?? {};
|
|
50
|
+
const evidence = runtime.evidence ?? detection.evidence ?? detectionActive.evidence ?? {};
|
|
51
|
+
const observed = runtime.observedCapabilities ?? detection.observedCapabilities ?? detectionActive.observedCapabilities ?? [];
|
|
52
|
+
return {
|
|
53
|
+
active,
|
|
54
|
+
detection,
|
|
55
|
+
detectionActive,
|
|
56
|
+
session,
|
|
57
|
+
turn,
|
|
58
|
+
evidence,
|
|
59
|
+
observed,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function selectRuntimeBinding(runtimeEnvironment) {
|
|
64
|
+
if (!isPlainObject(runtimeEnvironment)) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const parts = flattenRuntimeEvidence(runtimeEnvironment);
|
|
68
|
+
const threadId = firstPresent(
|
|
69
|
+
parts.active.threadId,
|
|
70
|
+
parts.active.thread_id,
|
|
71
|
+
parts.active.conversationId,
|
|
72
|
+
parts.active.conversation_id,
|
|
73
|
+
parts.detectionActive.threadId,
|
|
74
|
+
parts.detectionActive.thread_id,
|
|
75
|
+
parts.detectionActive.conversationId,
|
|
76
|
+
parts.detectionActive.conversation_id,
|
|
77
|
+
parts.session.threadId,
|
|
78
|
+
parts.session.thread_id,
|
|
79
|
+
parts.session.conversationId,
|
|
80
|
+
parts.session.conversation_id,
|
|
81
|
+
parts.turn.threadId,
|
|
82
|
+
parts.turn.thread_id,
|
|
83
|
+
parts.evidence.threadId,
|
|
84
|
+
parts.evidence.thread_id,
|
|
85
|
+
);
|
|
86
|
+
if (threadId) {
|
|
87
|
+
const title = normalizeBindingTitle(
|
|
88
|
+
parts.active.threadTitle,
|
|
89
|
+
parts.active.thread_title,
|
|
90
|
+
parts.active.conversationTitle,
|
|
91
|
+
parts.active.conversation_title,
|
|
92
|
+
parts.active.title,
|
|
93
|
+
parts.active.name,
|
|
94
|
+
parts.detectionActive.threadTitle,
|
|
95
|
+
parts.detectionActive.thread_title,
|
|
96
|
+
parts.detectionActive.conversationTitle,
|
|
97
|
+
parts.detectionActive.conversation_title,
|
|
98
|
+
parts.detectionActive.title,
|
|
99
|
+
parts.detectionActive.name,
|
|
100
|
+
parts.session.threadTitle,
|
|
101
|
+
parts.session.thread_title,
|
|
102
|
+
parts.session.conversationTitle,
|
|
103
|
+
parts.session.conversation_title,
|
|
104
|
+
parts.session.title,
|
|
105
|
+
parts.session.name,
|
|
106
|
+
parts.turn.threadTitle,
|
|
107
|
+
parts.turn.thread_title,
|
|
108
|
+
parts.turn.conversationTitle,
|
|
109
|
+
parts.turn.conversation_title,
|
|
110
|
+
parts.evidence.threadTitle,
|
|
111
|
+
parts.evidence.thread_title,
|
|
112
|
+
parts.evidence.conversationTitle,
|
|
113
|
+
parts.evidence.conversation_title,
|
|
114
|
+
);
|
|
115
|
+
return {
|
|
116
|
+
kind: 'thread',
|
|
117
|
+
id: threadId,
|
|
118
|
+
title,
|
|
119
|
+
threadTitle: title,
|
|
120
|
+
source: 'runtime-environment:thread',
|
|
121
|
+
activeClient: firstPresent(parts.active.client, parts.active.activeClient, parts.detectionActive.activeClient, parts.detection.activeClient),
|
|
122
|
+
surface: firstPresent(parts.active.surface, parts.detectionActive.surface, parts.detection.surface),
|
|
123
|
+
confidence: firstPresent(parts.active.confidence, parts.detectionActive.confidence, parts.detection.confidence),
|
|
124
|
+
observedCapabilities: parts.observed,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const sessionId = firstPresent(
|
|
129
|
+
parts.active.sessionId,
|
|
130
|
+
parts.active.session_id,
|
|
131
|
+
parts.detectionActive.sessionId,
|
|
132
|
+
parts.detectionActive.session_id,
|
|
133
|
+
parts.session.id,
|
|
134
|
+
parts.session.sessionId,
|
|
135
|
+
parts.session.session_id,
|
|
136
|
+
parts.turn.sessionId,
|
|
137
|
+
parts.turn.session_id,
|
|
138
|
+
parts.evidence.sessionId,
|
|
139
|
+
parts.evidence.session_id,
|
|
140
|
+
);
|
|
141
|
+
if (sessionId) {
|
|
142
|
+
const title = normalizeBindingTitle(
|
|
143
|
+
parts.active.sessionTitle,
|
|
144
|
+
parts.active.session_title,
|
|
145
|
+
parts.active.conversationTitle,
|
|
146
|
+
parts.active.conversation_title,
|
|
147
|
+
parts.active.title,
|
|
148
|
+
parts.active.name,
|
|
149
|
+
parts.detectionActive.sessionTitle,
|
|
150
|
+
parts.detectionActive.session_title,
|
|
151
|
+
parts.detectionActive.conversationTitle,
|
|
152
|
+
parts.detectionActive.conversation_title,
|
|
153
|
+
parts.detectionActive.title,
|
|
154
|
+
parts.detectionActive.name,
|
|
155
|
+
parts.session.sessionTitle,
|
|
156
|
+
parts.session.session_title,
|
|
157
|
+
parts.session.conversationTitle,
|
|
158
|
+
parts.session.conversation_title,
|
|
159
|
+
parts.session.title,
|
|
160
|
+
parts.session.name,
|
|
161
|
+
parts.turn.sessionTitle,
|
|
162
|
+
parts.turn.session_title,
|
|
163
|
+
parts.turn.conversationTitle,
|
|
164
|
+
parts.turn.conversation_title,
|
|
165
|
+
parts.evidence.sessionTitle,
|
|
166
|
+
parts.evidence.session_title,
|
|
167
|
+
parts.evidence.conversationTitle,
|
|
168
|
+
parts.evidence.conversation_title,
|
|
169
|
+
);
|
|
170
|
+
return {
|
|
171
|
+
kind: 'session',
|
|
172
|
+
id: sessionId,
|
|
173
|
+
title,
|
|
174
|
+
sessionTitle: title,
|
|
175
|
+
source: 'runtime-environment:session',
|
|
176
|
+
activeClient: firstPresent(parts.active.client, parts.active.activeClient, parts.detectionActive.activeClient, parts.detection.activeClient),
|
|
177
|
+
surface: firstPresent(parts.active.surface, parts.detectionActive.surface, parts.detection.surface),
|
|
178
|
+
confidence: firstPresent(parts.active.confidence, parts.detectionActive.confidence, parts.detection.confidence),
|
|
179
|
+
observedCapabilities: parts.observed,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function resolveCanvasSessionBinding(projectRoot, options = {}, env = process.env) {
|
|
187
|
+
const explicitThread = firstPresent(options.thread, options.conversation, options.conversationId);
|
|
188
|
+
if (explicitThread) {
|
|
189
|
+
const title = normalizeBindingTitle(options.threadTitle, options.conversationTitle, options.title);
|
|
190
|
+
return {
|
|
191
|
+
kind: 'thread',
|
|
192
|
+
id: explicitThread,
|
|
193
|
+
displayId: explicitThread,
|
|
194
|
+
title,
|
|
195
|
+
threadTitle: title,
|
|
196
|
+
source: 'flag:thread',
|
|
197
|
+
confidence: 'explicit',
|
|
198
|
+
sessionKey: buildSessionKey('thread', explicitThread),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const explicitSession = firstPresent(options.session, options.sessionId);
|
|
203
|
+
if (explicitSession) {
|
|
204
|
+
const title = normalizeBindingTitle(options.sessionTitle, options.conversationTitle, options.title);
|
|
205
|
+
return {
|
|
206
|
+
kind: 'session',
|
|
207
|
+
id: explicitSession,
|
|
208
|
+
displayId: explicitSession,
|
|
209
|
+
title,
|
|
210
|
+
sessionTitle: title,
|
|
211
|
+
source: 'flag:session',
|
|
212
|
+
confidence: 'explicit',
|
|
213
|
+
sessionKey: buildSessionKey('session', explicitSession),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const envThread = firstPresent(
|
|
218
|
+
env.OPENPRD_THREAD_ID,
|
|
219
|
+
env.OPENPRD_CONVERSATION_ID,
|
|
220
|
+
env.CODEX_THREAD_ID,
|
|
221
|
+
env.CODEX_CONVERSATION_ID,
|
|
222
|
+
env.CLAUDE_THREAD_ID,
|
|
223
|
+
env.CURSOR_THREAD_ID,
|
|
224
|
+
);
|
|
225
|
+
if (envThread) {
|
|
226
|
+
const title = normalizeBindingTitle(
|
|
227
|
+
env.OPENPRD_THREAD_TITLE,
|
|
228
|
+
env.OPENPRD_CONVERSATION_TITLE,
|
|
229
|
+
env.CODEX_THREAD_TITLE,
|
|
230
|
+
env.CODEX_CONVERSATION_TITLE,
|
|
231
|
+
env.CLAUDE_THREAD_TITLE,
|
|
232
|
+
env.CLAUDE_CONVERSATION_TITLE,
|
|
233
|
+
env.CURSOR_THREAD_TITLE,
|
|
234
|
+
env.CURSOR_CONVERSATION_TITLE,
|
|
235
|
+
);
|
|
236
|
+
return {
|
|
237
|
+
kind: 'thread',
|
|
238
|
+
id: envThread,
|
|
239
|
+
displayId: envThread,
|
|
240
|
+
title,
|
|
241
|
+
threadTitle: title,
|
|
242
|
+
source: 'env:thread',
|
|
243
|
+
confidence: 'high',
|
|
244
|
+
sessionKey: buildSessionKey('thread', envThread),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const envSession = firstPresent(
|
|
249
|
+
env.OPENPRD_SESSION_ID,
|
|
250
|
+
env.CODEX_SESSION_ID,
|
|
251
|
+
env.CLAUDE_SESSION_ID,
|
|
252
|
+
env.CLAUDE_CODE_SESSION_ID,
|
|
253
|
+
env.CLAUDE_CODE_CHILD_SESSION,
|
|
254
|
+
env.CURSOR_SESSION_ID,
|
|
255
|
+
);
|
|
256
|
+
if (envSession) {
|
|
257
|
+
const title = normalizeBindingTitle(
|
|
258
|
+
env.OPENPRD_SESSION_TITLE,
|
|
259
|
+
env.OPENPRD_CONVERSATION_TITLE,
|
|
260
|
+
env.CODEX_SESSION_TITLE,
|
|
261
|
+
env.CODEX_CONVERSATION_TITLE,
|
|
262
|
+
env.CLAUDE_SESSION_TITLE,
|
|
263
|
+
env.CLAUDE_CONVERSATION_TITLE,
|
|
264
|
+
env.CURSOR_SESSION_TITLE,
|
|
265
|
+
env.CURSOR_CONVERSATION_TITLE,
|
|
266
|
+
);
|
|
267
|
+
return {
|
|
268
|
+
kind: 'session',
|
|
269
|
+
id: envSession,
|
|
270
|
+
displayId: envSession,
|
|
271
|
+
title,
|
|
272
|
+
sessionTitle: title,
|
|
273
|
+
source: 'env:session',
|
|
274
|
+
confidence: 'medium-high',
|
|
275
|
+
sessionKey: buildSessionKey('session', envSession),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const runtime = await readRuntimeEnvironment(projectRoot);
|
|
280
|
+
const runtimeBinding = selectRuntimeBinding(runtime.data);
|
|
281
|
+
if (runtimeBinding) {
|
|
282
|
+
const binding = {
|
|
283
|
+
...runtimeBinding,
|
|
284
|
+
displayId: runtimeBinding.id,
|
|
285
|
+
runtimePath: runtime.path,
|
|
286
|
+
sessionKey: buildSessionKey(runtimeBinding.kind, runtimeBinding.id),
|
|
287
|
+
};
|
|
288
|
+
const title = normalizeBindingTitle(runtimeBinding.title, runtimeBinding.threadTitle, runtimeBinding.sessionTitle, runtimeBinding.name);
|
|
289
|
+
if (title) {
|
|
290
|
+
binding.title = title;
|
|
291
|
+
if (binding.kind === 'thread') {
|
|
292
|
+
binding.threadTitle = title;
|
|
293
|
+
}
|
|
294
|
+
if (binding.kind === 'session') {
|
|
295
|
+
binding.sessionTitle = title;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return binding;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const workspaceId = shortHash(projectRoot);
|
|
302
|
+
return {
|
|
303
|
+
kind: 'workspace',
|
|
304
|
+
id: workspaceId,
|
|
305
|
+
displayId: `workspace-${workspaceId}`,
|
|
306
|
+
source: 'workspace-fallback',
|
|
307
|
+
confidence: 'low',
|
|
308
|
+
sessionKey: buildSessionKey('workspace', workspaceId),
|
|
309
|
+
note: 'No current conversation evidence was found; pass --thread or --session to avoid sharing a fallback canvas.',
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function buildCanvasSessionPaths(projectRoot, binding) {
|
|
314
|
+
const sessionDir = cjoin(projectRoot, '.openprd', 'harness', 'canvas-sessions', binding.sessionKey);
|
|
315
|
+
return {
|
|
316
|
+
sessionDir,
|
|
317
|
+
sessionFile: cjoin(sessionDir, 'session.json'),
|
|
318
|
+
sceneFile: cjoin(sessionDir, 'scene.json'),
|
|
319
|
+
libraryFile: cjoin(sessionDir, 'library.json'),
|
|
320
|
+
opsFile: cjoin(sessionDir, 'ops.json'),
|
|
321
|
+
serverFile: cjoin(sessionDir, 'server.json'),
|
|
322
|
+
logFile: cjoin(sessionDir, 'server.log'),
|
|
323
|
+
assetsDir: cjoin(sessionDir, 'assets'),
|
|
324
|
+
exportsDir: cjoin(sessionDir, 'exports'),
|
|
325
|
+
relaysDir: cjoin(sessionDir, 'relays'),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function preserveCanvasBindingDisplayTitle(binding, existingBinding) {
|
|
330
|
+
if (!isPlainObject(binding) || !isPlainObject(existingBinding)) {
|
|
331
|
+
return binding;
|
|
332
|
+
}
|
|
333
|
+
if (binding.kind !== existingBinding.kind || binding.id !== existingBinding.id) {
|
|
334
|
+
return binding;
|
|
335
|
+
}
|
|
336
|
+
const title = normalizeBindingTitle(
|
|
337
|
+
binding.title,
|
|
338
|
+
binding.threadTitle,
|
|
339
|
+
binding.sessionTitle,
|
|
340
|
+
binding.name,
|
|
341
|
+
existingBinding.title,
|
|
342
|
+
existingBinding.threadTitle,
|
|
343
|
+
existingBinding.sessionTitle,
|
|
344
|
+
existingBinding.name,
|
|
345
|
+
);
|
|
346
|
+
if (!title) {
|
|
347
|
+
return binding;
|
|
348
|
+
}
|
|
349
|
+
const nextBinding = { ...binding, title };
|
|
350
|
+
if (nextBinding.kind === 'thread') {
|
|
351
|
+
nextBinding.threadTitle = title;
|
|
352
|
+
}
|
|
353
|
+
if (nextBinding.kind === 'session') {
|
|
354
|
+
nextBinding.sessionTitle = title;
|
|
355
|
+
}
|
|
356
|
+
return nextBinding;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function ensureCanvasSession(projectRoot, options = {}, env = process.env) {
|
|
360
|
+
const binding = await resolveCanvasSessionBinding(projectRoot, options, env);
|
|
361
|
+
const paths = buildCanvasSessionPaths(projectRoot, binding);
|
|
362
|
+
await fs.mkdir(paths.assetsDir, { recursive: true });
|
|
363
|
+
await fs.mkdir(paths.exportsDir, { recursive: true });
|
|
364
|
+
await fs.mkdir(paths.relaysDir, { recursive: true });
|
|
365
|
+
const existing = await readJson(paths.sessionFile).catch(() => null);
|
|
366
|
+
const resolvedBinding = preserveCanvasBindingDisplayTitle(binding, existing?.binding);
|
|
367
|
+
const now = timestamp();
|
|
368
|
+
const session = {
|
|
369
|
+
schema: `openprd.canvas.session.v${CANVAS_SCHEMA_VERSION}`,
|
|
370
|
+
version: CANVAS_SCHEMA_VERSION,
|
|
371
|
+
sessionKey: resolvedBinding.sessionKey,
|
|
372
|
+
projectRoot,
|
|
373
|
+
binding: resolvedBinding,
|
|
374
|
+
paths,
|
|
375
|
+
createdAt: existing?.createdAt ?? now,
|
|
376
|
+
updatedAt: now,
|
|
377
|
+
};
|
|
378
|
+
await writeJson(paths.sessionFile, session);
|
|
379
|
+
if (!(await exists(paths.sceneFile))) {
|
|
380
|
+
await writeJson(paths.sceneFile, {
|
|
381
|
+
type: 'openprd.canvas.scene.v1',
|
|
382
|
+
elements: [],
|
|
383
|
+
appState: {
|
|
384
|
+
viewBackgroundColor: CANVAS_SURFACE_BACKGROUND,
|
|
385
|
+
},
|
|
386
|
+
files: {},
|
|
387
|
+
savedAt: now,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
if (!(await exists(paths.opsFile))) {
|
|
391
|
+
await writeJson(paths.opsFile, {
|
|
392
|
+
type: 'openprd.canvas.ops.v1',
|
|
393
|
+
nextSeq: 0,
|
|
394
|
+
ops: [],
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
if (!(await exists(paths.libraryFile))) {
|
|
398
|
+
await writeJson(paths.libraryFile, {
|
|
399
|
+
type: 'excalidrawlib',
|
|
400
|
+
version: 2,
|
|
401
|
+
source: 'https://openprd.local/canvas',
|
|
402
|
+
libraryItems: [],
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
return session;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export {
|
|
409
|
+
CANVAS_SCHEMA_VERSION,
|
|
410
|
+
buildCanvasSessionPaths,
|
|
411
|
+
ensureCanvasSession,
|
|
412
|
+
preserveCanvasBindingDisplayTitle,
|
|
413
|
+
resolveCanvasSessionBinding,
|
|
414
|
+
};
|