@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,430 @@
1
+ /*
2
+ * Core purpose
3
+ * Deliver canvas handoffs back to a bound Codex thread: the direct
4
+ * app-server relay (thread/resume -> turn/start via a detached worker) and
5
+ * the agent-foreground-relay fallback outbox that in-conversation agents
6
+ * drain with codex_app.send_message_to_thread.
7
+ *
8
+ * Positioning
9
+ * Used by canvas-workspace.js when a handoff targets Codex and the session
10
+ * is thread-bound. Keep transports additive: skipping or failing a relay
11
+ * must never lose the handoff operation itself.
12
+ */
13
+ import fs from 'node:fs/promises';
14
+ import path from 'node:path';
15
+ import { spawn } from 'node:child_process';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { cjoin, exists, readJson, writeJson } from './fs-utils.js';
18
+ import { timestamp } from './time.js';
19
+ import {
20
+ appendOperation,
21
+ parsePositiveInteger,
22
+ readOps,
23
+ sanitizeSessionSegment,
24
+ trimString,
25
+ updateOperation,
26
+ } from './canvas-session-store.js';
27
+
28
+ const CODEX_APP_SERVER_RELAY_WORKER_PATH = cjoin(path.dirname(fileURLToPath(import.meta.url)), 'codex-app-server-relay.js');
29
+ const CODEX_APP_SERVER_RELAY_TIMEOUT_MS = 20 * 60 * 1000;
30
+ const CODEX_APP_SERVER_RELAY_START_WAIT_MS = 0;
31
+ const CODEX_APP_BUNDLE_BIN_PATH = '/Applications/Codex.app/Contents/Resources/codex';
32
+ const CODEX_DELEGATION_RELAY_TRANSPORT = 'codex-app-server-codex-delegation-turn-start';
33
+ const CODEX_AGENT_BRIDGE_TRANSPORT = 'codex-agent-send-message-to-thread';
34
+ const CODEX_RELAY_DELIVERED_STATUSES = new Set(['completed']);
35
+ const CODEX_RELAY_ACCEPTED_STATUSES = new Set([...CODEX_RELAY_DELIVERED_STATUSES, 'started']);
36
+ const CODEX_RELAY_PENDING_STATUSES = new Set(['queued', 'starting', 'resuming', 'connecting', 'started']);
37
+ const CODEX_RELAY_WAIT_READY_STATUSES = new Set([
38
+ ...CODEX_RELAY_ACCEPTED_STATUSES,
39
+ 'failed',
40
+ 'timeout-before-start',
41
+ 'timeout-after-start',
42
+ 'closed-before-start',
43
+ 'closed-after-start',
44
+ ]);
45
+ const CODEX_RELAY_AGENT_FALLBACK_STATUSES = new Set([
46
+ 'failed',
47
+ 'timeout-before-start',
48
+ 'closed-before-start',
49
+ ]);
50
+
51
+
52
+ function escapeCodexDelegationXml(value) {
53
+ return String(value ?? '')
54
+ .replace(/&/g, '&')
55
+ .replace(/</g, '&lt;')
56
+ .replace(/>/g, '&gt;');
57
+ }
58
+
59
+ function buildCodexDelegationText(sourceThreadId, inputText) {
60
+ const sourceId = trimString(sourceThreadId || 'openprd-canvas', 200);
61
+ const text = trimString(inputText || 'Please continue from this OpenPrd Canvas handoff.');
62
+ return [
63
+ '<codex_delegation>',
64
+ ` <source_thread_id>${escapeCodexDelegationXml(sourceId)}</source_thread_id>`,
65
+ ` <input>${escapeCodexDelegationXml(text)}</input>`,
66
+ '</codex_delegation>',
67
+ ].join('\n');
68
+ }
69
+
70
+ function relayTimeoutMs(env = process.env) {
71
+ return parsePositiveInteger(env.OPENPRD_CANVAS_CODEX_RELAY_TIMEOUT_MS, CODEX_APP_SERVER_RELAY_TIMEOUT_MS);
72
+ }
73
+
74
+ function relayStartWaitMs(options = {}, env = process.env) {
75
+ if (options.relayWaitMs != null) {
76
+ return parsePositiveInteger(options.relayWaitMs, 0);
77
+ }
78
+ return parsePositiveInteger(env.OPENPRD_CANVAS_CODEX_RELAY_START_WAIT_MS, CODEX_APP_SERVER_RELAY_START_WAIT_MS);
79
+ }
80
+
81
+ async function sleep(ms) {
82
+ await new Promise((resolve) => setTimeout(resolve, ms));
83
+ }
84
+
85
+ async function waitForCodexRelayStart(statusPath, waitMs) {
86
+ const timeoutMs = parsePositiveInteger(waitMs, 0);
87
+ if (!timeoutMs) {
88
+ return null;
89
+ }
90
+ const deadline = Date.now() + timeoutMs;
91
+ let latest = null;
92
+ while (Date.now() <= deadline) {
93
+ latest = await readJson(statusPath).catch(() => latest);
94
+ if (latest?.status && CODEX_RELAY_WAIT_READY_STATUSES.has(latest.status)) {
95
+ return latest;
96
+ }
97
+ await sleep(350);
98
+ }
99
+ latest = await readJson(statusPath).catch(() => latest);
100
+ return latest ? { ...latest, waitTimedOut: true } : {
101
+ status: 'queued',
102
+ waitTimedOut: true,
103
+ };
104
+ }
105
+
106
+ function relayStatusSummary(statusDoc) {
107
+ if (!statusDoc) {
108
+ return {};
109
+ }
110
+ const status = statusDoc.status ?? 'queued';
111
+ return {
112
+ status,
113
+ phase: statusDoc.phase ?? null,
114
+ turn: statusDoc.turn ?? null,
115
+ resolvedThread: statusDoc.resolvedThread ?? null,
116
+ error: statusDoc.error ?? null,
117
+ waitTimedOut: Boolean(statusDoc.waitTimedOut),
118
+ delivered: CODEX_RELAY_DELIVERED_STATUSES.has(status),
119
+ pending: Boolean(statusDoc.waitTimedOut) || CODEX_RELAY_PENDING_STATUSES.has(status),
120
+ accepted: CODEX_RELAY_ACCEPTED_STATUSES.has(status),
121
+ };
122
+ }
123
+
124
+ async function resolveCodexAppServerBin(env = process.env) {
125
+ if (env.OPENPRD_CODEX_APP_SERVER_BIN) {
126
+ return env.OPENPRD_CODEX_APP_SERVER_BIN;
127
+ }
128
+ if (process.platform === 'darwin' && await exists(CODEX_APP_BUNDLE_BIN_PATH)) {
129
+ return CODEX_APP_BUNDLE_BIN_PATH;
130
+ }
131
+ return 'codex';
132
+ }
133
+
134
+ function codexRelaySkipReason(session, op, options = {}, env = process.env) {
135
+ if (options.relay === false || options.disableRelay === true) {
136
+ return 'disabled-by-request';
137
+ }
138
+ if (env.OPENPRD_CANVAS_CODEX_RELAY === '0') {
139
+ return 'disabled-by-env';
140
+ }
141
+ if (String(op.target ?? '').toLowerCase() !== 'codex') {
142
+ return 'unsupported-target';
143
+ }
144
+ if (session.binding?.kind !== 'thread' || !session.binding?.id) {
145
+ return 'not-thread-bound';
146
+ }
147
+ return null;
148
+ }
149
+
150
+ function buildCodexRelayInput(session, op) {
151
+ const input = [];
152
+ const text = trimString(op.promptText || op.instruction || 'Please continue from this OpenPrd Canvas handoff.');
153
+ if (text.trim()) {
154
+ const sourceThreadId = session.binding?.id || op.binding?.id || op.threadId || 'openprd-canvas';
155
+ input.push({ type: 'text', text: buildCodexDelegationText(sourceThreadId, text) });
156
+ }
157
+ const exportPath = String(op.exportPath ?? '').trim();
158
+ if (exportPath) {
159
+ input.push({ type: 'localImage', path: exportPath });
160
+ }
161
+ return input;
162
+ }
163
+
164
+ function buildCodexAgentBridgePrompt(session, op) {
165
+ const text = trimString(op.promptText || op.instruction || 'Please continue from this OpenPrd Canvas handoff.');
166
+ const sourceThreadId = session.binding?.id || op.binding?.id || op.threadId || 'openprd-canvas';
167
+ return buildCodexDelegationText(sourceThreadId, text);
168
+ }
169
+
170
+ function summarizeAgentBridgeOperation(op) {
171
+ if (!op) {
172
+ return null;
173
+ }
174
+ return {
175
+ id: op.id,
176
+ seq: op.seq ?? null,
177
+ status: op.status ?? 'pending',
178
+ target: op.target ?? 'codex',
179
+ transport: op.transport ?? CODEX_AGENT_BRIDGE_TRANSPORT,
180
+ handoffOperationId: op.handoffOperationId ?? null,
181
+ threadId: op.threadId ?? null,
182
+ threadTitle: op.threadTitle ?? null,
183
+ prompt: op.prompt ?? null,
184
+ promptText: op.promptText ?? null,
185
+ exportPath: op.exportPath ?? null,
186
+ exportUrl: op.exportUrl ?? null,
187
+ selection: op.selection ?? null,
188
+ placeholderId: op.placeholderId ?? null,
189
+ elementCount: op.elementCount ?? null,
190
+ selectedElementIds: Array.isArray(op.selectedElementIds) ? op.selectedElementIds : [],
191
+ createdAt: op.createdAt ?? null,
192
+ updatedAt: op.updatedAt ?? null,
193
+ claimedAt: op.claimedAt ?? null,
194
+ sentAt: op.sentAt ?? null,
195
+ failedAt: op.failedAt ?? null,
196
+ reason: op.reason ?? null,
197
+ requiresAgentTool: op.requiresAgentTool ?? 'codex_app.send_message_to_thread',
198
+ };
199
+ }
200
+
201
+ function codexRelayNeedsAgentBridgeFallback(relay) {
202
+ if (!relay || relay.status === 'skipped') {
203
+ return true;
204
+ }
205
+ const status = String(relay.status ?? '');
206
+ if (CODEX_RELAY_AGENT_FALLBACK_STATUSES.has(status)) {
207
+ return true;
208
+ }
209
+ return Boolean(relay.waitTimedOut && ['queued', 'starting', 'resuming'].includes(status));
210
+ }
211
+
212
+ async function queueCodexAgentBridge(session, op, options = {}) {
213
+ const requested = Boolean(options.agentBridge || options.foregroundBridge);
214
+ const base = {
215
+ target: op.target ?? 'codex',
216
+ transport: CODEX_AGENT_BRIDGE_TRANSPORT,
217
+ supportsForegroundMessage: false,
218
+ requiresAgentTool: 'codex_app.send_message_to_thread',
219
+ };
220
+ if (options.agentBridgeSkipReason) {
221
+ return {
222
+ ...base,
223
+ status: 'skipped',
224
+ reason: options.agentBridgeSkipReason,
225
+ directRelayStatus: options.directRelay?.status ?? null,
226
+ directRelayTransport: options.directRelay?.transport ?? CODEX_DELEGATION_RELAY_TRANSPORT,
227
+ };
228
+ }
229
+ if (!requested) {
230
+ return {
231
+ ...base,
232
+ status: 'skipped',
233
+ reason: 'not-requested',
234
+ };
235
+ }
236
+ if (String(op.target ?? '').toLowerCase() !== 'codex') {
237
+ return {
238
+ ...base,
239
+ status: 'skipped',
240
+ reason: 'unsupported-target',
241
+ };
242
+ }
243
+ if (session.binding?.kind !== 'thread' || !session.binding?.id) {
244
+ return {
245
+ ...base,
246
+ status: 'skipped',
247
+ reason: 'not-thread-bound',
248
+ };
249
+ }
250
+ const bridgeOp = await appendOperation(session.paths, {
251
+ type: 'agent-foreground-relay',
252
+ target: 'codex',
253
+ transport: CODEX_AGENT_BRIDGE_TRANSPORT,
254
+ status: 'pending',
255
+ handoffOperationId: op.id,
256
+ threadId: session.binding.id,
257
+ threadTitle: session.binding.title || session.binding.threadTitle || null,
258
+ prompt: buildCodexAgentBridgePrompt(session, op),
259
+ promptText: op.promptText ?? null,
260
+ exportPath: op.exportPath ?? null,
261
+ exportUrl: op.exportUrl ?? null,
262
+ selection: op.selection ?? null,
263
+ placeholderId: op.placeholderId ?? null,
264
+ elementCount: op.elementCount ?? null,
265
+ selectedElementIds: Array.isArray(op.selectedElementIds) ? op.selectedElementIds : [],
266
+ locale: op.locale ?? null,
267
+ note: op.note ?? null,
268
+ requiresAgentTool: 'codex_app.send_message_to_thread',
269
+ supportsForegroundMessage: true,
270
+ });
271
+ return {
272
+ ...base,
273
+ ...summarizeAgentBridgeOperation(bridgeOp),
274
+ status: 'pending',
275
+ supportsForegroundMessage: true,
276
+ };
277
+ }
278
+
279
+ async function listCodexAgentBridgeOutbox(session, options = {}) {
280
+ const includeAll = Boolean(options.all);
281
+ const ops = await readOps(session.paths);
282
+ const items = ops.ops
283
+ .filter((op) => op.type === 'agent-foreground-relay')
284
+ .filter((op) => includeAll || ['pending', 'claimed'].includes(op.status ?? 'pending'))
285
+ .map(summarizeAgentBridgeOperation)
286
+ .filter(Boolean);
287
+ return items;
288
+ }
289
+
290
+ async function claimCodexAgentBridgeItem(session, claim = 'next') {
291
+ const items = await listCodexAgentBridgeOutbox(session);
292
+ const value = String(claim || 'next');
293
+ const item = value === 'next'
294
+ ? items.find((candidate) => candidate.status === 'pending')
295
+ : items.find((candidate) => candidate.id === value);
296
+ if (!item) {
297
+ return null;
298
+ }
299
+ const updated = await updateOperation(session.paths, item.id, {
300
+ status: 'claimed',
301
+ claimedAt: timestamp(),
302
+ });
303
+ await appendOperation(session.paths, {
304
+ type: 'agent-foreground-relay-status',
305
+ bridgeOperationId: item.id,
306
+ status: 'claimed',
307
+ });
308
+ return summarizeAgentBridgeOperation(updated);
309
+ }
310
+
311
+ async function markCodexAgentBridgeItem(session, id, status, options = {}) {
312
+ if (!id) {
313
+ const error = new Error('Expected --id for canvas bridge status update.');
314
+ error.statusCode = 400;
315
+ throw error;
316
+ }
317
+ const normalizedStatus = status === 'failed' ? 'failed' : 'sent';
318
+ const timestampField = normalizedStatus === 'failed' ? 'failedAt' : 'sentAt';
319
+ const updated = await updateOperation(session.paths, id, {
320
+ status: normalizedStatus,
321
+ [timestampField]: timestamp(),
322
+ reason: options.reason ?? null,
323
+ sentThreadId: options.threadId ?? session.binding?.id ?? null,
324
+ });
325
+ await appendOperation(session.paths, {
326
+ type: 'agent-foreground-relay-status',
327
+ bridgeOperationId: id,
328
+ status: normalizedStatus,
329
+ reason: options.reason ?? null,
330
+ threadId: options.threadId ?? session.binding?.id ?? null,
331
+ });
332
+ return summarizeAgentBridgeOperation(updated);
333
+ }
334
+
335
+ async function queueCodexThreadRelay(session, op, options = {}, env = process.env) {
336
+ const skipReason = codexRelaySkipReason(session, op, options, env);
337
+ const input = buildCodexRelayInput(session, op);
338
+ const supportsDirectAttachment = input.some((item) => item.type === 'localImage');
339
+ const base = {
340
+ target: op.target ?? 'codex',
341
+ transport: CODEX_DELEGATION_RELAY_TRANSPORT,
342
+ supportsDirectMessage: !skipReason,
343
+ supportsDirectAttachment: !skipReason && supportsDirectAttachment,
344
+ inputTypes: input.map((item) => item.type),
345
+ promptText: op.promptText,
346
+ delivered: false,
347
+ pending: false,
348
+ accepted: false,
349
+ };
350
+ if (skipReason) {
351
+ return {
352
+ ...base,
353
+ status: 'skipped',
354
+ reason: skipReason,
355
+ supportsDirectMessage: false,
356
+ supportsDirectAttachment: false,
357
+ };
358
+ }
359
+
360
+ await fs.mkdir(session.paths.relaysDir, { recursive: true });
361
+ const relayName = sanitizeSessionSegment(op.id || `handoff-${Date.now()}`, 'handoff');
362
+ const payloadPath = cjoin(session.paths.relaysDir, `${relayName}.payload.json`);
363
+ const statusPath = cjoin(session.paths.relaysDir, `${relayName}.status.json`);
364
+ const logPath = cjoin(session.paths.relaysDir, `${relayName}.log`);
365
+ const payload = {
366
+ schema: 'openprd.codex.app-server-relay.v1',
367
+ createdAt: timestamp(),
368
+ handoffOperationId: op.id,
369
+ threadId: session.binding.id,
370
+ threadTitle: session.binding.title || session.binding.threadTitle || null,
371
+ cwd: session.projectRoot,
372
+ input,
373
+ payloadPath,
374
+ statusPath,
375
+ logPath,
376
+ codexBin: await resolveCodexAppServerBin(env),
377
+ timeoutMs: relayTimeoutMs(env),
378
+ };
379
+ await writeJson(payloadPath, payload);
380
+ await writeJson(statusPath, {
381
+ schema: 'openprd.codex.app-server-relay.status.v1',
382
+ status: 'queued',
383
+ createdAt: payload.createdAt,
384
+ updatedAt: timestamp(),
385
+ handoffOperationId: op.id,
386
+ threadId: session.binding.id,
387
+ });
388
+
389
+ if (options.relayDryRun || env.OPENPRD_CANVAS_CODEX_RELAY_DRY_RUN === '1') {
390
+ return {
391
+ ...base,
392
+ ...relayStatusSummary({ status: 'queued' }),
393
+ status: 'queued',
394
+ dryRun: true,
395
+ payloadPath,
396
+ statusPath,
397
+ logPath,
398
+ };
399
+ }
400
+
401
+ const child = spawn(process.execPath, [CODEX_APP_SERVER_RELAY_WORKER_PATH, payloadPath], {
402
+ cwd: session.projectRoot,
403
+ detached: true,
404
+ stdio: 'ignore',
405
+ });
406
+ child.unref();
407
+ const waitedStatus = await waitForCodexRelayStart(statusPath, relayStartWaitMs(options, env));
408
+ const statusSummary = relayStatusSummary(waitedStatus || { status: 'queued' });
409
+ return {
410
+ ...base,
411
+ status: statusSummary.status ?? 'queued',
412
+ payloadPath,
413
+ statusPath,
414
+ logPath,
415
+ pid: child.pid ?? null,
416
+ ...statusSummary,
417
+ };
418
+ }
419
+
420
+ export {
421
+ CODEX_AGENT_BRIDGE_TRANSPORT,
422
+ CODEX_DELEGATION_RELAY_TRANSPORT,
423
+ buildCodexDelegationText,
424
+ claimCodexAgentBridgeItem,
425
+ codexRelayNeedsAgentBridgeFallback,
426
+ listCodexAgentBridgeOutbox,
427
+ markCodexAgentBridgeItem,
428
+ queueCodexAgentBridge,
429
+ queueCodexThreadRelay,
430
+ };