@openprd/cli 0.1.18 → 0.1.21
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-control-plane-v020/.openprd.yaml +2 -0
- package/.openprd/changes/openprd-control-plane-v020/design.md +78 -0
- package/.openprd/changes/openprd-control-plane-v020/proposal.md +54 -0
- package/.openprd/changes/openprd-control-plane-v020/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/openprd-control-plane-v020/task-events.jsonl +27 -0
- package/.openprd/changes/openprd-control-plane-v020/tasks-002.md +35 -0
- package/.openprd/changes/openprd-control-plane-v020/tasks.md +427 -0
- 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/changes/remove-session-tracking-humane-approval/.openprd.yaml +2 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/design.md +52 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/proposal.md +35 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/task-events.jsonl +1 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/tasks.md +170 -0
- package/.openprd/design/active/asset-spec.md +19 -14
- package/.openprd/design/active/direction-plan.md +19 -3
- package/.openprd/design/active/facts-sheet.md +16 -7
- package/.openprd/design/active/image-preflight.md +6 -5
- package/.openprd/design/active/review-studio-v020-directions/compare-plan.json +34 -0
- package/.openprd/design/active/review-studio-v020-directions/contact-sheet.jpg +0 -0
- package/.openprd/design/active/review-studio-v020-directions/crops/01.png +0 -0
- package/.openprd/design/active/review-studio-v020-directions/crops/02.png +0 -0
- package/.openprd/design/active/review-studio-v020-directions/crops/03.png +0 -0
- package/.openprd/design/active/review-studio-v020-directions/focus-board.template.json +69 -0
- package/.openprd/design/active/review-studio-v020-directions/parallel-board.template.json +45 -0
- package/.openprd/design/active/review-studio-v020-directions/reference-set.json +143 -0
- package/.openprd/design/active/review-studio-v020-directions/source.png +0 -0
- package/.openprd/design/active/selected-direction.md +23 -9
- package/.openprd/engagements/active/control-plane-architecture.json +203 -0
- package/.openprd/engagements/active/control-plane-intake.json +418 -0
- package/.openprd/engagements/active/prd.md +186 -100
- package/.openprd/engagements/active/review-presentation-v0018.json +176 -0
- package/.openprd/i18n-config.json +12 -0
- package/.openprd/ledger/events.jsonl +6 -0
- package/.openprd/ledger/heads/ebea1a71a9daa566f1c91b53.json +10 -0
- package/.openprd/manifest.json +21 -0
- package/AGENTS.md +1 -0
- package/README.md +22 -0
- package/README_EN.md +27 -0
- package/package.json +1 -1
- package/skills/openprd-frontend-design/SKILL.md +16 -0
- package/skills/openprd-harness/SKILL.md +1 -1
- package/skills/openprd-quality/SKILL.md +1 -1
- package/skills/openprd-requirement-intake/SKILL.md +1 -1
- package/skills/openprd-shared/SKILL.md +1 -1
- package/src/adapters/adapter-spi.js +193 -0
- package/src/adapters/capability-envelope.js +98 -0
- package/src/adapters/event-normalizer.js +55 -0
- package/src/adapters/index.js +4 -0
- package/src/adapters/install-safety.js +249 -0
- package/src/agent-canonical-content.js +6 -2
- package/src/agent-integration.js +147 -42
- 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/cli/args.js +63 -4
- package/src/cli/gate-print.js +17 -0
- package/src/cli/quality-commands.js +18 -0
- package/src/cli/quality-print.js +10 -0
- package/src/cli/runtime-print.js +24 -0
- package/src/codex-hook-runner-template.mjs +87 -77
- package/src/codex-runtime.js +48 -5
- package/src/context/cache.js +245 -0
- package/src/context/compiler.js +438 -0
- package/src/context/constants.js +30 -0
- package/src/context/index.js +39 -0
- package/src/context/redaction.js +84 -0
- package/src/context/stable.js +69 -0
- package/src/context/telemetry.js +42 -0
- package/src/dev-standards.js +57 -0
- package/src/fleet.js +112 -95
- package/src/gates/index.js +2 -0
- package/src/gates/scoped-gates.js +256 -0
- package/src/gates/store.js +126 -0
- package/src/gates/workspace.js +41 -0
- package/src/html-artifacts.js +725 -28
- package/src/kernel/atomic-store.js +299 -0
- package/src/kernel/event-envelope.js +166 -0
- package/src/kernel/index.js +4 -0
- package/src/kernel/project-ledger.js +467 -0
- package/src/kernel/project-manifest.js +205 -0
- package/src/knowledge-v3/index.js +1 -0
- package/src/knowledge-v3/lifecycle.js +290 -0
- package/src/knowledge.js +14 -7
- package/src/openprd.js +71 -2
- package/src/review-model.js +413 -0
- package/src/review-presentation.js +1 -1
- package/src/run-harness.js +431 -37
- package/src/runtime/cli_runtime_README.md +28 -0
- package/src/runtime/errors.js +66 -0
- package/src/runtime/index.js +44 -0
- package/src/runtime/lane-schema.js +141 -0
- package/src/runtime/lane-store.js +279 -0
- package/src/runtime/task-runtime.js +449 -0
- package/src/runtime/workspace.js +179 -0
- package/src/runtime/write-set.js +206 -0
- package/src/session-binding.js +16 -3
- package/src/session-registry.js +59 -1
- package/src/upgrade/fleet-mutation.js +166 -0
- package/src/upgrade/fleet-transaction.js +398 -0
- package/src/upgrade/transaction-store.js +416 -0
- package/src/visual-compare-core.js +66 -27
- package/src/visual-compare.js +18 -12
- package/src/workspace-core.js +109 -7
- package/src/workspace-registry.js +39 -1
- package/src/workspace-workflow.js +18 -15
|
@@ -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, '<')
|
|
56
|
+
.replace(/>/g, '>');
|
|
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
|
+
};
|
package/src/canvas-i18n.js
CHANGED
|
@@ -100,6 +100,7 @@ const CANVAS_LOCALES = {
|
|
|
100
100
|
aiTemplateOnCanvas: '比例 {ratio},等待图片回填',
|
|
101
101
|
aiImagePromptIntro: '请使用 Codex 原生生图能力生成一张图片,并回填到 OpenPrd Canvas 的这张 AI 生图卡片。',
|
|
102
102
|
aiImagePromptMeta: '画布会话:{binding};卡片比例:{ratio};占位卡 ID:{placeholderId}',
|
|
103
|
+
aiImagePromptSize: '目标尺寸契约:宽 {width}px、高 {height}px、宽高比 {ratio};生成图片请按这个比例出图,避免回填时变形。',
|
|
103
104
|
aiImagePromptReturn: '生成完成后,请回填到当前画布里的这张占位卡,回填信息如下:',
|
|
104
105
|
selectedCount: '已选中 {count} 个元素',
|
|
105
106
|
sendBarHint: '预览选区截图,输入指令后发送给 Codex。',
|
|
@@ -131,6 +132,9 @@ const CANVAS_LOCALES = {
|
|
|
131
132
|
codexPromptDefaultInstruction: '请根据这个画布选区继续协同。',
|
|
132
133
|
codexPromptIntro: '画布选区截图:',
|
|
133
134
|
codexPromptMeta: '来源:OpenPrd Canvas;选区元素:{count};会话:{binding}',
|
|
135
|
+
annotationEditPromptIntro: '这是一次画布标注改图请求:截图里包含原图和用户手绘标注,请把标注当作修改指令,生成一张修改后的新图。',
|
|
136
|
+
annotationEditPromptMeta: '原图元素 ID:{sourceIds};标注元素 ID:{annotationIds}',
|
|
137
|
+
annotationEditPromptReturn: '生成修改后的新图后,请调用画布接口把新图放到原图旁边(不要覆盖原图),插入信息如下:',
|
|
134
138
|
},
|
|
135
139
|
en: {
|
|
136
140
|
appSubtitle: 'Conversation canvas',
|
|
@@ -210,6 +214,7 @@ const CANVAS_LOCALES = {
|
|
|
210
214
|
aiTemplateOnCanvas: 'Ratio {ratio}, waiting for image',
|
|
211
215
|
aiImagePromptIntro: 'Use Codex native image generation to create one image and fill this OpenPrd Canvas AI image card.',
|
|
212
216
|
aiImagePromptMeta: 'Canvas session: {binding}; card ratio: {ratio}; placeholder ID: {placeholderId}',
|
|
217
|
+
aiImagePromptSize: 'Target size contract: width {width}px, height {height}px, aspect ratio {ratio}; generate the image at this ratio so it fills the card without distortion.',
|
|
213
218
|
aiImagePromptReturn: 'When the image is ready, fill this placeholder card back into the current canvas with this information:',
|
|
214
219
|
selectedCount: '{count} selected',
|
|
215
220
|
sendBarHint: 'Preview the selection, add instructions, then send it to Codex.',
|
|
@@ -241,6 +246,9 @@ const CANVAS_LOCALES = {
|
|
|
241
246
|
codexPromptDefaultInstruction: 'Please continue based on this canvas selection.',
|
|
242
247
|
codexPromptIntro: 'Canvas selection image:',
|
|
243
248
|
codexPromptMeta: 'Source: OpenPrd Canvas; selected elements: {count}; session: {binding}',
|
|
249
|
+
annotationEditPromptIntro: 'This is an annotation-driven image revision request: the screenshot contains the original image plus the user\'s hand-drawn annotations. Treat the annotations as edit instructions and generate a revised image.',
|
|
250
|
+
annotationEditPromptMeta: 'Source image element IDs: {sourceIds}; annotation element IDs: {annotationIds}',
|
|
251
|
+
annotationEditPromptReturn: 'After generating the revised image, call the canvas API to place it beside the original (do not overwrite the original). Insert with this information:',
|
|
244
252
|
},
|
|
245
253
|
};
|
|
246
254
|
|