@openprd/cli 0.1.11 → 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.
Files changed (147) hide show
  1. package/.openprd/changes/adaptive-project-framing/.openprd.yaml +2 -0
  2. package/.openprd/changes/adaptive-project-framing/design.md +47 -0
  3. package/.openprd/changes/adaptive-project-framing/proposal.md +35 -0
  4. package/.openprd/changes/adaptive-project-framing/specs/agent-requirements/spec.md +16 -0
  5. package/.openprd/changes/adaptive-project-framing/task-events.jsonl +20 -0
  6. package/.openprd/changes/adaptive-project-framing/tasks.md +340 -0
  7. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/.openprd.yaml +2 -0
  8. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/design.md +34 -0
  9. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/proposal.md +23 -0
  10. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/specs/agent-requirements/spec.md +16 -0
  11. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/task-events.jsonl +14 -0
  12. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/tasks.md +238 -0
  13. package/.openprd/changes/loop-isolated-worktree-commit/.openprd.yaml +2 -0
  14. package/.openprd/changes/loop-isolated-worktree-commit/design.md +53 -0
  15. package/.openprd/changes/loop-isolated-worktree-commit/proposal.md +38 -0
  16. package/.openprd/changes/loop-isolated-worktree-commit/specs/agent-requirements/spec.md +16 -0
  17. package/.openprd/changes/loop-isolated-worktree-commit/task-events.jsonl +22 -0
  18. package/.openprd/changes/loop-isolated-worktree-commit/tasks.md +357 -0
  19. package/.openprd/changes/openprd/.openprd.yaml +2 -0
  20. package/.openprd/changes/openprd/design.md +46 -0
  21. package/.openprd/changes/openprd/proposal.md +36 -0
  22. package/.openprd/changes/openprd/specs/agent-requirements/spec.md +16 -0
  23. package/.openprd/changes/openprd/task-events.jsonl +23 -0
  24. package/.openprd/changes/openprd/tasks.md +242 -0
  25. package/.openprd/changes/openprd-generated-change/.openprd.yaml +2 -0
  26. package/.openprd/changes/openprd-generated-change/design.md +50 -0
  27. package/.openprd/changes/openprd-generated-change/proposal.md +37 -0
  28. package/.openprd/changes/openprd-generated-change/specs/agent-requirements/spec.md +16 -0
  29. package/.openprd/changes/openprd-generated-change/task-events.jsonl +21 -0
  30. package/.openprd/changes/openprd-generated-change/tasks.md +357 -0
  31. package/.openprd/changes/pm-human-in-the-loop/.openprd.yaml +2 -0
  32. package/.openprd/changes/pm-human-in-the-loop/design.md +38 -0
  33. package/.openprd/changes/pm-human-in-the-loop/proposal.md +29 -0
  34. package/.openprd/changes/pm-human-in-the-loop/specs/agent-requirements/spec.md +16 -0
  35. package/.openprd/changes/pm-human-in-the-loop/task-events.jsonl +18 -0
  36. package/.openprd/changes/pm-human-in-the-loop/tasks.md +306 -0
  37. package/.openprd/changes/test-strategy-router/.openprd.yaml +2 -0
  38. package/.openprd/changes/test-strategy-router/design.md +65 -0
  39. package/.openprd/changes/test-strategy-router/proposal.md +45 -0
  40. package/.openprd/changes/test-strategy-router/specs/agent-requirements/spec.md +16 -0
  41. package/.openprd/changes/test-strategy-router/task-events.jsonl +1 -0
  42. package/.openprd/changes/test-strategy-router/tasks.md +132 -0
  43. package/.openprd/changes/verify-boundaries-and-historical-refresh/.openprd.yaml +2 -0
  44. package/.openprd/changes/verify-boundaries-and-historical-refresh/design.md +28 -0
  45. package/.openprd/changes/verify-boundaries-and-historical-refresh/proposal.md +23 -0
  46. package/.openprd/changes/verify-boundaries-and-historical-refresh/specs/agent-requirements/spec.md +16 -0
  47. package/.openprd/changes/verify-boundaries-and-historical-refresh/task-events.jsonl +11 -0
  48. package/.openprd/changes/verify-boundaries-and-historical-refresh/tasks.md +66 -0
  49. package/.openprd/changes/vibe-coding-dev-check/.openprd.yaml +2 -0
  50. package/.openprd/changes/vibe-coding-dev-check/design.md +37 -0
  51. package/.openprd/changes/vibe-coding-dev-check/proposal.md +28 -0
  52. package/.openprd/changes/vibe-coding-dev-check/specs/agent-requirements/spec.md +16 -0
  53. package/.openprd/changes/vibe-coding-dev-check/task-events.jsonl +18 -0
  54. package/.openprd/changes/vibe-coding-dev-check/tasks.md +306 -0
  55. package/.openprd/design/README.md +11 -6
  56. package/.openprd/design/README_EN.md +4 -1
  57. package/.openprd/design/active/asset-spec.md +6 -0
  58. package/.openprd/design/active/direction-plan.md +5 -5
  59. package/.openprd/design/active/selected-direction.md +2 -0
  60. package/.openprd/design/anti-slop.md +26 -1
  61. package/.openprd/design/assets/surface-presets.md +6 -0
  62. package/.openprd/design/checklists/ui-quality-gate.md +7 -0
  63. package/.openprd/design/lenses/frontend-lenses.md +28 -0
  64. package/.openprd/design/recipes/content-experience.md +8 -1
  65. package/.openprd/design/recipes/operational-tool.md +7 -0
  66. package/.openprd/design/recipes/product-launch.md +8 -1
  67. package/.openprd/design/templates/README.md +6 -3
  68. package/.openprd/design/templates/README_EN.md +6 -3
  69. package/.openprd/design/templates/ops-dashboard.html +8 -1
  70. package/.openprd/design/templates/product-launch.html +2 -1
  71. package/.openprd/design/themes/theme-catalog.json +23 -5
  72. package/.openprd/engagements/active/prd.md +137 -112
  73. package/.openprd/standards/config.json +7 -1
  74. package/AGENTS.md +13 -3
  75. package/README.md +127 -14
  76. package/README_EN.md +156 -13
  77. package/package.json +1 -1
  78. package/scripts/dev-check-wrapup-copy.mjs +46 -21
  79. package/skills/openprd-diagram-review/SKILL.md +27 -2
  80. package/skills/openprd-diagram-review/references/explanation-svg-patterns.md +131 -0
  81. package/skills/openprd-discovery-loop/SKILL.md +2 -2
  82. package/skills/openprd-frontend-design/SKILL.md +44 -20
  83. package/skills/openprd-frontend-design/references/design-asset-contract.md +6 -0
  84. package/skills/openprd-frontend-design/references/direction-engine.md +29 -4
  85. package/skills/openprd-harness/SKILL.md +22 -16
  86. package/skills/openprd-quality/SKILL.md +10 -8
  87. package/skills/openprd-requirement-intake/SKILL.md +2 -0
  88. package/skills/openprd-router/SKILL.md +2 -1
  89. package/skills/openprd-shared/SKILL.md +26 -16
  90. package/skills/openprd-standards/SKILL.md +4 -2
  91. package/src/agent-canonical-content.js +810 -0
  92. package/src/agent-integration.js +258 -777
  93. package/src/canvas-app-client-app.js +838 -0
  94. package/src/canvas-app-client-helpers.js +1138 -0
  95. package/src/canvas-app-styles.js +898 -0
  96. package/src/canvas-app.html.js +135 -0
  97. package/src/canvas-binding.js +414 -0
  98. package/src/canvas-codex-relay.js +430 -0
  99. package/src/canvas-i18n.js +282 -0
  100. package/src/canvas-scene.js +377 -0
  101. package/src/canvas-session-store.js +148 -0
  102. package/src/canvas-workspace.js +1264 -0
  103. package/src/cli/args.js +72 -4
  104. package/src/cli/basic-print.js +3 -0
  105. package/src/cli/canvas-print.js +106 -0
  106. package/src/cli/doctor-print.js +20 -0
  107. package/src/cli/print.js +2 -0
  108. package/src/cli/quality-commands.js +222 -0
  109. package/src/cli/quality-print.js +28 -2
  110. package/src/cli/run-print.js +5 -0
  111. package/src/cli/workflow-print.js +3 -0
  112. package/src/codex-app-server-relay.js +251 -0
  113. package/src/codex-hook-runner-template.mjs +978 -58
  114. package/src/design-starter-support.js +2 -2
  115. package/src/design-starter.js +39 -3
  116. package/src/dev-standards.js +125 -10
  117. package/src/diagram-core.js +6 -20
  118. package/src/discovery.js +2 -8
  119. package/src/docs-compact.js +125 -0
  120. package/src/execution-strategy.js +1 -1
  121. package/src/fleet.js +9 -3
  122. package/src/guide.js +227 -0
  123. package/src/html-artifacts.js +428 -7
  124. package/src/knowledge.js +45 -11
  125. package/src/language-policy.js +63 -3
  126. package/src/learning-review.js +1 -1
  127. package/src/loop.js +1 -1
  128. package/src/openprd.js +68 -131
  129. package/src/openspec/change-validate.js +2 -2
  130. package/src/openspec/constants.js +6 -7
  131. package/src/openspec/execute.js +2 -2
  132. package/src/openspec/generate.js +318 -102
  133. package/src/openspec/migration.js +168 -0
  134. package/src/openspec/paths.js +1 -22
  135. package/src/prd-core.js +1 -1
  136. package/src/quality-visual-review.js +257 -14
  137. package/src/quality.js +46 -8
  138. package/src/review-presentation.js +60 -0
  139. package/src/run-harness.js +41 -3
  140. package/src/session-evidence.js +363 -0
  141. package/src/standards.js +64 -0
  142. package/src/test-strategy.js +7 -4
  143. package/src/visual-compare-centering.js +622 -0
  144. package/src/visual-compare-core.js +1123 -0
  145. package/src/visual-compare.js +620 -698
  146. package/src/workspace-core.js +94 -4
  147. package/src/workspace-workflow.js +46 -1
