@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,249 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const INSTALL_SAFETY_PROTOCOL_VERSION = 1;
|
|
7
|
+
const SUPPORTED_SYMLINK_STRATEGIES = new Set(['block', 'materialize']);
|
|
8
|
+
|
|
9
|
+
class UnsafeManagedWriteError extends Error {
|
|
10
|
+
constructor(targetPath, symlink, strategy = 'block') {
|
|
11
|
+
const resolved = symlink.resolvedTarget ? ` -> ${symlink.resolvedTarget}` : '';
|
|
12
|
+
super(
|
|
13
|
+
`Refusing to write ${targetPath}: managed write path crosses symlink ${symlink.path}${resolved}. `
|
|
14
|
+
+ `Use symlinkStrategy=materialize with a trusted installSafetyBackupRoot to snapshot and replace the link before writing.`,
|
|
15
|
+
);
|
|
16
|
+
this.name = 'UnsafeManagedWriteError';
|
|
17
|
+
this.code = 'OPENPRD_UNSAFE_MANAGED_WRITE_SYMLINK';
|
|
18
|
+
this.targetPath = targetPath;
|
|
19
|
+
this.symlinkPath = symlink.path;
|
|
20
|
+
this.linkTarget = symlink.linkTarget;
|
|
21
|
+
this.resolvedTarget = symlink.resolvedTarget;
|
|
22
|
+
this.strategy = strategy;
|
|
23
|
+
this.repairHint = 'Choose a physical destination, or rerun with symlinkStrategy=materialize and a trusted backup root.';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeSymlinkStrategy(value) {
|
|
28
|
+
const strategy = String(value ?? 'block').trim().toLowerCase() || 'block';
|
|
29
|
+
if (!SUPPORTED_SYMLINK_STRATEGIES.has(strategy)) {
|
|
30
|
+
throw new Error(`Unsupported managed-write symlink strategy: ${value}. Use block or materialize.`);
|
|
31
|
+
}
|
|
32
|
+
return strategy;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function lstatOrNull(filePath) {
|
|
36
|
+
return fs.lstat(filePath).catch((error) => {
|
|
37
|
+
if (error?.code === 'ENOENT') return null;
|
|
38
|
+
throw error;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isWithinRoot(rootPath, targetPath) {
|
|
43
|
+
const relative = path.relative(rootPath, targetPath);
|
|
44
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function managedRootForTarget(targetPath, options = {}) {
|
|
48
|
+
const configured = Array.isArray(options.managedWriteRoots)
|
|
49
|
+
? options.managedWriteRoots
|
|
50
|
+
: [options.projectRoot].filter(Boolean);
|
|
51
|
+
return configured
|
|
52
|
+
.map((root) => path.resolve(root))
|
|
53
|
+
.filter((root) => isWithinRoot(root, targetPath))
|
|
54
|
+
.sort((left, right) => right.length - left.length)[0] ?? null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function findFirstSymlink(targetPath, options = {}) {
|
|
58
|
+
const absoluteTarget = path.resolve(targetPath);
|
|
59
|
+
const managedRoot = managedRootForTarget(absoluteTarget, options);
|
|
60
|
+
const parsed = path.parse(absoluteTarget);
|
|
61
|
+
const scanRoot = managedRoot ?? parsed.root;
|
|
62
|
+
const rootStat = managedRoot ? await lstatOrNull(scanRoot) : null;
|
|
63
|
+
if (rootStat?.isSymbolicLink()) {
|
|
64
|
+
const linkTarget = await fs.readlink(scanRoot);
|
|
65
|
+
const resolvedTarget = await fs.realpath(scanRoot).catch(() => path.resolve(path.dirname(scanRoot), linkTarget));
|
|
66
|
+
return {
|
|
67
|
+
path: scanRoot,
|
|
68
|
+
linkTarget,
|
|
69
|
+
resolvedTarget,
|
|
70
|
+
dangling: !(await lstatOrNull(resolvedTarget)),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (managedRoot && !rootStat) return null;
|
|
74
|
+
const parts = path.relative(scanRoot, absoluteTarget).split(path.sep).filter(Boolean);
|
|
75
|
+
let cursor = scanRoot;
|
|
76
|
+
for (const part of parts) {
|
|
77
|
+
cursor = path.join(cursor, part);
|
|
78
|
+
const stat = await lstatOrNull(cursor);
|
|
79
|
+
if (!stat) return null;
|
|
80
|
+
if (!stat.isSymbolicLink()) continue;
|
|
81
|
+
const linkTarget = await fs.readlink(cursor);
|
|
82
|
+
const resolvedTarget = await fs.realpath(cursor).catch(() => path.resolve(path.dirname(cursor), linkTarget));
|
|
83
|
+
return {
|
|
84
|
+
path: cursor,
|
|
85
|
+
linkTarget,
|
|
86
|
+
resolvedTarget,
|
|
87
|
+
dangling: !(await lstatOrNull(resolvedTarget)),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function resolveBackupRoot(options = {}) {
|
|
94
|
+
if (options.installSafetyBackupRoot) {
|
|
95
|
+
return path.resolve(options.installSafetyBackupRoot);
|
|
96
|
+
}
|
|
97
|
+
const physicalTemp = await fs.realpath(os.tmpdir()).catch(() => path.resolve(os.tmpdir()));
|
|
98
|
+
return path.join(physicalTemp, 'openprd-install-safety', String(process.pid));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function backupEntryName(symlinkPath) {
|
|
102
|
+
const digest = crypto.createHash('sha256').update(symlinkPath).digest('hex').slice(0, 12);
|
|
103
|
+
const suffix = `${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
104
|
+
return `${path.basename(symlinkPath) || 'root'}-${digest}-${suffix}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function copyPath(sourcePath, destinationPath, stat) {
|
|
108
|
+
if (stat.isDirectory()) {
|
|
109
|
+
await fs.cp(sourcePath, destinationPath, {
|
|
110
|
+
recursive: true,
|
|
111
|
+
dereference: false,
|
|
112
|
+
errorOnExist: true,
|
|
113
|
+
force: false,
|
|
114
|
+
});
|
|
115
|
+
return 'directory';
|
|
116
|
+
}
|
|
117
|
+
if (stat.isFile()) {
|
|
118
|
+
await fs.mkdir(path.dirname(destinationPath), { recursive: true });
|
|
119
|
+
await fs.copyFile(sourcePath, destinationPath);
|
|
120
|
+
return 'file';
|
|
121
|
+
}
|
|
122
|
+
throw new Error(`Cannot materialize unsupported symlink target type at ${sourcePath}.`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function materializeSymlink(targetPath, symlink, options = {}) {
|
|
126
|
+
const backupRoot = await resolveBackupRoot(options);
|
|
127
|
+
const backupSymlink = await findFirstSymlink(backupRoot);
|
|
128
|
+
if (backupSymlink) {
|
|
129
|
+
throw new UnsafeManagedWriteError(backupRoot, backupSymlink, 'block');
|
|
130
|
+
}
|
|
131
|
+
await fs.mkdir(backupRoot, { recursive: true });
|
|
132
|
+
|
|
133
|
+
const backupDir = path.join(backupRoot, backupEntryName(symlink.path));
|
|
134
|
+
await fs.mkdir(backupDir, { recursive: false });
|
|
135
|
+
const snapshotPath = path.join(backupDir, 'snapshot');
|
|
136
|
+
const targetStat = symlink.dangling ? null : await lstatOrNull(symlink.resolvedTarget);
|
|
137
|
+
const isDirectTarget = path.resolve(targetPath) === path.resolve(symlink.path);
|
|
138
|
+
if (targetStat?.isFile() && !isDirectTarget) {
|
|
139
|
+
throw new Error(`Cannot materialize ${symlink.path}: it resolves to a file but is an ancestor of ${targetPath}.`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let materializedType = isDirectTarget
|
|
143
|
+
? (options.managedWriteTargetType ?? 'file')
|
|
144
|
+
: 'directory';
|
|
145
|
+
if (targetStat) {
|
|
146
|
+
materializedType = await copyPath(symlink.resolvedTarget, snapshotPath, targetStat);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const metadata = {
|
|
150
|
+
protocolVersion: INSTALL_SAFETY_PROTOCOL_VERSION,
|
|
151
|
+
createdAt: new Date().toISOString(),
|
|
152
|
+
targetPath: path.resolve(targetPath),
|
|
153
|
+
symlinkPath: symlink.path,
|
|
154
|
+
linkTarget: symlink.linkTarget,
|
|
155
|
+
resolvedTarget: symlink.resolvedTarget,
|
|
156
|
+
dangling: symlink.dangling,
|
|
157
|
+
directTarget: isDirectTarget,
|
|
158
|
+
materializedType,
|
|
159
|
+
snapshotPath: targetStat ? snapshotPath : null,
|
|
160
|
+
};
|
|
161
|
+
await fs.writeFile(path.join(backupDir, 'metadata.json'), `${JSON.stringify(metadata, null, 2)}\n`, 'utf8');
|
|
162
|
+
|
|
163
|
+
const stagingPath = path.join(
|
|
164
|
+
path.dirname(symlink.path),
|
|
165
|
+
`.${path.basename(symlink.path)}.openprd-materialize-${crypto.randomBytes(6).toString('hex')}`,
|
|
166
|
+
);
|
|
167
|
+
if (targetStat) {
|
|
168
|
+
await copyPath(snapshotPath, stagingPath, targetStat);
|
|
169
|
+
} else if (!isDirectTarget) {
|
|
170
|
+
await fs.mkdir(stagingPath, { recursive: false });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const currentStat = await lstatOrNull(symlink.path);
|
|
174
|
+
if (!currentStat?.isSymbolicLink() || await fs.readlink(symlink.path) !== symlink.linkTarget) {
|
|
175
|
+
await fs.rm(stagingPath, { recursive: true, force: true }).catch(() => {});
|
|
176
|
+
throw new Error(`Symlink changed while preparing managed write: ${symlink.path}.`);
|
|
177
|
+
}
|
|
178
|
+
await fs.unlink(symlink.path);
|
|
179
|
+
if (targetStat || !isDirectTarget) {
|
|
180
|
+
await fs.rename(stagingPath, symlink.path);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const event = {
|
|
184
|
+
protocolVersion: INSTALL_SAFETY_PROTOCOL_VERSION,
|
|
185
|
+
strategy: 'materialize',
|
|
186
|
+
targetPath: path.resolve(targetPath),
|
|
187
|
+
symlinkPath: symlink.path,
|
|
188
|
+
linkTarget: symlink.linkTarget,
|
|
189
|
+
resolvedTarget: symlink.resolvedTarget,
|
|
190
|
+
backupPath: backupDir,
|
|
191
|
+
materializedType,
|
|
192
|
+
at: metadata.createdAt,
|
|
193
|
+
};
|
|
194
|
+
if (Array.isArray(options.installSafetyEvents)) {
|
|
195
|
+
options.installSafetyEvents.push(event);
|
|
196
|
+
}
|
|
197
|
+
return event;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function prepareManagedWriteTarget(targetPath, options = {}) {
|
|
201
|
+
const absoluteTarget = path.resolve(targetPath);
|
|
202
|
+
const strategy = normalizeSymlinkStrategy(options.symlinkStrategy);
|
|
203
|
+
const materializations = [];
|
|
204
|
+
for (let index = 0; index < 32; index += 1) {
|
|
205
|
+
const symlink = await findFirstSymlink(absoluteTarget, options);
|
|
206
|
+
if (!symlink) {
|
|
207
|
+
return {
|
|
208
|
+
ok: true,
|
|
209
|
+
protocolVersion: INSTALL_SAFETY_PROTOCOL_VERSION,
|
|
210
|
+
strategy,
|
|
211
|
+
targetPath: absoluteTarget,
|
|
212
|
+
materializations,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
if (strategy === 'block') {
|
|
216
|
+
throw new UnsafeManagedWriteError(absoluteTarget, symlink, strategy);
|
|
217
|
+
}
|
|
218
|
+
materializations.push(await materializeSymlink(absoluteTarget, symlink, options));
|
|
219
|
+
}
|
|
220
|
+
throw new Error(`Managed write path contains too many nested symlinks: ${absoluteTarget}.`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function safeEnsureDirectory(directoryPath, options = {}) {
|
|
224
|
+
await prepareManagedWriteTarget(directoryPath, { ...options, managedWriteTargetType: 'directory' });
|
|
225
|
+
await fs.mkdir(directoryPath, { recursive: true });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function safeWriteText(filePath, text, options = {}) {
|
|
229
|
+
await prepareManagedWriteTarget(filePath, { ...options, managedWriteTargetType: 'file' });
|
|
230
|
+
await safeEnsureDirectory(path.dirname(filePath), options);
|
|
231
|
+
await fs.writeFile(filePath, text, 'utf8');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function safeAppendText(filePath, text, options = {}) {
|
|
235
|
+
await prepareManagedWriteTarget(filePath, { ...options, managedWriteTargetType: 'file' });
|
|
236
|
+
await safeEnsureDirectory(path.dirname(filePath), options);
|
|
237
|
+
await fs.appendFile(filePath, text, 'utf8');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export {
|
|
241
|
+
INSTALL_SAFETY_PROTOCOL_VERSION,
|
|
242
|
+
UnsafeManagedWriteError,
|
|
243
|
+
findFirstSymlink,
|
|
244
|
+
normalizeSymlinkStrategy,
|
|
245
|
+
prepareManagedWriteTarget,
|
|
246
|
+
safeAppendText,
|
|
247
|
+
safeEnsureDirectory,
|
|
248
|
+
safeWriteText,
|
|
249
|
+
};
|
|
@@ -145,9 +145,11 @@ export const CANONICAL_SKILLS = [
|
|
|
145
145
|
'- 实现就绪还要运行 `openprd quality . --verify`,并审阅 HTML 质量评估报告中的场景标签、必需 EVO 门禁、可观测性、业务护栏、评估执行环境、性能和知识缺口。L2 或跨页面实现的最终回复必须带上最新 HTML 质量报告和 task-scoped 测试报告路径;缺失时只能说“实现完成但项目级收口未完成”。',
|
|
146
146
|
'- 用户要求生成图片、封面图、配图、海报、插画、图标、贴纸、头像、banner、主视觉/KV、运营图、效果图、视觉稿、mockup、先看样子或先确认设计方向时,先按生图路由选工具:Codex 环境用原生 `imagegen`(Image 2),Cursor 环境用内置 `GenerateImage`,两者都是对话工具内免费能力;都不可用或无法判断时,先读 `.openprd/harness/image-generation-preference.json` 的用户已确认偏好,没有偏好就先问用户选哪种生图方式并把答复以 `user-confirmed` 来源写回该文件作为下次默认;绝不擅自调用用户本地或自有付费生图 API。生图工具是工具路径,不是审美豁免。生成前先写清用途、受众、气质、约束和记忆点,并把它们写进 prompt;没有品牌或参考图依据时,用 anti-slop 排除默认紫白/蓝紫渐变、通用字体、白底卡片堆叠和无语境装饰。对 logo、icon、avatar、badge 等开发素材,如果用户未明确要求 mockup、场景图、设备框、卡片承载、名片/包装展示或参考界面复刻,默认按独立素材输出(standalone asset)处理:使用全画布单主体,不额外添加 UI frame、卡片、设备壳、名片、桌面陈列、手持实拍或其他展示容器。只有当用户明确要求 mockup、场景化效果图、容器化呈现,或参考图本身包含这些结构时,才生成对应容器或场景;除非用户明确指定 HTML、SVG、CSS、Canvas、代码稿或可编辑矢量/source artifact,不要改用临时 HTML/SVG/CSS 再截图。只有实际发生生图工具调用后,才能汇报生图结果、失败或限流。生图结果先当候选效果图,不要默认登记到 `.openprd/harness/visual-reviews/`;如果用户还要继续做实现,主动确认是否符合预期、是否纳入后续效果图/实现截图对比、以及是否按此继续实现。',
|
|
147
147
|
'- 对 logo、icon、avatar、badge、贴纸、空态插画、单物件 UI 位图等开发素材,如果最终要接入 UI 并需要透明背景,默认走“候选评审 -> 资产工程化 -> 接入验证”的图标资产链路:先基于用途、受众、气质、约束和记忆点生成 3 个差异足够大的独立素材候选方向,并保持纯 `#00ff00` 绿幕、无文字、无 UI 容器、主体居中且留足裁切边距;用户选定前不写入项目文件。用户选定后再定位源图或 contact sheet,保留绿幕源图,用 `remove_chroma_key.py` 抠成透明 PNG/WebP,按真实 UI 需要裁切居中并导出 384px 或多尺寸资产;接入时按首页卡片、工具格、吸顶栏、偏好预览等实际场景分别调显示比例,而不是只换图片路径。收口时同步写回 `.openprd/design/active/asset-spec.md` 和 `selected-direction.md`,说明选中的方向、资产路径、透明产物、接入位置和验证结果;最终回复必须区分绿幕源图、透明产物和是否已经接入。',
|
|
148
|
-
'- OpenPrd 的 `review.html` 用于需求评审,不能替代图片或效果图生成;`visual-compare` 只用于实现阶段视觉证据:已有确认参考图时对比“效果图 / 实现截图”;没有参考图时先判断新建界面还是修改既有界面,新建界面回到实现前 3 方向方案评审,修改既有界面再对比“修改前 / 修改后”;当局部细节更重要时,优先改用 `--board` 生成“局部焦点证据板”;当并行跑了多个优化方向时,优先改用 `--board` 生成“并行实验证据板”;当普通截图、Computer/Browser/Playwright 实测截图被用作证据时,必须改用 `--board` 生成“截图实测证据板”;当新功能或改动包含同构列表、卡片、网格、表格,或用户反馈排版没对齐时,必须改用 `--board`
|
|
148
|
+
'- OpenPrd 的 `review.html` 用于需求评审,不能替代图片或效果图生成;`visual-compare` 只用于实现阶段视觉证据:已有确认参考图时对比“效果图 / 实现截图”;没有参考图时先判断新建界面还是修改既有界面,新建界面回到实现前 3 方向方案评审,修改既有界面再对比“修改前 / 修改后”;当局部细节更重要时,优先改用 `--board` 生成“局部焦点证据板”;当并行跑了多个优化方向时,优先改用 `--board` 生成“并行实验证据板”;当普通截图、Computer/Browser/Playwright 实测截图被用作证据时,必须改用 `--board` 生成“截图实测证据板”;当新功能或改动包含同构列表、卡片、网格、表格,或用户反馈排版没对齐时,必须改用 `--board` 生成“对齐辅助线证据板”,且同时覆盖容器轨道和内部内容槽位轨道。网格、基线和对齐辅助线只允许出现在 `alignment-board` 的校验材料中;普通截图、最终 before/after、reference/actual 和 verification-board 只展示结果,不得自动叠加网格。当单个 logo、icon、avatar、badge、按钮图形或图片内部需要居中判定、视觉重心评估或用户反馈偏心时,必须改用 `--board` 生成“内部居中证据板”,用红色画布中心线、绿色主体外接框和黄色视觉重心点展示偏移。视觉证据不仅检查位置和结构,也要检查选定气质、层级、字体/色彩/动效/表面角色和记忆点是否保住。当参考图是一张整板、网格图、多对象或多子图组合时,先运行 `openprd visual-prepare` 生成 reference-set、contact sheet 和 board 模板,再进入实现对比。',
|
|
149
149
|
'- 轻量 UI 可视优化也要走视觉证据门:卡片宽度、间距、留白、对齐、颜色、圆角、字号、按钮、图标这类用户可见小改,不自动升级成大界面 3 方向评审,但动手前要有一句本轮审美意图和记忆点,收口前至少补 `visual-compare --before/--after`、`--board <focus-board.json>` 或 `--board <verification-board.json>`;如果存在同构列表、卡片、网格或表格,或用户反馈没对齐,还要补 `--board <alignment-board.json>`,把真实截图、辅助线、容器轨道 spread 和内容槽位 spread 放在一张板里;如果关注单个素材/图标/头像/徽标/按钮图形的内部居中或视觉重心,还要补 `--board <centering-board.json>`,把画布中心、主体外接框中心和视觉重心偏移放在一张板里;build、package、dev-check、单元测试和单张原始截图都不能替代视觉证据。',
|
|
150
150
|
'- 大界面改动在需求分流后、PRD 定稿或实现开工前先做视觉方案评审。先判断这是不是会决定首屏、核心布局、信息架构、主视觉或关键路径的场景,而不是按关键词触发;已有界面时用 Codex Computer Use 进入产品内对应功能并截当前真实界面,冷启动没有现有界面时基于已确认 PRD、用户群体、第一版切片和视觉目标生成设计 brief。brief 必须写清用途、受众、气质端点、约束和记忆点,3 个方向要分别说明审美主张和主动避开的模板味。再按生图路由调用内置生图工具(Codex=`imagegen`/Image 2,Cursor=`GenerateImage`)生成至少 3 个不同设计思想方向;把效果图横向拼成一张带 1/2/3 序号的大图,先作为候选效果图展示给用户。只有用户确认纳入后续对比或继续实现后,才把选定方向、整张图或其中子图整理到 `.openprd/harness/visual-reviews/` 并进入实现。',
|
|
151
|
+
'- 大界面三方向是 Agent 的默认责任,不要求用户另行提出生图;轻量 UI 微调不触发。已有界面时,Codex 原生 App 用 Computer Use、Codex 网页环境用 Chrome 插件任务专用窗口、Cursor 用可用浏览器或项目预览能力截图。三个方向必须共享同一张当前截图作为 image-to-image 参考;除非用户明确要求换风格,否则锁定字体角色、色彩系统、明暗模式、表面语言、圆角/边框/阴影、图标语言、平台框架和信息密度,只探索布局、层级、导航、关键路径与空间构图。冷启动没有现有界面时才使用 design brief,不伪造修改前截图。',
|
|
152
|
+
'- 界面候选图的内建质量标准:先明确用途、目标用户、业务约束、核心动作、气质和唯一记忆点,再从字体角色、色彩、空间构图、信息层级、表面/图标语言和动效含义评审;拒绝无依据蓝紫渐变、默认字体、等权白卡堆叠、过量大圆角、无语境装饰和只换颜色的伪方向。',
|
|
151
153
|
'- 界面、页面、视觉、样式或前端体验开发中,只要已经有效果图、设计稿、图片资产或用户给图并进入实现阶段,阶段性完成后必须先截实现图,再运行 `openprd visual-compare . --reference <效果图> --actual <实现截图> --locale <当前主语言>` 生成左右对比 JPG;中文语境默认标注“效果图 / 实现截图”,英文语境默认标注“Reference / Implementation”。如果这次要审局部细节,就补一份 `--board <focus-board.json>` 的局部焦点证据板。如果一张参考图里有多个子图、网格或对象,先运行 `openprd visual-prepare . --reference <效果图> --grid <列>x<行>` 或 `--boxes <plan.json>`,确认 contact sheet 后再逐项对比。普通截图和 Computer/Browser/Playwright 实测截图只能作为原始素材,收口前要用 `--board <verification-board.json> --locale <当前主语言>` 拼成截图实测证据板。证据板 JSON 里的 title、summary、label、notes、checks 等用户可见文案必须跟随用户当前主语言;中文语境不要整段写英文,英文语境不要混入默认中文标签。新功能开发或既有调整里出现同构列表、卡片、网格、表格时,即使用户没有主动提“对齐”,也要用 `--board <alignment-board.json>` 生成对齐辅助线证据板,先检查卡片外框、列宽、行顶等容器轨道,再检查标题、副标题、描述、标签、状态、价格、按钮、图标和操作区等内部内容槽位是否成轨;用户反馈没对齐/排版漂移时也走同一条路径。单个 logo、icon、avatar、badge、按钮图形或图片内部需要居中判定、视觉重心评估或用户反馈偏心时,要用 `--board <centering-board.json>` 生成内部居中证据板,检查画布中心、主体外接框中心和视觉重心偏移。Agent 必须查看合成图并继续对标,直到结构、气质、层级、字体/色彩/表面角色和记忆点都没有明显差异;如果用户后续说“跟效果图”“不一致”“好丑”“复刻”,至少先产出一份视觉证据图,不能只凭主观判断宣称完成。',
|
|
152
154
|
'- 界面、页面、视觉、样式或前端体验开发中,如果没有明确效果图、设计稿、图片资产或用户给图,要先判断这是新建界面还是修改既有界面:新建界面走实现前 3 方向方案评审;修改既有界面则动手前必须先用 Computer Use、Browser、Playwright 或项目现有工具截取修改前截图。完成后用同一入口、视口、账号和数据状态截取修改后截图,再运行 `openprd visual-compare . --before <修改前截图> --after <修改后截图> --locale <zh-CN|en>`。如果这次并行试了多条优化方向,再补一份 `--board <parallel-board.json>` 的并行实验证据板;如果只是普通截图或 Computer/Browser/Playwright 实测截图,也必须补 `--board <verification-board.json>` 的截图实测证据板。Agent 必须查看合成图,确认预期变化出现且未改区域没有明显漂移。',
|
|
153
155
|
'- 界面任务进入实现前,先用 `.openprd/design/` 锁定设计框架:页面涉及具体产品事实、版本、发布时间、规格、价格、引用数据或地点事实时,先补 `.openprd/design/active/facts-sheet.md`;页面依赖 logo、产品图、UI 图、摄影图、插图、图表或品牌色字体时,先补 `.openprd/design/active/asset-spec.md`;旅游、展览、内容、案例、发布、品牌故事等内容型页面,要先判断真实图片是不是页面成立前提,必要时先补 `.openprd/design/active/image-preflight.md`;没有明确参考方向时,先补 `.openprd/design/active/direction-plan.md`,并确保 3 个方向来自不同生成逻辑;用户选定方向后,再补 `.openprd/design/active/selected-direction.md`,把选中的 lens、theme、layout、组件和风险锁定。如果用户已经给了效果图、设计稿、参考截图或其他明确参考图,先把它当成主参考源:只有现有 starter、theme、layout 足够接近时才复用,不接近就允许偏离默认组合,以参考图为准。空白工作区的静态原型优先从 `.openprd/design/templates/` 里挑最近模板;如果当前轮用户已经把页面主题、模块范围或“直接实现”的意图说清,优先运行 `openprd run . --context --message <用户原话>`。如果页面主题和模块范围已经明确,优先运行 `openprd design-starter . --starter <starter-id> --out index.html --brief "<页面主题>" --sections "<模块1|模块2|模块3>"`,让 starter 一次写实 active design artifacts 和第一版真实页面。只有像个人博客、工具台、纯结构化产品页这类确认不靠真实图片成立的页面,才在 active design artifacts 写清无依赖并补 `--no-external-facts --no-brand-assets --no-real-images`;若题目更像旅游、导览、展览、博物馆、城市、自然观察或案例内容页,先不要带 `--no-real-images`,让 starter 先尝试补首批真实图片;若这类冷启动即使带 message 仍短暂返回 `clarify-user`,把它当成摘要级提醒,先用 3 到 5 行 mini-plan 收口,再继续。starter 落地后默认进入 `Patch Mode`:必须直接在生成的入口文件上补丁修改;即使结构要大改,也是在同一路径内覆盖,不做 delete-first,更不要删除 `index.html` 后另起新稿。如果确实要整页重写,先把完整新稿写到 sibling draft,例如 `index.next.html`,确认内容成形后再覆盖回 `index.html`,不要让正式入口出现空窗。starter 一落地后,只允许做一轮就地对焦:快速读一次生成的入口文件和必要的 active design artifacts;这轮对焦结束后,下一步就必须是真实写入口,不要再回头搜网页、翻 `docs/basic/` 或继续模板漫游。把最后一批必要的查事实、查图、读模板动作放在口头宣布之前做完;一旦已经说“开始覆盖入口文件”或“开始整页重写”,下一步必须出现真实写文件动作,而不是继续只读浏览、压图或停在口头承诺;必要时 hook 会把这类非写入动作挡回去。`Patch Mode` 完成不等于只补合同、只下载素材或只写计划;至少要把入口文件本体改完、主要占位清掉,并把已准备好的真实图片或参考约束真正落进页面。',
|
|
@@ -178,7 +180,7 @@ export const CANONICAL_SKILLS = [
|
|
|
178
180
|
'- 用户明确要求 Computer Use 时优先使用 Computer Use,并尽量在 Codex-owned browser window 中操作;对提交、删除、发送、切换账号、退出登录、支付、关闭标签页等高风险网页动作先确认窗口归属。',
|
|
179
181
|
'- 写产品界面上用户能看到的文案时,先站在“正在使用这个界面的用户”视角:这句话要说明用户现在能做什么、会发生什么、为什么值得点、下一步怎么走;不要站在开发者、产品经理或运营分类视角描述“适合某类用户”“这个模块用于承载某能力”。除非这是面向开发者或专业技术人员的技术型产品,否则默认用普通人能看懂的语言写结果、状态、限制和行动。',
|
|
180
182
|
'- 产品界面文案要先做一次用户视角改写检查:坏例“适合想保留全部工具入口的用户”,改成“首页会显示所有工具入口,你可以直接选择需要的功能”;坏例“API 请求失败,错误码 500”,改成“暂时保存失败,请稍后再试”;坏例“专业版会启用高级 pipeline”,改成“打开更多编辑工具,方便继续细调”。如果确实是技术型产品,可以保留必要术语,但仍要写清用户动作、影响和修复路径,例如“Webhook 验证失败,请检查签名密钥和权限”。',
|
|
181
|
-
'- 修改用户可见文案前,先检查 `i18n`、`locales`、`translations`、`Localizable` 或其他语言资源;若项目已有多语言结构,用户可见文案要同步维护到所有已支持语言,并避免暴露 API、SDK
|
|
183
|
+
'- 修改用户可见文案前,先检查 `i18n`、`locales`、`translations`、`Localizable` 或其他语言资源;若项目已有多语言结构,用户可见文案要同步维护到所有已支持语言,并避免暴露 API、SDK、模型、数据库、缓存或错误码等实现细节。如果项目有 `.openprd/i18n-config.json`,按配置的 `defaultLanguage` 和 `supportedLanguages` 执行;最佳实践参考 `docs/basic/i18n-guidelines.md`。',
|
|
182
184
|
'',
|
|
183
185
|
'## 写入纪律',
|
|
184
186
|
'',
|
|
@@ -751,6 +753,8 @@ export function renderCommandCatalog() {
|
|
|
751
753
|
'- `openprd canvas . --thread <id> --thread-title <name> --daemon --open --open-target codex-browser`:持久开启对话协同画布,本地页面按当前 thread/session ID 保存到 `.openprd/harness/canvas-sessions/<session-key>/`,title/name 只用于右侧“当前对话”展示和 daemon 透传,避免多个 Codex / Claude / Cursor 窗口串用同一块画布。若 `runtime-environment.json` 已有当前 hook/session 证据,可省略 `--thread`,若证据里有 `threadTitle/conversationTitle/sessionTitle` 会自动显示;证据不足时必须显式传 `--thread` 或 `--session`,知道标题时同步传 `--thread-title` 或 `--session-title`。`--open-target codex-browser` 表示不要调用系统 `open`,由 Codex Agent 使用右侧 Browser 工具打开返回地址;已验证的 Agent 侧最小路径是 Browser plugin + `node_repl`:`setupBrowserRuntime -> agent.browsers.get("iab") -> visibility.set(true) -> browser.tabs.new() -> tab.goto(url)`。缺少 Codex Browser 证据时 `auto` 会回退系统浏览器。用户点击“发送给 Codex”会在 `ops.json` 里保存 `handoff` 记录,并默认通过本机 Codex app-server `thread/resume -> turn/start` 自动投递到绑定 thread,同时在 `relays/` 留 payload/status/log;只有自动投递被关闭、明确失败或长时间未启动时,才追加 `agent-foreground-relay` 待发送项,由当前 Agent 用 `openprd canvas . --bridge-outbox --claim next` 领取后调用线程工具发送,并用 `--bridge-sent` 或 `--bridge-failed` 回写状态。浏览器网页不能直接调用 Codex Agent 工具;OpenPrd 不提供自己的生图 API,AI 生图仍由 Codex 原生 Image 2/imagegen 完成。不带 `--daemon` 是前台调试模式,会随当前命令结束而停止;停止后台服务使用 `openprd canvas . --thread <id> --stop`。',
|
|
752
754
|
'- Codex 画布协同模式:用户可在画布拖动空白卡片,Agent 可把 Image 2 生成图或本地图片通过本地 canvas service 推入画布并替换选中卡片;用户圈选、标注或选择元素后,可导出图片到该 session 的 `exports/`,并由默认 app-server relay 把文本和本地图片路径提交到绑定线程;Agent 前台接力队列只作为自动投递不可用时的兜底。OpenPrd 不提供自己的生图 API,AI 生图仍由 Codex 原生 `imagegen` / Image 2 完成。',
|
|
753
755
|
'- 画布评审协同:画布右栏顶部“评审协同”区显示当前 7 步流程位置(读取 `/api/guide`),“导入评审方向”按钮会把当前 review 的 designDirections 交互方向连图带说明摆上画布(读取 `/api/review-directions`,已选定方向会标出);用户可直接在方向图上圈选、批注,再用“发送给 Codex”把反馈回传当前对话。引导用户对交互稿做视觉级反馈时优先走这条路径。',
|
|
756
|
+
'- 画布结构化协同接口:Agent 不要只靠截图猜画布状态。要知道用户正在圈哪些元素时用 `GET /api/selection` 读取选中元素的 ID、类型、坐标、尺寸、文本和整体 bounds;要把新图放到指定元素旁边时用 `POST /api/insert-image` 带 `anchorElementId` + `placement`(right/left/below/above),不要覆盖原图;回填 AI 生图占位卡仍用 `POST /api/images` 带 `placeholderId`。',
|
|
757
|
+
'- 画布标注改图闭环:handoff 的 `selection` 为 `annotation-edit` 或带 `sourceImageElementIds` / `annotationElementIds` 时,说明用户在原图上画了标注要求改图。把截图里的标注当修改指令,生成修改后的新图,再用 `POST /api/insert-image` 以原图为 anchor 放到旁边,方便用户对比原图与新版并继续下一轮标注迭代。handoff 或占位卡带 `sizeContract`(width/height/aspectRatio)时,生成图片必须按该宽高比出图,避免回填变形。',
|
|
754
758
|
'- `openprd change . --generate --change <id>`:把 PRD 转成 change。',
|
|
755
759
|
'- `openprd change . --validate --change <id>`:校验 change 结构。',
|
|
756
760
|
'- `openprd tasks . --change <id>`:查看当前 dependency-ready 任务。',
|