@openprd/cli 0.1.19 → 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.
Files changed (102) hide show
  1. package/.openprd/changes/openprd-control-plane-v020/.openprd.yaml +2 -0
  2. package/.openprd/changes/openprd-control-plane-v020/design.md +78 -0
  3. package/.openprd/changes/openprd-control-plane-v020/proposal.md +54 -0
  4. package/.openprd/changes/openprd-control-plane-v020/specs/agent-requirements/spec.md +16 -0
  5. package/.openprd/changes/openprd-control-plane-v020/task-events.jsonl +27 -0
  6. package/.openprd/changes/openprd-control-plane-v020/tasks-002.md +35 -0
  7. package/.openprd/changes/openprd-control-plane-v020/tasks.md +427 -0
  8. package/.openprd/changes/remove-session-tracking-humane-approval/.openprd.yaml +2 -0
  9. package/.openprd/changes/remove-session-tracking-humane-approval/design.md +52 -0
  10. package/.openprd/changes/remove-session-tracking-humane-approval/proposal.md +35 -0
  11. package/.openprd/changes/remove-session-tracking-humane-approval/specs/agent-requirements/spec.md +16 -0
  12. package/.openprd/changes/remove-session-tracking-humane-approval/task-events.jsonl +1 -0
  13. package/.openprd/changes/remove-session-tracking-humane-approval/tasks.md +170 -0
  14. package/.openprd/design/active/asset-spec.md +19 -14
  15. package/.openprd/design/active/direction-plan.md +19 -3
  16. package/.openprd/design/active/facts-sheet.md +16 -7
  17. package/.openprd/design/active/image-preflight.md +6 -5
  18. package/.openprd/design/active/review-studio-v020-directions/compare-plan.json +34 -0
  19. package/.openprd/design/active/review-studio-v020-directions/contact-sheet.jpg +0 -0
  20. package/.openprd/design/active/review-studio-v020-directions/crops/01.png +0 -0
  21. package/.openprd/design/active/review-studio-v020-directions/crops/02.png +0 -0
  22. package/.openprd/design/active/review-studio-v020-directions/crops/03.png +0 -0
  23. package/.openprd/design/active/review-studio-v020-directions/focus-board.template.json +69 -0
  24. package/.openprd/design/active/review-studio-v020-directions/parallel-board.template.json +45 -0
  25. package/.openprd/design/active/review-studio-v020-directions/reference-set.json +143 -0
  26. package/.openprd/design/active/review-studio-v020-directions/source.png +0 -0
  27. package/.openprd/design/active/selected-direction.md +23 -9
  28. package/.openprd/engagements/active/control-plane-architecture.json +203 -0
  29. package/.openprd/engagements/active/control-plane-intake.json +418 -0
  30. package/.openprd/engagements/active/prd.md +183 -119
  31. package/.openprd/engagements/active/review-presentation-v0018.json +176 -0
  32. package/.openprd/i18n-config.json +12 -0
  33. package/.openprd/ledger/events.jsonl +6 -0
  34. package/.openprd/ledger/heads/ebea1a71a9daa566f1c91b53.json +10 -0
  35. package/.openprd/manifest.json +21 -0
  36. package/AGENTS.md +1 -0
  37. package/README.md +2 -0
  38. package/README_EN.md +2 -0
  39. package/package.json +1 -1
  40. package/skills/openprd-frontend-design/SKILL.md +16 -0
  41. package/skills/openprd-harness/SKILL.md +1 -1
  42. package/skills/openprd-quality/SKILL.md +1 -1
  43. package/skills/openprd-requirement-intake/SKILL.md +1 -1
  44. package/skills/openprd-shared/SKILL.md +1 -1
  45. package/src/adapters/adapter-spi.js +193 -0
  46. package/src/adapters/capability-envelope.js +98 -0
  47. package/src/adapters/event-normalizer.js +55 -0
  48. package/src/adapters/index.js +4 -0
  49. package/src/adapters/install-safety.js +249 -0
  50. package/src/agent-canonical-content.js +4 -2
  51. package/src/agent-integration.js +147 -42
  52. package/src/cli/args.js +63 -4
  53. package/src/cli/gate-print.js +17 -0
  54. package/src/cli/quality-commands.js +18 -0
  55. package/src/cli/quality-print.js +10 -0
  56. package/src/cli/runtime-print.js +24 -0
  57. package/src/codex-hook-runner-template.mjs +65 -77
  58. package/src/codex-runtime.js +48 -5
  59. package/src/context/cache.js +245 -0
  60. package/src/context/compiler.js +438 -0
  61. package/src/context/constants.js +30 -0
  62. package/src/context/index.js +39 -0
  63. package/src/context/redaction.js +84 -0
  64. package/src/context/stable.js +69 -0
  65. package/src/context/telemetry.js +42 -0
  66. package/src/dev-standards.js +57 -0
  67. package/src/fleet.js +112 -95
  68. package/src/gates/index.js +2 -0
  69. package/src/gates/scoped-gates.js +256 -0
  70. package/src/gates/store.js +126 -0
  71. package/src/gates/workspace.js +41 -0
  72. package/src/html-artifacts.js +725 -28
  73. package/src/kernel/atomic-store.js +299 -0
  74. package/src/kernel/event-envelope.js +166 -0
  75. package/src/kernel/index.js +4 -0
  76. package/src/kernel/project-ledger.js +467 -0
  77. package/src/kernel/project-manifest.js +205 -0
  78. package/src/knowledge-v3/index.js +1 -0
  79. package/src/knowledge-v3/lifecycle.js +290 -0
  80. package/src/knowledge.js +14 -7
  81. package/src/openprd.js +71 -2
  82. package/src/review-model.js +413 -0
  83. package/src/review-presentation.js +1 -1
  84. package/src/run-harness.js +431 -37
  85. package/src/runtime/cli_runtime_README.md +28 -0
  86. package/src/runtime/errors.js +66 -0
  87. package/src/runtime/index.js +44 -0
  88. package/src/runtime/lane-schema.js +141 -0
  89. package/src/runtime/lane-store.js +279 -0
  90. package/src/runtime/task-runtime.js +449 -0
  91. package/src/runtime/workspace.js +179 -0
  92. package/src/runtime/write-set.js +206 -0
  93. package/src/session-binding.js +16 -3
  94. package/src/session-registry.js +59 -1
  95. package/src/upgrade/fleet-mutation.js +166 -0
  96. package/src/upgrade/fleet-transaction.js +398 -0
  97. package/src/upgrade/transaction-store.js +416 -0
  98. package/src/visual-compare-core.js +66 -27
  99. package/src/visual-compare.js +18 -12
  100. package/src/workspace-core.js +109 -7
  101. package/src/workspace-registry.js +39 -1
  102. 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` 生成“对齐辅助线证据板”,且同时覆盖容器轨道和内部内容槽位轨道;当单个 logo、icon、avatar、badge、按钮图形或图片内部需要居中判定、视觉重心评估或用户反馈偏心时,必须改用 `--board` 生成“内部居中证据板”,用红色画布中心线、绿色主体外接框和黄色视觉重心点展示偏移。视觉证据不仅检查位置和结构,也要检查选定气质、层级、字体/色彩/动效/表面角色和记忆点是否保住。当参考图是一张整板、网格图、多对象或多子图组合时,先运行 `openprd visual-prepare` 生成 reference-set、contact sheet 和 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
  '',