@@ -0,0 +1,135 @@
1
+ /*
2
+ * Core purpose
3
+ * Render the browser app used by `openprd canvas`.
4
+ *
5
+ * Inputs
6
+ * Receives a resolved canvas session and emits a single HTML document.
7
+ *
8
+ * Outputs
9
+ * A local Excalidraw-powered canvas shell that saves scenes to the OpenPrd
10
+ * canvas session service and applies agent-queued image/card operations.
11
+ *
12
+ * Positioning
13
+ * This file is presentation-only. Server routes, file storage, and session
14
+ * binding live in canvas-workspace.js.
15
+ *
16
+ * Maintenance rules
17
+ * Keep this bundle dependency-light. If the CDN embed is replaced with a
18
+ * packaged build later, keep the API contract with canvas-workspace.js stable.
19
+ */
20
+ import {
21
+ CANVAS_DEFAULT_LOCALE,
22
+ CANVAS_LOCALE_STORAGE_KEY,
23
+ CANVAS_LOCALES,
24
+ } from './canvas-i18n.js';
25
+ import { CANVAS_APP_STYLES } from './canvas-app-styles.js';
26
+ import { CANVAS_APP_CLIENT_HELPERS } from './canvas-app-client-helpers.js';
27
+ import { CANVAS_APP_CLIENT_APP } from './canvas-app-client-app.js';
28
+
29
+ function escapeHtml(value) {
30
+ return String(value ?? '')
31
+ .replaceAll('&', '&')
32
+ .replaceAll('<', '&lt;')
33
+ .replaceAll('>', '&gt;')
34
+ .replaceAll('"', '&quot;')
35
+ .replaceAll("'", '&#39;');
36
+ }
37
+
38
+ function serialize(value) {
39
+ return JSON.stringify(value ?? null).replaceAll('</script', '<\\/script');
40
+ }
41
+
42
+ function renderCanvasAppHtml(session) {
43
+ const bindingTitle = session.binding?.title
44
+ ?? session.binding?.threadTitle
45
+ ?? session.binding?.sessionTitle
46
+ ?? session.binding?.name
47
+ ?? session.binding?.displayId
48
+ ?? session.sessionKey;
49
+ const title = `OpenPrd Canvas · ${bindingTitle}`;
50
+ return `<!doctype html>
51
+ <html lang="zh-CN">
52
+ <head>
53
+ <meta charset="utf-8" />
54
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
55
+ <title>${escapeHtml(title)}</title>
56
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%232563eb'/%3E%3Cpath d='M9 10h14M9 16h14M9 22h9' stroke='white' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E" />
57
+ <script>
58
+ window.EXCALIDRAW_ASSET_PATH = "https://esm.sh/@excalidraw/excalidraw@0.18.0/dist/prod/";
59
+ </script>
60
+ <script type="importmap">
61
+ {
62
+ "imports": {
63
+ "react": "https://esm.sh/react@18.3.1",
64
+ "react/jsx-runtime": "https://esm.sh/react@18.3.1/jsx-runtime",
65
+ "react-dom": "https://esm.sh/react-dom@18.3.1",
66
+ "react-dom/client": "https://esm.sh/react-dom@18.3.1/client",
67
+ "@excalidraw/excalidraw": "https://esm.sh/@excalidraw/excalidraw@0.18.0/dist/dev/index.js?external=react,react-dom"
68
+ }
69
+ }
70
+ </script>
71
+ <link rel="stylesheet" href="https://esm.sh/@excalidraw/excalidraw@0.18.0/dist/dev/index.css" />
72
+ <style>
73
+ ${CANVAS_APP_STYLES}
74
+ </style>
75
+ </head>
76
+ <body>
77
+ <div id="root">
78
+ <div class="fallback">
79
+ <div>
80
+ <strong>正在打开 OpenPrd 画布</strong>
81
+ <span>画布会自动绑定当前对话记录。</span>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ <script id="openprd-session" type="application/json">${serialize(session)}</script>
86
+ <script id="openprd-canvas-locales" type="application/json">${serialize({
87
+ defaultLocale: CANVAS_DEFAULT_LOCALE,
88
+ storageKey: CANVAS_LOCALE_STORAGE_KEY,
89
+ messages: CANVAS_LOCALES,
90
+ })}</script>
91
+ <script>
92
+ function escapeBootText(value) {
93
+ return String(value ?? "")
94
+ .replaceAll("&", "&amp;")
95
+ .replaceAll("<", "&lt;")
96
+ .replaceAll(">", "&gt;")
97
+ .replaceAll('"', "&quot;")
98
+ .replaceAll("'", "&#39;");
99
+ }
100
+
101
+ window.__openprdCanvasBootError = function(error) {
102
+ const root = document.getElementById("root");
103
+ if (!root || root.querySelector(".shell, .excalidraw")) {
104
+ return;
105
+ }
106
+ const message = error && (error.stack || error.message || String(error)) || "unknown boot error";
107
+ root.innerHTML =
108
+ '<div class="fallback"><div><strong>画布启动失败</strong><span>请刷新页面;如果仍失败,把下面这段错误发给 Agent。</span><pre>' +
109
+ escapeBootText(message) +
110
+ "</pre></div></div>";
111
+ };
112
+
113
+ window.addEventListener("error", (event) => {
114
+ if (event.target && event.target !== window) {
115
+ const tagName = event.target.tagName;
116
+ if (tagName !== "SCRIPT" && tagName !== "LINK") {
117
+ return;
118
+ }
119
+ }
120
+ window.__openprdCanvasBootError(event.error || event.message);
121
+ }, true);
122
+
123
+ window.addEventListener("unhandledrejection", (event) => {
124
+ window.__openprdCanvasBootError(event.reason);
125
+ });
126
+ </script>
127
+ <script type="module">
128
+ ${CANVAS_APP_CLIENT_HELPERS}
129
+ ${CANVAS_APP_CLIENT_APP}
130
+ </script>
131
+ </body>
132
+ </html>`;
133
+ }
134
+
135
+ export { renderCanvasAppHtml };
@@ -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
+ };