@openprd/cli 0.1.11 → 0.1.18
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/adaptive-project-framing/.openprd.yaml +2 -0
- package/.openprd/changes/adaptive-project-framing/design.md +47 -0
- package/.openprd/changes/adaptive-project-framing/proposal.md +35 -0
- package/.openprd/changes/adaptive-project-framing/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/adaptive-project-framing/task-events.jsonl +20 -0
- package/.openprd/changes/adaptive-project-framing/tasks.md +340 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/.openprd.yaml +2 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/design.md +34 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/proposal.md +23 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/task-events.jsonl +14 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/tasks.md +238 -0
- package/.openprd/changes/loop-isolated-worktree-commit/.openprd.yaml +2 -0
- package/.openprd/changes/loop-isolated-worktree-commit/design.md +53 -0
- package/.openprd/changes/loop-isolated-worktree-commit/proposal.md +38 -0
- package/.openprd/changes/loop-isolated-worktree-commit/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/loop-isolated-worktree-commit/task-events.jsonl +22 -0
- package/.openprd/changes/loop-isolated-worktree-commit/tasks.md +357 -0
- package/.openprd/changes/openprd/.openprd.yaml +2 -0
- package/.openprd/changes/openprd/design.md +46 -0
- package/.openprd/changes/openprd/proposal.md +36 -0
- package/.openprd/changes/openprd/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/openprd/task-events.jsonl +23 -0
- package/.openprd/changes/openprd/tasks.md +242 -0
- package/.openprd/changes/pm-human-in-the-loop/.openprd.yaml +2 -0
- package/.openprd/changes/pm-human-in-the-loop/design.md +38 -0
- package/.openprd/changes/pm-human-in-the-loop/proposal.md +29 -0
- package/.openprd/changes/pm-human-in-the-loop/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/pm-human-in-the-loop/task-events.jsonl +18 -0
- package/.openprd/changes/pm-human-in-the-loop/tasks.md +306 -0
- package/.openprd/changes/test-strategy-router/.openprd.yaml +2 -0
- package/.openprd/changes/test-strategy-router/design.md +65 -0
- package/.openprd/changes/test-strategy-router/proposal.md +45 -0
- package/.openprd/changes/test-strategy-router/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/test-strategy-router/task-events.jsonl +1 -0
- package/.openprd/changes/test-strategy-router/tasks.md +132 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/.openprd.yaml +2 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/design.md +28 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/proposal.md +23 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/task-events.jsonl +11 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/tasks.md +66 -0
- package/.openprd/changes/vibe-coding-dev-check/.openprd.yaml +2 -0
- package/.openprd/changes/vibe-coding-dev-check/design.md +37 -0
- package/.openprd/changes/vibe-coding-dev-check/proposal.md +28 -0
- package/.openprd/changes/vibe-coding-dev-check/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/vibe-coding-dev-check/task-events.jsonl +18 -0
- package/.openprd/changes/vibe-coding-dev-check/tasks.md +306 -0
- package/.openprd/design/README.md +11 -6
- package/.openprd/design/README_EN.md +4 -1
- package/.openprd/design/active/asset-spec.md +6 -0
- package/.openprd/design/active/direction-plan.md +5 -5
- package/.openprd/design/active/selected-direction.md +2 -0
- package/.openprd/design/anti-slop.md +26 -1
- package/.openprd/design/assets/surface-presets.md +6 -0
- package/.openprd/design/checklists/ui-quality-gate.md +7 -0
- package/.openprd/design/lenses/frontend-lenses.md +28 -0
- package/.openprd/design/recipes/content-experience.md +8 -1
- package/.openprd/design/recipes/operational-tool.md +7 -0
- package/.openprd/design/recipes/product-launch.md +8 -1
- package/.openprd/design/templates/README.md +6 -3
- package/.openprd/design/templates/README_EN.md +6 -3
- package/.openprd/design/templates/ops-dashboard.html +8 -1
- package/.openprd/design/templates/product-launch.html +2 -1
- package/.openprd/design/themes/theme-catalog.json +23 -5
- package/.openprd/engagements/active/prd.md +103 -100
- package/.openprd/standards/config.json +7 -1
- package/AGENTS.md +13 -3
- package/README.md +107 -14
- package/README_EN.md +131 -13
- package/package.json +1 -1
- package/scripts/dev-check-wrapup-copy.mjs +46 -21
- package/skills/openprd-diagram-review/SKILL.md +27 -2
- package/skills/openprd-diagram-review/references/explanation-svg-patterns.md +131 -0
- package/skills/openprd-discovery-loop/SKILL.md +2 -2
- package/skills/openprd-frontend-design/SKILL.md +44 -20
- package/skills/openprd-frontend-design/references/design-asset-contract.md +6 -0
- package/skills/openprd-frontend-design/references/direction-engine.md +29 -4
- package/skills/openprd-harness/SKILL.md +22 -16
- package/skills/openprd-quality/SKILL.md +10 -8
- package/skills/openprd-requirement-intake/SKILL.md +2 -0
- package/skills/openprd-router/SKILL.md +2 -1
- package/skills/openprd-shared/SKILL.md +26 -16
- package/skills/openprd-standards/SKILL.md +4 -2
- package/src/agent-canonical-content.js +808 -0
- package/src/agent-integration.js +258 -777
- package/src/canvas-app.html.js +2910 -0
- package/src/canvas-i18n.js +274 -0
- package/src/canvas-workspace.js +2271 -0
- package/src/cli/args.js +72 -4
- package/src/cli/basic-print.js +3 -0
- package/src/cli/canvas-print.js +106 -0
- package/src/cli/doctor-print.js +20 -0
- package/src/cli/print.js +2 -0
- package/src/cli/quality-commands.js +222 -0
- package/src/cli/quality-print.js +28 -2
- package/src/cli/run-print.js +5 -0
- package/src/cli/workflow-print.js +3 -0
- package/src/codex-app-server-relay.js +251 -0
- package/src/codex-hook-runner-template.mjs +957 -59
- package/src/design-starter-support.js +2 -2
- package/src/design-starter.js +39 -3
- package/src/dev-standards.js +125 -10
- package/src/diagram-core.js +6 -20
- package/src/discovery.js +2 -8
- package/src/docs-compact.js +125 -0
- package/src/execution-strategy.js +1 -1
- package/src/fleet.js +9 -3
- package/src/guide.js +227 -0
- package/src/html-artifacts.js +428 -7
- package/src/knowledge.js +45 -11
- package/src/language-policy.js +63 -3
- package/src/learning-review.js +1 -1
- package/src/loop.js +1 -1
- package/src/openprd.js +68 -131
- package/src/openspec/change-validate.js +2 -2
- package/src/openspec/constants.js +6 -7
- package/src/openspec/execute.js +2 -2
- package/src/openspec/generate.js +318 -102
- package/src/openspec/migration.js +168 -0
- package/src/openspec/paths.js +1 -22
- package/src/prd-core.js +1 -1
- package/src/quality-visual-review.js +257 -14
- package/src/quality.js +46 -8
- package/src/review-presentation.js +60 -0
- package/src/run-harness.js +41 -3
- package/src/session-evidence.js +363 -0
- package/src/standards.js +64 -0
- package/src/test-strategy.js +7 -4
- package/src/visual-compare-centering.js +622 -0
- package/src/visual-compare-core.js +1123 -0
- package/src/visual-compare.js +620 -698
- package/src/workspace-core.js +94 -4
- package/src/workspace-workflow.js +46 -1
|
@@ -80,6 +80,37 @@ function ensureHarness(root) {
|
|
|
80
80
|
if (!fs.existsSync(eventsPath)) {
|
|
81
81
|
fs.writeFileSync(eventsPath, '');
|
|
82
82
|
}
|
|
83
|
+
const runtimePath = path.join(dir, 'runtime-environment.json');
|
|
84
|
+
if (!fs.existsSync(runtimePath)) {
|
|
85
|
+
writeJsonSync(runtimePath, {
|
|
86
|
+
version: 1,
|
|
87
|
+
active: {
|
|
88
|
+
activeClient: 'unknown',
|
|
89
|
+
surface: 'unknown',
|
|
90
|
+
executionMode: 'interactive',
|
|
91
|
+
confidence: 0,
|
|
92
|
+
sessionId: null,
|
|
93
|
+
turnId: null,
|
|
94
|
+
agentType: null,
|
|
95
|
+
model: null,
|
|
96
|
+
evidence: [],
|
|
97
|
+
observedCapabilities: [],
|
|
98
|
+
},
|
|
99
|
+
runtimeDetection: {
|
|
100
|
+
version: 1,
|
|
101
|
+
activeEvidencePriority: [
|
|
102
|
+
'hook-session-payload',
|
|
103
|
+
'explicit-launcher',
|
|
104
|
+
'sdk-or-headless-session',
|
|
105
|
+
'child-process-env',
|
|
106
|
+
'config-or-cli-probe',
|
|
107
|
+
'agent-self-report',
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
platformCapabilityPacks: [],
|
|
111
|
+
updatedAt: null,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
83
114
|
}
|
|
84
115
|
|
|
85
116
|
function readJsonSync(filePath, fallback) {
|
|
@@ -100,6 +131,98 @@ function appendEvent(root, event) {
|
|
|
100
131
|
fs.appendFileSync(path.join(harnessDir(root), 'events.jsonl'), JSON.stringify({ at: now(), ...event }) + '\n');
|
|
101
132
|
}
|
|
102
133
|
|
|
134
|
+
function runtimeEnvironmentPath(root) {
|
|
135
|
+
return path.join(harnessDir(root), 'runtime-environment.json');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function compactString(value) {
|
|
139
|
+
const text = String(value ?? '').trim();
|
|
140
|
+
return text || null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function runtimeEvidenceForPayload(eventName, payload) {
|
|
144
|
+
const evidence = ['hook.event_name'];
|
|
145
|
+
const addIfPresent = (key, label = key) => {
|
|
146
|
+
if (payload && payload[key] !== undefined && payload[key] !== null && String(payload[key]).trim() !== '') {
|
|
147
|
+
evidence.push(`hook.${label}`);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
addIfPresent('session_id', 'session_id');
|
|
151
|
+
addIfPresent('sessionId', 'sessionId');
|
|
152
|
+
addIfPresent('transcript_path', 'transcript_path');
|
|
153
|
+
addIfPresent('transcriptPath', 'transcriptPath');
|
|
154
|
+
addIfPresent('cwd', 'cwd');
|
|
155
|
+
addIfPresent('model', 'model');
|
|
156
|
+
addIfPresent('turn_id', 'turn_id');
|
|
157
|
+
addIfPresent('turnId', 'turnId');
|
|
158
|
+
addIfPresent('agent_type', 'agent_type');
|
|
159
|
+
addIfPresent('agentType', 'agentType');
|
|
160
|
+
addIfPresent('tool_name', 'tool_name');
|
|
161
|
+
addIfPresent('toolName', 'toolName');
|
|
162
|
+
if (eventName) {
|
|
163
|
+
evidence.push(`event.${eventName}`);
|
|
164
|
+
}
|
|
165
|
+
return [...new Set(evidence)];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function observedCodexCapabilities(payload) {
|
|
169
|
+
const observed = ['codex-hooks'];
|
|
170
|
+
const tool = toolName(payload).toLowerCase();
|
|
171
|
+
const haystack = `${tool}\n${payloadText(payload)}`;
|
|
172
|
+
if (/\b(imagegen|image_gen|image-generation|image_generation)\b/i.test(haystack)) {
|
|
173
|
+
observed.push('native-image-generation');
|
|
174
|
+
}
|
|
175
|
+
if (/\b(computer-use|computer_use|browser|chrome)\b/i.test(haystack)) {
|
|
176
|
+
observed.push('computer-use');
|
|
177
|
+
}
|
|
178
|
+
return [...new Set(observed)];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function recordRuntimeEnvironment(root, eventName, payload, sessionId) {
|
|
182
|
+
ensureHarness(root);
|
|
183
|
+
const current = readJsonSync(runtimeEnvironmentPath(root), {});
|
|
184
|
+
const executionSignals = detectExecutionSignals(eventName, payload);
|
|
185
|
+
const evidence = [...new Set([
|
|
186
|
+
...runtimeEvidenceForPayload(eventName, payload),
|
|
187
|
+
...executionSignals.evidence,
|
|
188
|
+
...executionSignals.optInEvidence,
|
|
189
|
+
])];
|
|
190
|
+
const active = {
|
|
191
|
+
activeClient: 'codex',
|
|
192
|
+
surface: 'codex-hook',
|
|
193
|
+
executionMode: executionSignals.executionMode,
|
|
194
|
+
confidence: evidence.some((item) => item === 'hook.session_id' || item === 'hook.sessionId' || item === 'hook.transcript_path' || item === 'hook.transcriptPath') ? 0.95 : 0.9,
|
|
195
|
+
sessionId: sessionId ?? null,
|
|
196
|
+
turnId: compactString(payload?.turn_id ?? payload?.turnId),
|
|
197
|
+
agentType: compactString(payload?.agent_type ?? payload?.agentType),
|
|
198
|
+
model: compactString(payload?.model),
|
|
199
|
+
evidence,
|
|
200
|
+
observedCapabilities: observedCodexCapabilities(payload),
|
|
201
|
+
};
|
|
202
|
+
const entry = {
|
|
203
|
+
version: 1,
|
|
204
|
+
...current,
|
|
205
|
+
active,
|
|
206
|
+
lastEventName: eventName,
|
|
207
|
+
updatedAt: now(),
|
|
208
|
+
recentObservations: [
|
|
209
|
+
...(Array.isArray(current.recentObservations) ? current.recentObservations : []),
|
|
210
|
+
{
|
|
211
|
+
at: now(),
|
|
212
|
+
eventName,
|
|
213
|
+
activeClient: active.activeClient,
|
|
214
|
+
surface: active.surface,
|
|
215
|
+
executionMode: active.executionMode,
|
|
216
|
+
sessionId: active.sessionId,
|
|
217
|
+
evidence: active.evidence,
|
|
218
|
+
observedCapabilities: active.observedCapabilities,
|
|
219
|
+
},
|
|
220
|
+
].slice(-20),
|
|
221
|
+
};
|
|
222
|
+
writeJsonSync(runtimeEnvironmentPath(root), entry);
|
|
223
|
+
return entry;
|
|
224
|
+
}
|
|
225
|
+
|
|
103
226
|
function turnStatePath(root) {
|
|
104
227
|
return path.join(harnessDir(root), 'turn-state.json');
|
|
105
228
|
}
|
|
@@ -117,6 +240,7 @@ function defaultTurnState() {
|
|
|
117
240
|
startedAt: null,
|
|
118
241
|
updatedAt: null,
|
|
119
242
|
touchedFiles: [],
|
|
243
|
+
gateBlockCounts: {},
|
|
120
244
|
reviewSignals: [],
|
|
121
245
|
runtimeEvents: [],
|
|
122
246
|
timeline: [],
|
|
@@ -132,6 +256,28 @@ function readTurnState(root) {
|
|
|
132
256
|
};
|
|
133
257
|
}
|
|
134
258
|
|
|
259
|
+
// 流程型门禁(需求入口、外部证据、设计预检、Patch Mode)在同一轮内
|
|
260
|
+
// 反复阻断超过该次数时自动熔断降级为提醒放行,避免 Agent 把整轮消耗在
|
|
261
|
+
// 与门禁系统搏斗上;安全型门禁(云端写入、业务写死、secrets)不参与熔断。
|
|
262
|
+
const PROCESS_GATE_FUSE_LIMIT = 3;
|
|
263
|
+
|
|
264
|
+
function bumpProcessGateBlockCount(root, gateKind) {
|
|
265
|
+
const turnState = readTurnState(root);
|
|
266
|
+
const counts = { ...(turnState.gateBlockCounts || {}) };
|
|
267
|
+
counts[gateKind] = Number(counts[gateKind] || 0) + 1;
|
|
268
|
+
writeTurnState(root, { ...turnState, gateBlockCounts: counts });
|
|
269
|
+
return counts[gateKind];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function processGateFuseMessage(gateKind, streak, originalReason) {
|
|
273
|
+
return [
|
|
274
|
+
`OpenPrd 门禁熔断: ${gateKind} 门禁本轮已阻断 ${streak - 1} 次,本次自动降级为提醒放行。`,
|
|
275
|
+
'继续按用户已确认的方向完成实际工作,不要再花动作调试门禁本身,也不要再向用户重复索取同一轮确认。',
|
|
276
|
+
'收口时补齐这个门禁原本要求的产物(例如需求确认稿、外部证据或设计合同),并在最终回复里说明本轮发生过门禁熔断降级。',
|
|
277
|
+
originalReason ? `原始门禁提示(供收口对照):\n${originalReason}` : null,
|
|
278
|
+
].filter(Boolean).join('\n');
|
|
279
|
+
}
|
|
280
|
+
|
|
135
281
|
function writeTurnState(root, next) {
|
|
136
282
|
writeJsonSync(turnStatePath(root), {
|
|
137
283
|
...defaultTurnState(),
|
|
@@ -200,6 +346,7 @@ function beginTurnReview(root, baseEvent, prompt) {
|
|
|
200
346
|
status: 'needs-attention',
|
|
201
347
|
startedAt: now(),
|
|
202
348
|
touchedFiles: [],
|
|
349
|
+
gateBlockCounts: {},
|
|
203
350
|
reviewSignals: [],
|
|
204
351
|
runtimeEvents: promptPreview ? [{
|
|
205
352
|
eventName: 'user-prompt',
|
|
@@ -231,6 +378,96 @@ function recordTouchedFiles(root, payload) {
|
|
|
231
378
|
return touchedFiles;
|
|
232
379
|
}
|
|
233
380
|
|
|
381
|
+
function appendTurnReviewSignals(root, signals) {
|
|
382
|
+
const normalizedSignals = (Array.isArray(signals) ? signals : [signals])
|
|
383
|
+
.filter(Boolean)
|
|
384
|
+
.map((signal) => ({
|
|
385
|
+
at: now(),
|
|
386
|
+
...signal,
|
|
387
|
+
}));
|
|
388
|
+
if (normalizedSignals.length === 0) {
|
|
389
|
+
return [];
|
|
390
|
+
}
|
|
391
|
+
const state = readTurnState(root);
|
|
392
|
+
const existing = Array.isArray(state.reviewSignals) ? state.reviewSignals : [];
|
|
393
|
+
writeTurnState(root, {
|
|
394
|
+
...state,
|
|
395
|
+
reviewSignals: [...existing, ...normalizedSignals].slice(-80),
|
|
396
|
+
timeline: [
|
|
397
|
+
...(Array.isArray(state.timeline) ? state.timeline : []),
|
|
398
|
+
...normalizedSignals.map((signal) => ({
|
|
399
|
+
event: signal.kind,
|
|
400
|
+
status: 'pass',
|
|
401
|
+
message: signal.message ?? signal.preview ?? signal.kind,
|
|
402
|
+
at: signal.at,
|
|
403
|
+
})),
|
|
404
|
+
].slice(-120),
|
|
405
|
+
});
|
|
406
|
+
return normalizedSignals;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function detectVisualEvidenceSignals(payload) {
|
|
410
|
+
const tool = toolName(payload).toLowerCase();
|
|
411
|
+
const command = commandText(payload);
|
|
412
|
+
const text = payloadText(payload);
|
|
413
|
+
const haystack = `${tool}\n${command}\n${text}`;
|
|
414
|
+
const signals = [];
|
|
415
|
+
if (/openprd\s+visual-prepare\b/i.test(haystack)) {
|
|
416
|
+
signals.push({
|
|
417
|
+
kind: 'visual-reference-prepared',
|
|
418
|
+
message: 'visual-prepare reference-set/contact sheet observed',
|
|
419
|
+
preview: preview(command || text, 180),
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
if (/openprd\s+visual-compare\b/i.test(haystack)
|
|
423
|
+
|| /\.openprd\/harness\/visual-reviews\/(?:visual-|reference-sets\/)/i.test(haystack)
|
|
424
|
+
|| /visual-(?:compare|before-after|focus-board|parallel-board|verification-board|alignment-board|centering-board)/i.test(haystack)
|
|
425
|
+
|| /\b(?:reference-actual|before-after|focus-board|parallel-board|verification-board|alignment-board|centering-board|center-board)\b/i.test(haystack)) {
|
|
426
|
+
signals.push({
|
|
427
|
+
kind: 'visual-review-artifact',
|
|
428
|
+
message: 'visual-compare artifact observed',
|
|
429
|
+
preview: preview(command || text, 180),
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
if (/\bverification-board\b|截图实测证据板|visual-verification-board/i.test(haystack)) {
|
|
433
|
+
signals.push({
|
|
434
|
+
kind: 'visual-verification-board',
|
|
435
|
+
message: 'verification-board artifact observed',
|
|
436
|
+
preview: preview(command || text, 180),
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
if (/\balignment-board\b|对齐辅助线证据板|visual-alignment-board/i.test(haystack)) {
|
|
440
|
+
signals.push({
|
|
441
|
+
kind: 'visual-alignment-board',
|
|
442
|
+
message: 'alignment-board artifact observed',
|
|
443
|
+
preview: preview(command || text, 180),
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
if (/\b(?:centering-board|center-board)\b|内部居中证据板|visual-centering-board/i.test(haystack)) {
|
|
447
|
+
signals.push({
|
|
448
|
+
kind: 'visual-centering-board',
|
|
449
|
+
message: 'centering-board artifact observed',
|
|
450
|
+
preview: preview(command || text, 180),
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
const screenshotTool = /(?:^|[_-])(?:screenshot|view_image)(?:$|[_-])/i.test(tool)
|
|
454
|
+
|| /(?:computer|browser|playwright|page|mp|miniprogram|weapp).*screenshot/i.test(tool);
|
|
455
|
+
const screenshotCommand = /\bscreencapture\b|page\.screenshot|\.screenshot\(|mp_screenshot|browser.*screenshot|computer.*screenshot|playwright.*screenshot/i.test(command);
|
|
456
|
+
const screenshotText = /(?:普通截图|实测截图|运行态截图|Computer\s*实测|Browser\s*实测|截图证据|screenshot captured|captured screenshot)/i.test(text);
|
|
457
|
+
if ((screenshotTool || screenshotCommand || screenshotText) && !/openprd\s+visual-compare\b/i.test(haystack)) {
|
|
458
|
+
signals.push({
|
|
459
|
+
kind: 'visual-raw-screenshot',
|
|
460
|
+
message: 'raw screenshot or Computer/Browser visual evidence observed',
|
|
461
|
+
preview: preview(command || text || tool, 180),
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
return signals;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function recordVisualEvidenceSignals(root, payload) {
|
|
468
|
+
return appendTurnReviewSignals(root, detectVisualEvidenceSignals(payload));
|
|
469
|
+
}
|
|
470
|
+
|
|
234
471
|
function updateHookState(root, event) {
|
|
235
472
|
ensureHarness(root);
|
|
236
473
|
const statePath = path.join(harnessDir(root), 'hook-state.json');
|
|
@@ -248,6 +485,9 @@ function updateHookState(root, event) {
|
|
|
248
485
|
if (event.fingerprint) {
|
|
249
486
|
state.recentFingerprints[event.fingerprint] = Date.now();
|
|
250
487
|
}
|
|
488
|
+
if (event.runtimeEnvironment) {
|
|
489
|
+
state.runtimeEnvironment = event.runtimeEnvironment;
|
|
490
|
+
}
|
|
251
491
|
for (const [fingerprint, seenAt] of Object.entries(state.recentFingerprints)) {
|
|
252
492
|
if (Date.now() - Number(seenAt) > 300000) {
|
|
253
493
|
delete state.recentFingerprints[fingerprint];
|
|
@@ -390,6 +630,203 @@ function sessionRegistryPath() {
|
|
|
390
630
|
return path.join(openprdHome(), 'registry', 'sessions.jsonl');
|
|
391
631
|
}
|
|
392
632
|
|
|
633
|
+
function truthyFlag(value) {
|
|
634
|
+
return /^(1|true|yes|on)$/i.test(String(value ?? '').trim());
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function normalizedMode(value) {
|
|
638
|
+
return String(value ?? '').trim().toLowerCase();
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function compactPayloadField(payload, keys) {
|
|
642
|
+
for (const key of keys) {
|
|
643
|
+
const value = payload?.[key];
|
|
644
|
+
if (value === undefined || value === null || value === false) {
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
const text = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
648
|
+
if (text.trim()) {
|
|
649
|
+
return { key, value: text.trim() };
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function automationPromptHeader(prompt) {
|
|
656
|
+
const header = String(prompt || '').split(/\r?\n/).slice(0, 12).join('\n');
|
|
657
|
+
if (/^\s*Automation\s*:/im.test(header) && /^\s*Automation\s+(ID|memory)\s*:/im.test(header)) {
|
|
658
|
+
return 'prompt.automation-header';
|
|
659
|
+
}
|
|
660
|
+
if (/^\s*(Scheduled|Cron|Unattended)\s+(task|automation)\s*:/im.test(header)) {
|
|
661
|
+
return 'prompt.scheduled-header';
|
|
662
|
+
}
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function explicitOpenPrdOptInEvidence(payload) {
|
|
667
|
+
const evidence = [];
|
|
668
|
+
const mode = normalizedMode(process.env.OPENPRD_EXECUTION_MODE);
|
|
669
|
+
if (['openprd', 'openprd-maintenance', 'maintenance'].includes(mode)) {
|
|
670
|
+
evidence.push(`env.OPENPRD_EXECUTION_MODE=${mode}`);
|
|
671
|
+
}
|
|
672
|
+
if (truthyFlag(process.env.OPENPRD_AUTOMATION_OPT_IN) || truthyFlag(process.env.OPENPRD_ENABLE_IN_AUTOMATION)) {
|
|
673
|
+
evidence.push('env.OPENPRD_AUTOMATION_OPT_IN');
|
|
674
|
+
}
|
|
675
|
+
const explicitField = compactPayloadField(payload, [
|
|
676
|
+
'openprd_automation_opt_in',
|
|
677
|
+
'openprdAutomationOptIn',
|
|
678
|
+
'openprd_opt_in',
|
|
679
|
+
'openprdOptIn',
|
|
680
|
+
'enable_openprd',
|
|
681
|
+
'enableOpenPrd',
|
|
682
|
+
]);
|
|
683
|
+
if (explicitField) {
|
|
684
|
+
evidence.push(`payload.${explicitField.key}`);
|
|
685
|
+
}
|
|
686
|
+
const text = `${promptText(payload)}\n${payloadText(payload)}`;
|
|
687
|
+
const negative = /(不要|不需要|无需|禁止|别|不是|非|不要执行|不要调用|do\s+not|don't|never|without|no|not)\s+.{0,24}\bopen\s*prd\b/i
|
|
688
|
+
.test(text)
|
|
689
|
+
|| /(不要|不需要|无需|禁止|别|不是|非).{0,24}OpenPrd/i.test(text);
|
|
690
|
+
if (!negative && /(OpenPrd\s*(维护任务|maintenance|maintain|opt[- ]?in)|显式启用\s*OpenPrd|启用\s*OpenPrd|使用\s*OpenPrd|运行\s*OpenPrd|enable\s+OpenPrd|use\s+OpenPrd|openprd-maintenance)/i.test(text)) {
|
|
691
|
+
evidence.push('prompt.openprd-opt-in');
|
|
692
|
+
}
|
|
693
|
+
return [...new Set(evidence)];
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function detectExecutionSignals(eventName, payload) {
|
|
697
|
+
const evidence = [];
|
|
698
|
+
const mode = normalizedMode(process.env.OPENPRD_EXECUTION_MODE);
|
|
699
|
+
if (['automation', 'cron', 'scheduled', 'headless', 'unattended'].includes(mode)) {
|
|
700
|
+
evidence.push(`env.OPENPRD_EXECUTION_MODE=${mode}`);
|
|
701
|
+
}
|
|
702
|
+
if (truthyFlag(process.env.OPENPRD_AUTOMATION)) {
|
|
703
|
+
evidence.push('env.OPENPRD_AUTOMATION');
|
|
704
|
+
}
|
|
705
|
+
if (compactString(process.env.CODEX_AUTOMATION_ID)) {
|
|
706
|
+
evidence.push('env.CODEX_AUTOMATION_ID');
|
|
707
|
+
}
|
|
708
|
+
if (truthyFlag(process.env.CLAUDE_CODE_HEADLESS)) {
|
|
709
|
+
evidence.push('env.CLAUDE_CODE_HEADLESS');
|
|
710
|
+
}
|
|
711
|
+
const field = compactPayloadField(payload, [
|
|
712
|
+
'automation_id',
|
|
713
|
+
'automationId',
|
|
714
|
+
'automation_name',
|
|
715
|
+
'automationName',
|
|
716
|
+
'automation',
|
|
717
|
+
'scheduled_task',
|
|
718
|
+
'scheduledTask',
|
|
719
|
+
'cron',
|
|
720
|
+
'rrule',
|
|
721
|
+
'headless',
|
|
722
|
+
'unattended',
|
|
723
|
+
'is_automation',
|
|
724
|
+
'isAutomation',
|
|
725
|
+
]);
|
|
726
|
+
if (field) {
|
|
727
|
+
evidence.push(`payload.${field.key}`);
|
|
728
|
+
}
|
|
729
|
+
const runMode = compactPayloadField(payload, [
|
|
730
|
+
'run_kind',
|
|
731
|
+
'runKind',
|
|
732
|
+
'execution_mode',
|
|
733
|
+
'executionMode',
|
|
734
|
+
'surface',
|
|
735
|
+
'agent_surface',
|
|
736
|
+
'agentSurface',
|
|
737
|
+
]);
|
|
738
|
+
if (runMode && /(automation|cron|scheduled|headless|unattended)/i.test(runMode.value)) {
|
|
739
|
+
evidence.push(`payload.${runMode.key}=${runMode.value}`);
|
|
740
|
+
}
|
|
741
|
+
const agentType = compactString(payload?.agent_type ?? payload?.agentType);
|
|
742
|
+
if (agentType && /(automation|cron|scheduled|headless|unattended)/i.test(agentType)) {
|
|
743
|
+
evidence.push(`payload.agentType=${agentType}`);
|
|
744
|
+
}
|
|
745
|
+
const promptHeader = automationPromptHeader(promptText(payload));
|
|
746
|
+
if (promptHeader) {
|
|
747
|
+
evidence.push(promptHeader);
|
|
748
|
+
}
|
|
749
|
+
const transcript = compactString(payload?.transcript_path ?? payload?.transcriptPath);
|
|
750
|
+
if (transcript && /(?:^|[/-])(automation|automations|cron|scheduled|headless)(?:[/-]|$)/i.test(transcript)) {
|
|
751
|
+
evidence.push('payload.transcript_path.automation');
|
|
752
|
+
}
|
|
753
|
+
const optInEvidence = explicitOpenPrdOptInEvidence(payload);
|
|
754
|
+
return {
|
|
755
|
+
eventName,
|
|
756
|
+
isAutomation: evidence.length > 0,
|
|
757
|
+
explicitOpenPrdOptIn: optInEvidence.length > 0,
|
|
758
|
+
evidence: [...new Set(evidence)],
|
|
759
|
+
optInEvidence,
|
|
760
|
+
executionMode: optInEvidence.length > 0
|
|
761
|
+
? 'automation-openprd-opt-in'
|
|
762
|
+
: evidence.length > 0
|
|
763
|
+
? 'automation'
|
|
764
|
+
: 'interactive',
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function latestSessionRegistryEntry(root, sessionId) {
|
|
769
|
+
if (!sessionId) {
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
const filePath = sessionRegistryPath();
|
|
773
|
+
if (!fs.existsSync(filePath)) {
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
const workspaceRoot = path.resolve(root);
|
|
777
|
+
let latest = null;
|
|
778
|
+
for (const line of fs.readFileSync(filePath, 'utf8').split(/\r?\n/)) {
|
|
779
|
+
if (!line.trim()) {
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
try {
|
|
783
|
+
const entry = JSON.parse(line);
|
|
784
|
+
if (entry?.sessionId !== sessionId) {
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
const entryRoot = entry?.workspaceRoot ? path.resolve(String(entry.workspaceRoot)) : null;
|
|
788
|
+
if (entryRoot && entryRoot !== workspaceRoot) {
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
latest = entry;
|
|
792
|
+
} catch {}
|
|
793
|
+
}
|
|
794
|
+
return latest;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function rememberAutomationMode(root, sessionId, payload, signals, laneKind) {
|
|
798
|
+
if (!sessionId) {
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
return writeSessionRegistryEntry(root, sessionId, {
|
|
802
|
+
laneKind,
|
|
803
|
+
tool: 'openprd-hook',
|
|
804
|
+
promptPreview: preview(promptText(payload) || payloadText(payload), 500),
|
|
805
|
+
gateActive: false,
|
|
806
|
+
gateStatus: laneKind === 'automation' ? 'automation-safe-mode' : 'openprd-opt-in',
|
|
807
|
+
title: laneKind === 'automation'
|
|
808
|
+
? 'Unattended automation bypassed OpenPrd'
|
|
809
|
+
: 'Unattended automation explicitly enabled OpenPrd',
|
|
810
|
+
executionEvidence: signals.evidence,
|
|
811
|
+
optInEvidence: signals.optInEvidence,
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function automationBypassDecision(root, eventName, payload, sessionId) {
|
|
816
|
+
const signals = detectExecutionSignals(eventName, payload);
|
|
817
|
+
if (signals.explicitOpenPrdOptIn) {
|
|
818
|
+
return { bypass: false, optIn: true, remembered: false, signals };
|
|
819
|
+
}
|
|
820
|
+
if (signals.isAutomation) {
|
|
821
|
+
return { bypass: true, optIn: false, remembered: false, signals };
|
|
822
|
+
}
|
|
823
|
+
const remembered = eventName === 'UserPromptSubmit' ? null : latestSessionRegistryEntry(root, sessionId);
|
|
824
|
+
if (remembered?.laneKind === 'automation' && remembered?.gateStatus === 'automation-safe-mode') {
|
|
825
|
+
return { bypass: true, optIn: false, remembered: true, signals };
|
|
826
|
+
}
|
|
827
|
+
return { bypass: false, optIn: false, remembered: false, signals };
|
|
828
|
+
}
|
|
829
|
+
|
|
393
830
|
function sessionStatePath(root, sessionId = null) {
|
|
394
831
|
if (!sessionId) {
|
|
395
832
|
return null;
|
|
@@ -579,7 +1016,28 @@ function readRequirementLaneReview(root, sessionId = null) {
|
|
|
579
1016
|
artifact: binding.reviewPath || binding.stableReviewArtifact || binding.activeReviewPath || binding.reviewArtifact || null,
|
|
580
1017
|
};
|
|
581
1018
|
}
|
|
582
|
-
|
|
1019
|
+
const globalReview = readCurrentPrdReview(root);
|
|
1020
|
+
if (sessionId && globalReview.versionId) {
|
|
1021
|
+
// 全局 current.json 里的确认稿可能属于另一个会话的历史需求。
|
|
1022
|
+
// 如果 lane 归属明确不是当前会话,就按“本轮还没有确认稿”自愈,
|
|
1023
|
+
// 避免把其他需求的旧稿卡成本轮的待确认死锁。
|
|
1024
|
+
const laneSessionId = String(readJsonSync(currentStatePath(root), {})?.laneSessionId || '').trim() || null;
|
|
1025
|
+
if (laneSessionId && laneSessionId !== sessionId) {
|
|
1026
|
+
return {
|
|
1027
|
+
versionId: null,
|
|
1028
|
+
digest: null,
|
|
1029
|
+
workUnitId: null,
|
|
1030
|
+
status: 'missing',
|
|
1031
|
+
artifact: null,
|
|
1032
|
+
staleReview: {
|
|
1033
|
+
versionId: globalReview.versionId,
|
|
1034
|
+
workUnitId: globalReview.workUnitId,
|
|
1035
|
+
laneSessionId,
|
|
1036
|
+
},
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return globalReview;
|
|
583
1041
|
}
|
|
584
1042
|
|
|
585
1043
|
function reviewCommandMatchesReview(command, review) {
|
|
@@ -873,9 +1331,8 @@ function isPatchModeFocusAttempt(root, payload, gate) {
|
|
|
873
1331
|
|
|
874
1332
|
function findChangeDir(root, changeId) {
|
|
875
1333
|
const candidates = [
|
|
876
|
-
path.join(root, 'openprd', 'changes', changeId),
|
|
877
|
-
path.join(root, '
|
|
878
|
-
path.join(root, 'openprd', 'archive', 'changes', changeId),
|
|
1334
|
+
path.join(root, '.openprd', 'changes', changeId),
|
|
1335
|
+
path.join(root, '.openprd', 'archive', 'changes', changeId),
|
|
879
1336
|
];
|
|
880
1337
|
return candidates.find((candidate) => fs.existsSync(candidate)) || candidates[0];
|
|
881
1338
|
}
|
|
@@ -1186,6 +1643,54 @@ function renderRunContextText(result) {
|
|
|
1186
1643
|
return lines.filter(Boolean).join('\n');
|
|
1187
1644
|
}
|
|
1188
1645
|
|
|
1646
|
+
function inferLearningGenre(text) {
|
|
1647
|
+
const value = String(text || '');
|
|
1648
|
+
if (/仙侠|修仙|玄幻/.test(value)) {
|
|
1649
|
+
return 'xianxia';
|
|
1650
|
+
}
|
|
1651
|
+
if (/武侠/.test(value)) {
|
|
1652
|
+
return 'wuxia';
|
|
1653
|
+
}
|
|
1654
|
+
if (/童话|寓言/.test(value)) {
|
|
1655
|
+
return 'fairy-tale';
|
|
1656
|
+
}
|
|
1657
|
+
if (/科幻|赛博|cyberpunk|sci[-\s]?fi/i.test(value)) {
|
|
1658
|
+
return 'sci-fi';
|
|
1659
|
+
}
|
|
1660
|
+
if (/互联网产品|产品经理|PM/i.test(value)) {
|
|
1661
|
+
return 'internet-product';
|
|
1662
|
+
}
|
|
1663
|
+
return null;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
function detectLearningMaterialIntent(prompt) {
|
|
1667
|
+
const text = stripMarkdown(prompt).trim();
|
|
1668
|
+
if (!text) {
|
|
1669
|
+
return { requested: false, genre: null, reason: null };
|
|
1670
|
+
}
|
|
1671
|
+
const explicitChatOnly = /(只要|仅要|仅需|就|直接).{0,16}(在对话里|回复|回答|写一段|一段文字|短文|纯文本|普通文本)|不要.{0,18}(文件|阅读器|学习包|HTML|html|openprd)/i.test(text);
|
|
1672
|
+
const durableReaderSignal = /(可长期阅读|长期阅读|可回看|阅读器|reader|电子书|学习包|归档)/i.test(text);
|
|
1673
|
+
if (explicitChatOnly && !durableReaderSignal) {
|
|
1674
|
+
return { requested: false, genre: null, reason: 'chat-only-requested' };
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
// This is an intent-shape detector, not a closed keyword table. The hook
|
|
1678
|
+
// looks for a requested learning deliverable, its durability/teaching goal,
|
|
1679
|
+
// and whether the expected output wants reader-like structure.
|
|
1680
|
+
const deliveryAction = /(生成|整理|产出|做一份|做一套|做个|做一个|创建|写一份|准备|沉淀|归档|转成|转换成|变成|输出|构建|给我)/i.test(text);
|
|
1681
|
+
const learningGoal = /(学习|教学|培训|复盘|教程|讲义|教材|课程|知识沉淀|经验沉淀|可回看|可复用|长期阅读|新人入门|onboarding)/i.test(text);
|
|
1682
|
+
const readerShape = /(学习(材料|资料|包|读本|文档)|复盘材料|教学材料|培训材料|教程|讲义|教材|课程|读本|电子书|阅读器|reader|ebook|章节|目录|证据锚点|图文讲解|检索练习|工作示例|可长期阅读|长期阅读)/i.test(text);
|
|
1683
|
+
const workToLearningArtifact = /(把|将|基于|围绕).{0,48}(工作|任务|过程|对话|会话|修复|需求|项目|实现|经验|记录|材料).{0,32}(转成|转换成|整理成|沉淀成|归档成|做成|写成).{0,40}(学习|复盘|教学|教程|讲义|材料|读本|可回看|可复用|长期阅读)/i.test(text);
|
|
1684
|
+
const requested = workToLearningArtifact
|
|
1685
|
+
|| (readerShape && (deliveryAction || learningGoal || durableReaderSignal))
|
|
1686
|
+
|| (learningGoal && durableReaderSignal);
|
|
1687
|
+
return {
|
|
1688
|
+
requested,
|
|
1689
|
+
genre: requested ? inferLearningGenre(text) : null,
|
|
1690
|
+
reason: workToLearningArtifact ? 'work-to-learning-artifact' : (readerShape ? 'reader-shaped-learning-artifact' : (requested ? 'durable-learning-goal' : null)),
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1189
1694
|
function analyzePromptIntent(prompt) {
|
|
1190
1695
|
const text = String(prompt || '').trim();
|
|
1191
1696
|
const normalized = text.toLowerCase();
|
|
@@ -1267,7 +1772,7 @@ function analyzePromptIntent(prompt) {
|
|
|
1267
1772
|
/copy/i,
|
|
1268
1773
|
];
|
|
1269
1774
|
const simpleConcretePatterns = [
|
|
1270
|
-
|
|
1775
|
+
/按钮|文案|颜色|圆角|位置|间距|留白|边距|宽度|高度|卡片|字号|图标|标题|空格|标点|对齐|密度|label|copy|padding|margin|spacing|gap/i,
|
|
1271
1776
|
/从.+(改到|移到|移动到|换到|变成|改成|改为).+/,
|
|
1272
1777
|
];
|
|
1273
1778
|
const l2StructuralScopePatterns = [
|
|
@@ -1370,8 +1875,10 @@ function analyzePromptIntent(prompt) {
|
|
|
1370
1875
|
];
|
|
1371
1876
|
const reviewConfirmPatterns = [
|
|
1372
1877
|
/认可方案/,
|
|
1373
|
-
|
|
1374
|
-
|
|
1878
|
+
// 容忍“确认 v0276 评审稿”这类中间插版本号/空格的自然语言确认。
|
|
1879
|
+
/确认(?:当前|这个|这版|该)?\s*(?:[vV]\d+\s*(?:的)?)?(?:PRD|评审稿|评审页|review|需求稿|版本)/i,
|
|
1880
|
+
/确认\s*[vV]\d+/,
|
|
1881
|
+
/(?:PRD|评审稿|评审页|review|需求稿|版本)\s*(?:[vV]\d+)?.{0,12}(确认|通过|没问题|认可|可以)/i,
|
|
1375
1882
|
/按这版(?:PRD|评审|需求稿)/i,
|
|
1376
1883
|
];
|
|
1377
1884
|
const reviewNeedsRevisionPatterns = [
|
|
@@ -1403,7 +1910,8 @@ function analyzePromptIntent(prompt) {
|
|
|
1403
1910
|
|| /失败.{0,20}(原因|根因|排查|定位|修|修复|处理|解决)|(?:原因|根因|排查|定位).{0,20}失败/.test(text);
|
|
1404
1911
|
const tinyEdit = tinyEditPatterns.some((pattern) => pattern.test(text));
|
|
1405
1912
|
const crossSystemRiskMatched = crossSystemRiskPatterns.some((pattern) => pattern.test(text));
|
|
1406
|
-
const localUiScopeMatched =
|
|
1913
|
+
const localUiScopeMatched = hasLightweightUiVisualSignal(text)
|
|
1914
|
+
|| /(样式|页面|界面|布局|信息架构|导航|列表|详情页|设置页)/i.test(text);
|
|
1407
1915
|
const reviewContinuationRequested = reviewContinuationPatterns.some((pattern) => pattern.test(text));
|
|
1408
1916
|
const explicitExecution = internalOpenPrdExecution
|
|
1409
1917
|
|| continuationVerbMatched
|
|
@@ -1414,6 +1922,7 @@ function analyzePromptIntent(prompt) {
|
|
|
1414
1922
|
const noReviewRequested = noReviewRequestedPatterns.some((pattern) => pattern.test(text));
|
|
1415
1923
|
const noConfirmationRequested = noConfirmationRequestedPatterns.some((pattern) => pattern.test(text));
|
|
1416
1924
|
const reviewDecision = promptReviewCommand?.mark
|
|
1925
|
+
?? (reviewContinuationRequested ? 'confirmed' : null)
|
|
1417
1926
|
?? (reviewConfirmPatterns.some((pattern) => pattern.test(text)) ? 'confirmed' : null)
|
|
1418
1927
|
?? (reviewNeedsRevisionPatterns.some((pattern) => pattern.test(text)) ? 'needs-revision' : null);
|
|
1419
1928
|
const confirmation = implementationConfirmation || Boolean(reviewDecision);
|
|
@@ -1424,6 +1933,8 @@ function analyzePromptIntent(prompt) {
|
|
|
1424
1933
|
const visualMockupRequest = imageGenerationTerms.test(text)
|
|
1425
1934
|
&& imageGenerationAction.test(text)
|
|
1426
1935
|
&& !codeVisualArtifactRequested;
|
|
1936
|
+
const learningMaterialIntent = detectLearningMaterialIntent(text);
|
|
1937
|
+
const learningMaterialRequest = learningMaterialIntent.requested;
|
|
1427
1938
|
const visualReview = /效果图|实现截图|视觉对比|视觉评审|对标效果图|复刻|不一致|好丑|没对齐|对不上|不像/i.test(text);
|
|
1428
1939
|
const directBugfixExecution = explicitExecution && bugfixOrDiagnostic;
|
|
1429
1940
|
const newFeatureVerbMatched = /(新增|增加|新建)/.test(text);
|
|
@@ -1441,19 +1952,22 @@ function analyzePromptIntent(prompt) {
|
|
|
1441
1952
|
const requirementSignalMatched = requirementRoutingSignalsStrong.some((pattern) => pattern.test(text))
|
|
1442
1953
|
|| (requirementRoutingSignalsWeak.some((pattern) => pattern.test(text))
|
|
1443
1954
|
&& requirementChangeIntentSignals.some((pattern) => pattern.test(text)));
|
|
1444
|
-
const l2FeatureExpansionMatched =
|
|
1445
|
-
&&
|
|
1446
|
-
(capabilityCreationMatched && (strategicL2ScopeMatched || structuralExpansionMatched))
|
|
1447
|
-
|| (newFeatureVerbMatched && text.length > 18)
|
|
1448
|
-
);
|
|
1955
|
+
const l2FeatureExpansionMatched = (capabilityCreationMatched && (strategicL2ScopeMatched || structuralExpansionMatched))
|
|
1956
|
+
|| (!localUiScopeMatched && newFeatureVerbMatched && text.length > 18);
|
|
1449
1957
|
const l2PlanningRequestMatched = !readOnly
|
|
1450
1958
|
&& featurePlanningMatched
|
|
1451
1959
|
&& requirementChangeIntentSignals.some((pattern) => pattern.test(text))
|
|
1452
1960
|
&& (strategicL2ScopeMatched || structuralExpansionMatched);
|
|
1453
1961
|
const l1OptimizationMatched = l1OptimizationPatterns.some((pattern) => pattern.test(text))
|
|
1454
|
-
&& /(页面|界面|视觉|样式|布局|信息架构|交互|体验|流程|入口|导航|表单|设置页|列表|详情页)/i.test(text);
|
|
1962
|
+
&& (hasLightweightUiVisualSignal(text) || /(页面|界面|视觉|样式|布局|信息架构|交互|体验|流程|入口|导航|表单|设置页|列表|详情页)/i.test(text));
|
|
1963
|
+
// 改现有脚本、CLI、发布/构建流程、参数或文档这类既有资产优化,
|
|
1964
|
+
// 不应仅因出现“新增/增加/补一个”动词就升级成 L2 重流程;
|
|
1965
|
+
// 只有同时命中真正的结构/战略扩张信号时才保留 L2。
|
|
1966
|
+
const existingAssetOptimizationMatched = /(脚本|script|发布|上传|部署|构建|打包|导出|同步|清理|CLI|命令行|命令|参数|选项|flag|配置文件|文档|README)/i.test(text)
|
|
1967
|
+
&& /(补|加|增加|支持|筛选|过滤|优化|调整|收敛|清理|改成|改为|升级|更新|修改|梳理)/.test(text)
|
|
1968
|
+
&& !/(新产品|新模块|新平台|新系统|新页面|新界面|新客户端|全新|从零|从\s*0|重写整个|重构整个)/i.test(text);
|
|
1455
1969
|
const l0AdjustmentMatched = l0AdjustmentPatterns.some((pattern) => pattern.test(text))
|
|
1456
|
-
&& /(
|
|
1970
|
+
&& (hasLightweightUiVisualSignal(text) || /(样式|一处)/i.test(text));
|
|
1457
1971
|
const publicRepoResearchRequest = githubRepoPattern.test(text)
|
|
1458
1972
|
&& /(github|仓库|repo|项目|参考|对标|复刻|review|学习|架构|模块|流程|构建|测试|扩展点)/i.test(text);
|
|
1459
1973
|
const externalTechResearchRequest = /(第三方|library|framework|sdk|api|mcp|cli|依赖|包|版本|迁移|弃用|官方文档|参数|返回值|生命周期)/i.test(text)
|
|
@@ -1472,8 +1986,12 @@ function analyzePromptIntent(prompt) {
|
|
|
1472
1986
|
const l2RequirementCandidate = (bugfixOrDiagnostic && crossSystemRiskMatched)
|
|
1473
1987
|
|| l2FeatureExpansionMatched
|
|
1474
1988
|
|| l2PlanningRequestMatched;
|
|
1989
|
+
const existingAssetDemotionApplies = existingAssetOptimizationMatched
|
|
1990
|
+
&& !strategicL2ScopeMatched
|
|
1991
|
+
&& structuralL2ScopeMatchCount === 0;
|
|
1475
1992
|
const requirementTier = !internalOpenPrdExecution
|
|
1476
1993
|
? (l2RequirementCandidate
|
|
1994
|
+
&& !existingAssetDemotionApplies
|
|
1477
1995
|
&& !tinyEdit
|
|
1478
1996
|
&& !simpleConcrete
|
|
1479
1997
|
&& !visualMockupRequest
|
|
@@ -1489,7 +2007,9 @@ function analyzePromptIntent(prompt) {
|
|
|
1489
2007
|
? 'l0'
|
|
1490
2008
|
: (!visualMockupRequest
|
|
1491
2009
|
&& !readOnly
|
|
1492
|
-
&& (l1OptimizationMatched
|
|
2010
|
+
&& (l1OptimizationMatched
|
|
2011
|
+
|| requirementSignalMatched
|
|
2012
|
+
|| (l2RequirementCandidate && existingAssetDemotionApplies))
|
|
1493
2013
|
? 'l1'
|
|
1494
2014
|
: null)))
|
|
1495
2015
|
: null;
|
|
@@ -1520,6 +2040,9 @@ function analyzePromptIntent(prompt) {
|
|
|
1520
2040
|
weappValidationRequest,
|
|
1521
2041
|
browserSafetyRequest,
|
|
1522
2042
|
productCopyRequest,
|
|
2043
|
+
learningMaterialRequest,
|
|
2044
|
+
learningGenre: learningMaterialIntent.genre,
|
|
2045
|
+
learningMaterialReason: learningMaterialIntent.reason,
|
|
1523
2046
|
shouldInject: requiresIntake
|
|
1524
2047
|
|| requirementTier === 'l1'
|
|
1525
2048
|
|| explicitExecution
|
|
@@ -1535,6 +2058,7 @@ function analyzePromptIntent(prompt) {
|
|
|
1535
2058
|
|| weappValidationRequest
|
|
1536
2059
|
|| browserSafetyRequest
|
|
1537
2060
|
|| productCopyRequest
|
|
2061
|
+
|| learningMaterialRequest
|
|
1538
2062
|
|| /openprd/i.test(normalized)
|
|
1539
2063
|
|| /\bprd\b/i.test(normalized),
|
|
1540
2064
|
};
|
|
@@ -1542,7 +2066,7 @@ function analyzePromptIntent(prompt) {
|
|
|
1542
2066
|
|
|
1543
2067
|
function isShortAffirmativeConfirmation(prompt) {
|
|
1544
2068
|
const text = stripMarkdown(prompt).trim();
|
|
1545
|
-
return /^(
|
|
2069
|
+
return /^(可以|好|行|确认|确定|没问题|同意|就这样|OK|ok|yes|Yes|yep|Yep)[。!!,.,s]*$/.test(text);
|
|
1546
2070
|
}
|
|
1547
2071
|
|
|
1548
2072
|
function looksLikeClarificationFollowupReply(prompt, intent = null) {
|
|
@@ -1657,6 +2181,32 @@ function authorizeRequirementGateExecution(root, prompt, sessionId = null, optio
|
|
|
1657
2181
|
return next;
|
|
1658
2182
|
}
|
|
1659
2183
|
|
|
2184
|
+
function escalateRequirementGateApproval(root, prompt, gate, sessionId = null) {
|
|
2185
|
+
// 用户已经确认过一次需求摘要,又再次给出明确肯定/继续意图时,
|
|
2186
|
+
// 把当前 lane 升级为 silent-record + 自动执行授权:后续 review 记录、change、tasks
|
|
2187
|
+
// 都沿已确认范围静默推进,不再向用户反复索取同一轮确认。
|
|
2188
|
+
const approvalPolicy = normalizeRequirementApprovalPolicy({
|
|
2189
|
+
...(gate?.approvalPolicy ?? {}),
|
|
2190
|
+
reviewPolicy: 'silent-record',
|
|
2191
|
+
executionMode: 'auto-after-review-and-tasks',
|
|
2192
|
+
suppressExtraConfirmation: true,
|
|
2193
|
+
});
|
|
2194
|
+
return updateRequirementGate(root, {
|
|
2195
|
+
approvalPolicy,
|
|
2196
|
+
intent: { ...(gate?.intent ?? {}), explicitExecution: true },
|
|
2197
|
+
executionIntent: {
|
|
2198
|
+
...(gate?.executionIntent ?? {}),
|
|
2199
|
+
explicitRequested: true,
|
|
2200
|
+
suppressExtraConfirmation: true,
|
|
2201
|
+
source: 'repeated-user-confirmation',
|
|
2202
|
+
latchedAt: now(),
|
|
2203
|
+
},
|
|
2204
|
+
confirmationCount: Number(gate?.confirmationCount ?? 1) + 1,
|
|
2205
|
+
repeatedConfirmationAt: now(),
|
|
2206
|
+
confirmationPreview: preview(prompt, 500),
|
|
2207
|
+
}, sessionId);
|
|
2208
|
+
}
|
|
2209
|
+
|
|
1660
2210
|
function updateRequirementGate(root, patch, sessionId = null) {
|
|
1661
2211
|
const current = readRequirementGate(root, sessionId);
|
|
1662
2212
|
if (!current?.active) {
|
|
@@ -1935,6 +2485,9 @@ function evaluateRequirementGateProgress(root, sessionId = null) {
|
|
|
1935
2485
|
nextStep = 'task-breakdown-required';
|
|
1936
2486
|
reason = '本次调整范围已经立起来了,但还没拆成可直接执行的后续任务。';
|
|
1937
2487
|
}
|
|
2488
|
+
if (review.staleReview?.versionId) {
|
|
2489
|
+
reason += ` 另外检测到工作区里残留了一份属于其他会话的历史确认稿(${review.staleReview.versionId}),本轮已自动忽略它;不要尝试确认或复用那份旧稿,直接按当前需求生成新的确认稿。`;
|
|
2490
|
+
}
|
|
1938
2491
|
return {
|
|
1939
2492
|
runContext: parsed,
|
|
1940
2493
|
binding,
|
|
@@ -2088,6 +2641,94 @@ function isMutationPayload(payload, risk) {
|
|
|
2088
2641
|
|| /\*\*\* Begin Patch/.test(text);
|
|
2089
2642
|
}
|
|
2090
2643
|
|
|
2644
|
+
function isRuntimeAutonomyRiskFile(file) {
|
|
2645
|
+
const normalized = String(file || '').split(path.sep).join('/');
|
|
2646
|
+
if (!normalized) {
|
|
2647
|
+
return true;
|
|
2648
|
+
}
|
|
2649
|
+
if (/^(docs|test|tests|__tests__|fixtures|examples|templates?)\//i.test(normalized)) {
|
|
2650
|
+
return false;
|
|
2651
|
+
}
|
|
2652
|
+
if (/(^|\/)(AGENTS|CLAUDE|README|CHANGELOG|SKILL)\.md$/i.test(normalized)) {
|
|
2653
|
+
return false;
|
|
2654
|
+
}
|
|
2655
|
+
if (/\.(?:md|mdx|txt|rst)$/i.test(normalized)) {
|
|
2656
|
+
return false;
|
|
2657
|
+
}
|
|
2658
|
+
return true;
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
function hasExplicitAutonomyRiskApproval(intent = null) {
|
|
2662
|
+
const text = String(intent?.promptText || '');
|
|
2663
|
+
if (!text.trim()) {
|
|
2664
|
+
return false;
|
|
2665
|
+
}
|
|
2666
|
+
const riskTerms = /(云端|线上|生产环境|远端|热修复|hotfix|兜底|临时兜底|fallback|fall\s*back|写死|硬编码|hardcode|hard-coded|hard coded)/i;
|
|
2667
|
+
if (!riskTerms.test(text)) {
|
|
2668
|
+
return false;
|
|
2669
|
+
}
|
|
2670
|
+
const negatedRisk = /(不要|禁止|不允许|不得|不能|避免|默认不|未明确|未经|除非).{0,48}(云端|线上|生产环境|远端|热修复|hotfix|兜底|临时兜底|fallback|fall\s*back|写死|硬编码|hardcode|hard-coded|hard coded)|(云端|线上|生产环境|远端|热修复|hotfix|兜底|临时兜底|fallback|fall\s*back|写死|硬编码|hardcode|hard-coded|hard coded).{0,48}(不要|禁止|不允许|不得|不能|避免|默认不|未明确|未经|除非)/i;
|
|
2671
|
+
if (negatedRisk.test(text)) {
|
|
2672
|
+
return false;
|
|
2673
|
+
}
|
|
2674
|
+
return /(明确)?(允许|批准|同意|确认|可以|请|直接|必须|这次|现在).{0,40}(云端|线上|生产环境|远端|热修复|hotfix|兜底|临时兜底|fallback|fall\s*back|写死|硬编码|hardcode|hard-coded|hard coded)|(云端|线上|生产环境|远端|热修复|hotfix|兜底|临时兜底|fallback|fall\s*back|写死|硬编码|hardcode|hard-coded|hard coded).{0,40}(允许|批准|同意|确认|可以|直接|必须|这次|现在)/i.test(text);
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
function detectHighRiskAutonomyMutation(root, payload, intent, risk) {
|
|
2678
|
+
if (!isMutationPayload(payload, risk)) {
|
|
2679
|
+
return null;
|
|
2680
|
+
}
|
|
2681
|
+
const command = commandText(payload);
|
|
2682
|
+
const text = [
|
|
2683
|
+
toolName(payload),
|
|
2684
|
+
command,
|
|
2685
|
+
payloadText(payload),
|
|
2686
|
+
].filter(Boolean).join('\n');
|
|
2687
|
+
const touched = extractTouchedFiles(root, payload);
|
|
2688
|
+
const runtimeTarget = touched.length === 0 || touched.some((file) => isRuntimeAutonomyRiskFile(file));
|
|
2689
|
+
const cloudMutation = /(?:\b(?:ssh|scp|rsync)\b[^\n]*(?:prod|production|线上|生产|云端|server|remote)|\b(?:kubectl|helm)\b[^\n]*(?:apply|patch|set|delete|rollout|scale|exec)[^\n]*(?:prod|production|线上|生产|云端|cluster)|\b(?:vercel|wrangler|fly|flyctl|railway|firebase)\b[^\n]*(?:deploy|up)[^\n]*(?:--prod|prod|production|线上|生产)|\bsupabase\s+db\s+push\b[^\n]*(?:--linked|--db-url|prod|production|线上|生产)|\bcurl\b[^\n]*(?:-X|--request)\s*(?:POST|PUT|PATCH|DELETE)\b[^\n]*(?:prod|production|线上|生产|云端|api\.))/i.test(text);
|
|
2690
|
+
if (cloudMutation) {
|
|
2691
|
+
return {
|
|
2692
|
+
kind: 'cloud-hotfix',
|
|
2693
|
+
title: '云端/生产远端写入',
|
|
2694
|
+
details: preview(command || text, 260),
|
|
2695
|
+
};
|
|
2696
|
+
}
|
|
2697
|
+
if (!runtimeTarget) {
|
|
2698
|
+
return null;
|
|
2699
|
+
}
|
|
2700
|
+
// 只保留真实业务权益/计费类词汇;配置、服务端、客户端、缓存、feature 这类
|
|
2701
|
+
// 基础设施通用词在正常代码里大量出现,叠加“fallback/兜底”会造成海量误拦。
|
|
2702
|
+
const businessTerms = /(业务|权益|会员|额度|价格|计费|订阅|套餐|entitlement|membership|billing|quota|price|paywall|subscription|localstorage)/i;
|
|
2703
|
+
const fallbackTerms = /(兜底|fallback|fall\s*back|临时补|临时插入|补行|补进|本地补|客户端补|服务端补|缓存补|默认插入|补一条|补数据)/i;
|
|
2704
|
+
const hardcodeTerms = /(写死|硬编码|hardcode|hard-coded|hard coded)/i;
|
|
2705
|
+
const fixedBusinessValue = /(unlimited|premium|pro|vip|admin|owner|free|trial|quota|limit|count_limit|free_limit|权益|会员).{0,80}(?:=|:).{0,80}(?:unlimited|premium|pro|vip|admin|owner|free|true|false|999999|Infinity)/i;
|
|
2706
|
+
if ((businessTerms.test(text) && (fallbackTerms.test(text) || hardcodeTerms.test(text))) || fixedBusinessValue.test(text)) {
|
|
2707
|
+
return {
|
|
2708
|
+
kind: 'hardcoded-fallback',
|
|
2709
|
+
title: '业务兜底/写死逻辑',
|
|
2710
|
+
details: preview(touched.length > 0 ? `${touched.join(', ')}\n${command || text}` : command || text, 260),
|
|
2711
|
+
};
|
|
2712
|
+
}
|
|
2713
|
+
return null;
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
function formatHighRiskAutonomyBlock(detection) {
|
|
2717
|
+
return [
|
|
2718
|
+
`OpenPrd blocked a high-risk autonomy action: ${detection.title}.`,
|
|
2719
|
+
'当前用户消息只授权了本轮实现范围,不等于授权 Agent 私自做云端热修复、生产远端写入、业务兜底、写死逻辑或缓存补行。',
|
|
2720
|
+
detection.details ? `Matched action: ${detection.details}` : null,
|
|
2721
|
+
'默认路径:先改本地源码、配置、迁移或数据修复脚本,让本地与云端保持同一事实源;完成后再按正常发布/部署路径验证。',
|
|
2722
|
+
'如果这次确实必须临时处理,请先让用户明确说“允许这次云端热修复”或“允许这次临时兜底/写死”,并说明影响范围、回滚方式和如何同步回本地源码/配置/迁移。',
|
|
2723
|
+
].filter(Boolean).join('\n');
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
const LIGHTWEIGHT_UI_VISUAL_SIGNAL_PATTERN = /(按钮|文案|颜色|圆角|位置|间距|留白|边距|内边距|外边距|宽度|高度|卡片|字号|字体|行高|图标|标题|空格|标点|对齐|密度|阴影|边框|层级|组件|输入框|菜单|侧栏|表格|列表|头像|badge|label|copy|toast|placeholder|padding|margin|spacing|gap|太宽|太窄|太挤|拥挤|松散|留得多|留着有点多|没对齐|好看|美观|审美|气质|记忆点|高级|太丑|丑|视觉风格|上架图|截图优化)/i;
|
|
2727
|
+
|
|
2728
|
+
function hasLightweightUiVisualSignal(text = '') {
|
|
2729
|
+
return LIGHTWEIGHT_UI_VISUAL_SIGNAL_PATTERN.test(String(text || ''));
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2091
2732
|
function isFrontendTaskIntent(intent = null) {
|
|
2092
2733
|
const text = String(intent?.promptText || '').trim();
|
|
2093
2734
|
if (!text || intent?.visualMockupRequest) {
|
|
@@ -2162,7 +2803,7 @@ function frontendDesignPreflightIssues(root, intent, implementationFiles) {
|
|
|
2162
2803
|
if (shouldRequireAssetSpec(intent) && designArtifactHasPlaceholder(root, 'asset-spec.md')) {
|
|
2163
2804
|
issues.push({
|
|
2164
2805
|
file: 'asset-spec.md',
|
|
2165
|
-
reason: '进入实现前先冻结资产口径;没有官方资产也要写“缺失 / 暂无 /
|
|
2806
|
+
reason: '进入实现前先冻结资产口径;没有官方资产也要写“缺失 / 暂无 / 不适用”,不要留模板占位。新功能出现新入口、按钮、tab、卡片或空态时,同步补“功能图标”行:先复用项目已有图标体系,缺失时按最佳实践路由(Phosphor / Lucide / Tabler / React Icons / LobeHub / Tech Icons / iconfont)选型并登记图标名、来源、用途,不等用户要求。',
|
|
2166
2807
|
});
|
|
2167
2808
|
}
|
|
2168
2809
|
if (shouldRequireImagePreflight(intent) && designArtifactHasPlaceholder(root, 'image-preflight.md')) {
|
|
@@ -2458,7 +3099,9 @@ function productCopyMessage(intent) {
|
|
|
2458
3099
|
return [
|
|
2459
3100
|
'OpenPrd 产品内文案提醒:',
|
|
2460
3101
|
'先检查项目是否已有 i18n、locales、translations、Localizable 或其他语言资源;若已有,多语言要同步维护。',
|
|
2461
|
-
'
|
|
3102
|
+
'产品界面上用户能看到的文案必须站在当前用户视角写:说明用户现在能做什么、会发生什么、为什么值得点、下一步怎么走。',
|
|
3103
|
+
'除非这是面向开发者或专业技术人员的技术型产品,否则默认用普通人能看懂的语言写结果、状态、限制和行动,不要把 API、SDK、模型、数据库、缓存、错误码或其他实现细节直接暴露给用户。',
|
|
3104
|
+
'快速正反例:不要写“适合想保留全部工具入口的用户”,改成“首页会显示所有工具入口,你可以直接选择需要的功能”;不要写“API 请求失败,错误码 500”,改成“暂时保存失败,请稍后再试”。技术型产品可以保留必要术语,但仍要写清用户动作、影响和修复路径。',
|
|
2462
3105
|
].join('\n');
|
|
2463
3106
|
}
|
|
2464
3107
|
|
|
@@ -2565,13 +3208,25 @@ function devCheckWrapUpMessage(root, turnState) {
|
|
|
2565
3208
|
if (!parsed?.wrapUp?.required || !parsed.wrapUp.markdownTable) {
|
|
2566
3209
|
return null;
|
|
2567
3210
|
}
|
|
3211
|
+
const autoMode = parsed.wrapUp.mode === 'auto-refactor';
|
|
2568
3212
|
return [
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
3213
|
+
autoMode
|
|
3214
|
+
? 'OpenPRD 自动优化报告:本轮有超行改动对象,自动优化默认开启。'
|
|
3215
|
+
: 'OpenPrd 后续建议:本轮有改动对象需要主动说明。',
|
|
3216
|
+
autoMode
|
|
3217
|
+
? '先在本轮按 dev-check 给出的动作完成高内聚低耦合拆分(不丢任何功能、行为不变,拆完重跑测试与 dev-check),把每行“本次处理结果”回填为真实优化产出;最终回复必须以 **OpenPRD 自动优化报告** 为标题直接复用下面的 Markdown 表格,按 🔴 → 🟠 → 🟡 排序,并附一句:“如果下次不需要自动优化代码结构,回复关闭自动优化即可”。用户确认关闭后运行 `openprd dev-check . --auto-refactor off`。'
|
|
3218
|
+
: '最终回复必须以 **后续建议** 为标题直接复用下面的 Markdown 表格,按 🔴 → 🟠 → 🟡 的顺序帮助产品或业务理解影响对象、本次处理结果和后续建议;不要只用工具名或一段话带过。',
|
|
3219
|
+
autoMode
|
|
3220
|
+
? '如果你改写了“源文件规模 / 优化原因 / 本次处理结果 / 后续建议”,先用 `node scripts/dev-check-wrapup-copy.mjs --validate` 校验每格不超过 20 字;若报错,按提示缩短后重试。'
|
|
3221
|
+
: '如果你改写了“规模信号 / 预警原因 / 本次处理结果 / 后续建议”,先用 `node scripts/dev-check-wrapup-copy.mjs --validate` 校验每格不超过 20 字;若报错,按提示缩短后重试。',
|
|
3222
|
+
autoMode
|
|
3223
|
+
? '不要把“问题级别”列改写成纯 emoji;必须保留例如“🟠 中风险|建议优先关注”这类完整标签。'
|
|
3224
|
+
: '不要把“关注程度”列改写成纯 emoji;必须保留例如“🟠 中风险|建议优先关注”这类完整标签。',
|
|
2573
3225
|
parsed.wrapUp.markdownBlock ?? parsed.wrapUp.markdownTable,
|
|
2574
|
-
|
|
3226
|
+
parsed.visualEvidence?.required
|
|
3227
|
+
? `视觉证据提醒:${parsed.visualEvidence.reason} 建议命令:${(parsed.visualEvidence.suggestedCommands ?? []).join(';')}。${parsed.visualEvidence.chatEmbedHint ?? ''}`
|
|
3228
|
+
: null,
|
|
3229
|
+
].filter(Boolean).join('\n');
|
|
2575
3230
|
}
|
|
2576
3231
|
|
|
2577
3232
|
function shouldRunDoctorForHighRisk(payload) {
|
|
@@ -2721,8 +3376,10 @@ function requirementGateMessage(intent, gate) {
|
|
|
2721
3376
|
'OpenPrd requirement intake gate is active only for implementation writes.',
|
|
2722
3377
|
'The user is asking for an image asset such as a cover image, poster, illustration, icon, sticker, visual mockup, or effect image, not code implementation.',
|
|
2723
3378
|
'For logo, icon, avatar, badge, and similar development assets, default to a standalone asset: full-frame single subject with no extra UI frame, card shell, device mockup, or presentation container unless the user explicitly asked for one.',
|
|
3379
|
+
'If the final development asset should be transparent for UI use, use an icon-asset pipeline: generate three visibly different standalone candidates first, each on a solid #00ff00 chroma-key background with no text, no UI container, centered subject, and enough padding; do not write project files before the user chooses one. After selection, verify the selected source or contact sheet, keep the green-screen source, run remove_chroma_key.py to produce a transparent PNG/WebP, crop and center it, export 384px or required sizes, tune the display ratio per real UI placement, and record the source, transparent output, integration point, and verification in asset-spec.md / selected-direction.md before claiming completion.',
|
|
2724
3380
|
'Do not create temporary HTML/SVG/CSS files for this image unless the user explicitly requested that format.',
|
|
2725
|
-
'
|
|
3381
|
+
'Image tool routing: on a Codex surface use native `imagegen` (Image 2); on a Cursor surface use the built-in `GenerateImage` tool; both are free inside the conversation tool. If neither surface applies, read the confirmed preference in `.openprd/harness/image-generation-preference.json`; when no preference exists, ask the user which image-generation path to use and persist the answer to that file as the future default. NEVER silently call the user\'s own paid image API. The image tool is the tool path, not an aesthetic exemption, so first state the purpose, audience, intended tone, constraints, and memory point, then put them into the image prompt.',
|
|
3382
|
+
'Before generation, do a quick anti-slop check: without brand or reference support, avoid default purple/blue-purple gradients, generic font taste, white card piles, and context-free decoration.',
|
|
2726
3383
|
].join('\n');
|
|
2727
3384
|
}
|
|
2728
3385
|
const status = gateBlocksImplementation ? 'active' : 'opened';
|
|
@@ -2767,28 +3424,63 @@ function lightweightRequirementMessage(intent) {
|
|
|
2767
3424
|
return null;
|
|
2768
3425
|
}
|
|
2769
3426
|
|
|
2770
|
-
function
|
|
3427
|
+
function imageGenerationPreferencePath(root) {
|
|
3428
|
+
return path.join(harnessDir(root), 'image-generation-preference.json');
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3431
|
+
function imageGenerationRoutingLines(root) {
|
|
3432
|
+
const preference = readJsonSync(imageGenerationPreferencePath(root), null);
|
|
3433
|
+
const lines = [
|
|
3434
|
+
'生图路由: 第一层先判断当前对话工具面——Codex 环境用原生 `imagegen`(Image 2),Cursor 环境用内置 `GenerateImage` 工具;两者都是对话工具内免费能力,优先默认使用。',
|
|
3435
|
+
'如果当前既不是 Codex 也不是 Cursor,或内置生图工具面证据不存在,先读 `.openprd/harness/image-generation-preference.json` 里用户已确认的生图偏好;没有已确认偏好时必须先询问用户要用哪种生图方式,并把用户答复以 `{"provider","source":"user-confirmed","updatedAt"}` 写回该文件,下次直接作为默认。',
|
|
3436
|
+
'成本护栏: 绝对不要在用户未明确指定的情况下擅自调用用户本地或自有付费生图 API(如用户自己的 OpenAI / Stability key),避免产生额外成本;只有偏好文件或当前消息明确授权时才可以使用。',
|
|
3437
|
+
];
|
|
3438
|
+
if (preference?.provider) {
|
|
3439
|
+
lines.push(`当前已保存的用户生图偏好: ${preference.provider}(来源: ${preference.source || 'user-confirmed'})。内置免费生图工具不可用时优先使用它,不要再重复询问。`);
|
|
3440
|
+
}
|
|
3441
|
+
return lines;
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
function visualMockupMessage(intent, root = null) {
|
|
2771
3445
|
if (!intent?.visualMockupRequest) {
|
|
2772
3446
|
return null;
|
|
2773
3447
|
}
|
|
2774
3448
|
return [
|
|
2775
3449
|
'当前用户要的是图片内容生成,例如图片、封面图、配图、海报、插画、图标、贴纸、头像、banner、主视觉/KV、运营图、效果图、视觉稿或 mockup。',
|
|
2776
|
-
|
|
2777
|
-
'
|
|
3450
|
+
...(root ? imageGenerationRoutingLines(root) : []),
|
|
3451
|
+
'按生图路由选定的内置生图工具生成图片(Codex=`imagegen`/Image 2,Cursor=`GenerateImage`);生图工具是工具路径,不是审美豁免;生图前先写清用途、受众、气质、约束和记忆点,并把这些写进 prompt。',
|
|
3452
|
+
'生图前用 `.openprd/design/anti-slop.md` 做快速自检:没有品牌或参考图依据时,不要默认紫白/蓝紫渐变、通用字体、白底卡片堆叠或无语境装饰。',
|
|
3453
|
+
'除非用户明确指定 HTML、SVG、CSS、Canvas、代码稿或可编辑矢量/source artifact,不要改用临时 HTML/SVG/CSS 再截图。',
|
|
3454
|
+
'对 logo、icon、avatar、badge 等开发素材,如果用户没有明确要求 mockup、场景图、设备框、卡片承载、名片/包装展示或参考界面复刻,默认按独立素材输出(standalone asset)处理:使用全画布单主体,不额外添加 UI frame、卡片、设备壳、名片、桌面陈列、手持实拍或其他展示容器;如果最终要接入 UI 并需要透明背景,先生成 3 个差异足够大的纯 `#00ff00` 绿幕候选方向,用户选定前不写项目文件,选定后再保留源图、抠透明、裁切居中、导出 384px 或多尺寸资产,按真实 UI 场景调显示比例,并把源图、透明产物、接入位置和验证结果写回 `asset-spec.md` / `selected-direction.md`。',
|
|
2778
3455
|
'只有当用户明确要求 mockup、场景化效果图、容器化呈现,或参考图本身就包含这些承载结构时,才生成对应的容器或场景。',
|
|
2779
|
-
'
|
|
2780
|
-
'
|
|
3456
|
+
'只有在实际发生生图工具调用后,才能汇报生图结果、失败或限流;未调用前,不要声称“生图限流”或“生图失败”。',
|
|
3457
|
+
'把生图结果先当成候选效果图,不要默认登记到 `.openprd/harness/visual-reviews/`,也不要自动当成后续验收参考;看图时不仅判断是否生成成功,还要判断气质、层级、字体/色彩/表面角色和记忆点是否成立。',
|
|
2781
3458
|
'如果用户看完图后还要继续做实现,主动连问三件事:是否符合预期、是否纳入后续效果图/实现截图对比、是否按此继续后续实现。只有用户确认纳入对比或继续实现后,才把这张图、这组图或其中选定子图整理成后续 reference-set;如果用户只是认可图片但暂不实现,就继续把它当候选效果图。',
|
|
2782
3459
|
'如果一张图里有多个子图、网格、多对象或多个方向,不要把整板直接当成单一参考图;先运行 `openprd visual-prepare . --reference <效果图> --grid <列>x<行>` 或 `--boxes <plan.json>` 生成 reference-set、contact sheet 和 compare-plan,确认 contact sheet 后再逐项对比或统一验收。',
|
|
2783
|
-
'OpenPrd review.html 只用于需求评审,visual-compare 只用于实现阶段视觉证据:已有参考图时做效果图/实现截图对比;没有参考图时先判断新建界面还是修改既有界面,新建界面回到实现前 3
|
|
3460
|
+
'OpenPrd review.html 只用于需求评审,visual-compare 只用于实现阶段视觉证据:已有参考图时做效果图/实现截图对比;没有参考图时先判断新建界面还是修改既有界面,新建界面回到实现前 3 方向方案评审,修改既有界面做修改前/修改后自检;局部细节优先补局部焦点证据板,并行优化方向优先补并行实验证据板;同构列表、卡片、网格或表格需要补对齐辅助线证据板,并同时检查容器轨道和内部内容槽位轨道;单个 logo、icon、avatar、badge、按钮图形或图片内部需要居中判定、视觉重心评估或偏心排查时,需要补内部居中证据板,并同时检查画布中心、主体外接框中心和视觉重心偏移。',
|
|
2784
3461
|
].join('\n');
|
|
2785
3462
|
}
|
|
2786
3463
|
|
|
2787
|
-
function
|
|
2788
|
-
return
|
|
3464
|
+
function learningGenreFlag(intent = null) {
|
|
3465
|
+
return intent?.learningGenre ? ` --genre ${intent.learningGenre}` : '';
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3468
|
+
function learningReviewCommand(intent = null) {
|
|
3469
|
+
return `openprd learn . --topic "<学习主题>"${learningGenreFlag(intent)} --open`;
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
function learningReviewMessage(intent = null) {
|
|
3473
|
+
const command = learningReviewCommand(intent);
|
|
3474
|
+
const lines = [
|
|
2789
3475
|
'如果当前交付目标是把一次工作转成可学习、可复用、可回看、可教学或可沉淀的材料,不要先按关键词判断,也不要只停留在普通说明文档。',
|
|
2790
3476
|
'先判断期望产物形态:是否需要章节结构、证据锚点、图文讲解、检索练习、工作示例或阅读体验。需要这些形态时,优先使用 `openprd learn .` 生成学习包骨架、阅读器和证据清单,再补充正文内容;普通 Markdown 只能作为辅助讲义。',
|
|
2791
|
-
]
|
|
3477
|
+
];
|
|
3478
|
+
if (intent?.learningMaterialRequest) {
|
|
3479
|
+
lines.unshift('当前输入已被识别为学习型交付物请求:这是按产物形态和长期阅读/教学目的判断,不是固定关键词表。');
|
|
3480
|
+
lines.push(`建议先运行 \`${command}\`。如果用户指定了仙侠、科幻、童话等风格,把它当作学习包题材/文体参数,而不是绕过阅读器流程的理由。`);
|
|
3481
|
+
lines.push('生成骨架后,读取 `.openprd/learning/archive/<packageId>/agent-prompt.md`,由 Agent 基于证据写入 `learning-content.json`,再运行 `openprd learn . --content-json <file> --open` 渲染最终 reader;不要把聊天里的普通 Markdown 成稿当完成。');
|
|
3482
|
+
}
|
|
3483
|
+
return lines.join('\n');
|
|
2792
3484
|
}
|
|
2793
3485
|
|
|
2794
3486
|
function largeUiVisualDirectionMessage() {
|
|
@@ -2796,7 +3488,7 @@ function largeUiVisualDirectionMessage() {
|
|
|
2796
3488
|
'OpenPrd 大界面改动视觉方案评审:',
|
|
2797
3489
|
'位置: 需求分流之后、PRD 定稿或实现开工之前;它不同于 review.html,也不同于实现后的 visual-compare。',
|
|
2798
3490
|
'判断: 先从用户目标、信息架构变化、视觉决策成本和验收风险判断是否需要方向评审,不用关键词触发。会决定首屏、核心布局、主视觉、关键路径、组件层级/密度,或用户需要先选设计方向时触发。',
|
|
2799
|
-
'步骤: 已有界面时用 Codex Computer Use 进入产品内对应功能并截当前真实界面;冷启动没有现有界面时,基于已确认 PRD、用户画像、第一版切片和视觉目标写设计 brief
|
|
3491
|
+
'步骤: 已有界面时用 Codex Computer Use 进入产品内对应功能并截当前真实界面;冷启动没有现有界面时,基于已确认 PRD、用户画像、第一版切片和视觉目标写设计 brief。brief 必须写清用途、受众、气质端点、约束和记忆点,3 个方向要分别说明审美主张和主动避开的模板味。然后按生图路由调用内置生图工具(Codex=`imagegen`/Image 2,Cursor=`GenerateImage`;都不可用时按 `.openprd/harness/image-generation-preference.json` 的用户已确认偏好,没有偏好先问用户)生成至少 3 个不同设计思想方向;把效果图横向拼成一张带 1/2/3 序号的大图,先作为候选效果图给用户评审,不要默认写入 `.openprd/harness/visual-reviews/` 当验收参考。',
|
|
2800
3492
|
'交互: 主动追问三件事:是否符合预期、是否纳入后续效果图/实现截图对比、是否按此继续后续实现。只有用户确认纳入后续对比或继续实现后,才把选定方向、整张图或其中子图整理成 reference-set 并写入 `.openprd/harness/visual-reviews/`;用户确认前,不进入大 UI 实现,也不要声称界面方案已定。',
|
|
2801
3493
|
'多对象参考处理: 如果候选效果图是一张图里包含多个子图、网格、多对象或多方向,不要拿整板直接硬比;先运行 `openprd visual-prepare . --reference <效果图> --grid <列>x<行>` 或 `--boxes <plan.json>`,检查 contact sheet,确认 reference-set 后再逐项进入实现和验收。',
|
|
2802
3494
|
].join('\n');
|
|
@@ -2805,15 +3497,16 @@ function largeUiVisualDirectionMessage() {
|
|
|
2805
3497
|
function visualLocalAdjustmentMessage() {
|
|
2806
3498
|
return [
|
|
2807
3499
|
'如果这轮验收重点在局部变化,不要只改代码后凭主观判断收尾。',
|
|
2808
|
-
'
|
|
2809
|
-
'
|
|
3500
|
+
'修改前先用一句话锁定本次审美意图和记忆点;收口的 visual-compare/focus-board 不只看位置,也要看气质、层级、色彩、字号、间距和表面角色是否被保住。',
|
|
3501
|
+
'先判断这是新建界面还是修改既有界面:新建界面走实现前 3 方向方案评审;修改既有界面先保留修改前截图。完成后从同一入口、视口、账号和数据状态截修改后截图,再运行 `openprd visual-compare . --before <修改前截图> --after <修改后截图> --locale <zh-CN|en>`。',
|
|
3502
|
+
'如果局部细节需要放到同一张证据板里审阅,再补一份 `openprd visual-compare . --board <focus-board.json> --locale <zh-CN|en>` 的局部焦点证据板,把局部变化组合到同一张证据板里统一验收;如果只是普通截图、Computer/Browser/Playwright 实测截图,也要用 `openprd visual-compare . --board <verification-board.json> --locale <zh-CN|en>` 拼成截图实测证据板;如果界面存在同构列表、卡片、网格或表格,或用户反馈元素没有对齐,要用 `openprd visual-compare . --board <alignment-board.json> --locale <zh-CN|en>` 拼成对齐辅助线证据板,且同一张板里同时覆盖容器轨道和标题/副标题/描述/标签/状态/价格/按钮/图标等内部内容槽位;如果单个素材/图标/头像/徽标/按钮图形/图片内部需要居中或视觉重心判定,要用 `openprd visual-compare . --board <centering-board.json> --locale <zh-CN|en>` 拼成内部居中证据板,且同一张板里同时覆盖红色画布中心、绿色主体外接框和黄色视觉重心点,不能把单张截图当最终视觉证据。',
|
|
2810
3503
|
'如果参考图是一张整板、网格图或多对象组合图,先运行 `openprd visual-prepare . --reference <效果图> --grid <列>x<行>` 或 `--boxes <plan.json>`,确认 contact sheet 后,再决定是逐项 `--reference/--actual` 还是统一做 `focus-board` / `parallel-board`。',
|
|
2811
|
-
'
|
|
3504
|
+
'当用户后续说“跟效果图”“不一致”“好丑”“复刻”“没对齐”“偏心”“不居中”这类反馈时,不能只口头说已经对比过了;至少先产出一份 `openprd visual-compare`、`focus-board`、`parallel-board`、`verification-board`、`alignment-board` 或 `centering-board` 证据图再下结论。'
|
|
2812
3505
|
].join('\n');
|
|
2813
3506
|
}
|
|
2814
3507
|
|
|
2815
3508
|
function codexConfirmationReplyRule() {
|
|
2816
|
-
return 'Codex 回复规则: 只有当前真的还缺人来拍板时,才在 final answer
|
|
3509
|
+
return 'Codex 回复规则: 只有当前真的还缺人来拍板时,才在 final answer 里停下来请求确认。停下请求确认时执行“一次确认合同”:把本条需求后续本来会出现的全部确认点合并成一段话一次性说清,并告诉用户只要回一句“确认,继续执行”,剩余环节会自动连续推进;同一条需求不允许出现第二个人工确认停顿。绝对不要让用户粘贴 digest、work unit、版本号或任何 openprd 命令——这些值 hook 会自动绑定,需要时由你自己填进命令。只要用户已经明确说了“认可并继续”,或已经确认了 mini-plan、范围边界、正式产品边界,就直接沿着已确认路径继续,不要再写“如果你认可”“确认的话我就继续”这类二次索取确认的话。面向用户时,不要直接抛 lane、change、tasks、review artifact、work unit、digest、worker shard、write-scope、worktree、内部版本号、命令或文件路径这些内部词;统一改说“需求确认稿”“本次调整”“后续任务”“继续落地”“完成后检查”。如果后面真的还需要额外授权,再用人话把会做什么、不会做什么、完成后怎么检查说清楚。如果当前仍在 L2 的首轮澄清或需求摘要确认阶段,不要写成“你回我一句我就开始实现”;只能承诺“我先整理需求摘要给你确认,确认后再继续”。如果当前 lane 已进入 silent-record 策略,只能说明用户已经明确说了“不需要进行任何确认”;单纯的“请帮我实现/继续实现”不能把 lane 切到 silent-record。';
|
|
2817
3510
|
}
|
|
2818
3511
|
|
|
2819
3512
|
function confirmationGateMessage(gate) {
|
|
@@ -2826,7 +3519,7 @@ function confirmationGateMessage(gate) {
|
|
|
2826
3519
|
return [
|
|
2827
3520
|
intro,
|
|
2828
3521
|
'Implementation may proceed only within the confirmed scope, with docs/basic, file manuals, folder README docs, standards verification, and OpenPrd run verification kept up to date. For backend, script, agent, tooling, service, or data-processing changes, keep CLI and API surface review current in docs/basic/backend-structure.md.',
|
|
2829
|
-
'For UI or visual work with an existing reference image, first confirm the user has accepted that image as a later comparison reference; if one image contains multiple sub-images, grid cells, or objects, run openprd visual-prepare . --reference <effect-image> --grid <columns>x<rows> or --boxes <plan.json>, inspect the contact sheet, and use the generated reference-set / compare-plan before comparing. Then capture the implemented UI and run openprd visual-compare . --reference <effect-image> --actual <implementation-screenshot>; if local detail matters more than the whole screen, add openprd visual-compare . --board <focus-board.json> so the agent can review numbered zoom regions. When there is no reference image, capture the before screenshot first, implement, capture the after screenshot from the same entry, viewport, account, and data state, then run openprd visual-compare . --before <before-screenshot> --after <after-screenshot>; if the agent explored multiple optimization directions, add openprd visual-compare . --board <parallel-board.json> and
|
|
3522
|
+
'For UI or visual work with an existing reference image, first confirm the user has accepted that image as a later comparison reference; if one image contains multiple sub-images, grid cells, or objects, run openprd visual-prepare . --reference <effect-image> --grid <columns>x<rows> or --boxes <plan.json>, inspect the contact sheet, and use the generated reference-set / compare-plan before comparing. Then capture the implemented UI and run openprd visual-compare . --reference <effect-image> --actual <implementation-screenshot>; if local detail matters more than the whole screen, add openprd visual-compare . --board <focus-board.json> --locale <zh-CN|en> so the agent can review numbered zoom regions. When there is no reference image, capture the before screenshot first, implement, capture the after screenshot from the same entry, viewport, account, and data state, then run openprd visual-compare . --before <before-screenshot> --after <after-screenshot>; if the agent explored multiple optimization directions, add openprd visual-compare . --board <parallel-board.json> --locale <zh-CN|en>. If ordinary screenshots or Computer/Browser/Playwright checks are used as evidence, assemble them with the checked path and checkpoints through openprd visual-compare . --board <verification-board.json> --locale <zh-CN|en> before claiming visual completion. If the UI contains repeated homogeneous cards, lists, grids, or tables, or the user reports misalignment, capture the real screen, overlay guide lines, measure both container-track and internal content-slot x/y/size/baseline spread, and assemble an alignment-board through openprd visual-compare . --board <alignment-board.json> --locale <zh-CN|en>. If a single logo, icon, avatar, badge, button graphic, or image crop needs internal-centering or visual-centroid judgment, crop the target and assemble a centering-board through openprd visual-compare . --board <centering-board.json> --locale <zh-CN|en> before claiming it is centered.',
|
|
2830
3523
|
'If the user later says the implementation does not match the effect image, looks wrong, or asks for replication, do not rely on subjective narration alone; produce at least one visual evidence artifact before claiming completion.',
|
|
2831
3524
|
].join('\n');
|
|
2832
3525
|
}
|
|
@@ -2842,7 +3535,7 @@ function currentRequirementStatusLine(gate, progress) {
|
|
|
2842
3535
|
}
|
|
2843
3536
|
switch (progress?.nextStep) {
|
|
2844
3537
|
case 'clarification-confirmation-required':
|
|
2845
|
-
return '当前卡点:
|
|
3538
|
+
return '当前卡点: 先让用户确认当前需求摘要;在此之前不要把核心需求写成既定事实,也不要直接往后推进。请求确认时按“一次确认合同”表达:明确告诉用户这是本条需求默认唯一的一次人工确认,只要回复“确认,继续执行”,后面的需求稿整理、调整范围、任务拆解和实现都会自动连续推进,不会再逐环节找他确认。';
|
|
2846
3539
|
case 'clarification-writeback-required':
|
|
2847
3540
|
return '当前卡点: 用户已经确认这轮需求摘要或默认方案,但 current.json 里还没有把确认后的事实稳定写回。先按 canonical 字段路径用 user-confirmed 回写,再继续 synthesize;不要再回退成泛化 clarify,也不要继续写 agent-inferred。';
|
|
2848
3541
|
case 'product-type-classification-required':
|
|
@@ -2855,7 +3548,7 @@ function currentRequirementStatusLine(gate, progress) {
|
|
|
2855
3548
|
: '当前卡点: 当前需求摘要已确认,下一步把已确认内容整理成这版需求确认稿。';
|
|
2856
3549
|
case 'prd-review-required':
|
|
2857
3550
|
return progress?.review?.versionId
|
|
2858
|
-
? '当前卡点:
|
|
3551
|
+
? '当前卡点: 先等用户确认这版需求确认稿;不要把“继续做”“开落地吧”或单纯的“请帮我实现/继续实现”当成已经认可这版内容。请求确认时只需要用户回一句自然语言,例如“确认这版,继续执行”;版本、digest、work unit 由 hook 自动绑定,绝对不要让用户粘贴 digest、work unit 或任何 openprd 命令。同时说明这一句确认之后整条链会自动执行到实现,不会再停。'
|
|
2859
3552
|
: '当前卡点: 先整理出这版需求确认稿,再等待用户确认。';
|
|
2860
3553
|
case 'review-recording-required':
|
|
2861
3554
|
return '当前卡点: 这版需求确认稿已经生成,而且用户已明确表示不需要再停下来确认;可以先记录这次确认结果,再继续后面的整理。';
|
|
@@ -2957,16 +3650,16 @@ function contextMessage(cwd, intent = null, gate = null, progress = null) {
|
|
|
2957
3650
|
knowledgeSkillReminder(run.parsed),
|
|
2958
3651
|
'OpenPrd 上下文只是建议,不是自动执行指令。请先判断用户当前意图。',
|
|
2959
3652
|
lightweightRequirementMessage(intent),
|
|
2960
|
-
visualMockupMessage(intent),
|
|
2961
|
-
learningReviewMessage(),
|
|
3653
|
+
visualMockupMessage(intent, cwd),
|
|
3654
|
+
learningReviewMessage(intent),
|
|
2962
3655
|
largeUiVisualDirectionMessage(),
|
|
2963
3656
|
visualLocalAdjustmentMessage(),
|
|
2964
3657
|
requirementRoutingSummary(),
|
|
2965
3658
|
'如果用户只是要求看看、规划、分析、审查、解释影响或列出文件,请保持只读并基于证据回答;不要运行 OpenPrd loop、任务推进、discovery 推进、commit 或其他写入命令。',
|
|
2966
3659
|
'只有当用户当前明确要求开发、实现、修复、继续任务、深度调研、对标复刻或提交时,才运行 openprd loop --run、openprd tasks --advance、openprd discovery --advance、commit/push 等执行命令。',
|
|
2967
|
-
'代码修改完成后、最终回复前,针对本轮实际 touched code files 运行 openprd dev-check . <file
|
|
3660
|
+
'代码修改完成后、最终回复前,针对本轮实际 touched code files 运行 openprd dev-check . <file...>;若出现需要关注的文件:自动优化开启(默认)时,先在本轮完成高内聚低耦合拆分(不丢功能、行为不变、拆完重跑测试),最终回复以 **OpenPRD 自动优化报告** 为标题直接复用 dev-check 生成的 Markdown 表格(列:影响对象、问题级别、源文件规模、优化原因、本次处理结果、后续建议),并附自动优化开关确认一句话;自动优化关闭时,以 **后续建议** 为标题复用表格(列:影响对象、关注程度、规模信号、预警原因、本次处理结果、后续建议)。两种模式都按 🔴 → 🟠 → 🟡 排序;不要把“问题级别/关注程度”列改写成纯 emoji,必须保留例如“🟠 中风险|建议优先关注”这类完整标签;如果你改写了表格文案,先用 `node scripts/dev-check-wrapup-copy.mjs --validate` 校验每格不超过 20 字;若报错,按提示缩短后重试。',
|
|
2968
3661
|
'大界面改动的效果图先当候选效果图;出图后主动确认是否符合预期、是否纳入后续效果图/实现截图对比、是否按此继续实现。只有确认后,才把选定方向、整张图或其中子图整理到 `.openprd/harness/visual-reviews/` 并进入大 UI 实现。',
|
|
2969
|
-
'涉及界面、页面、视觉、样式或前端体验时,已有确认参考图才运行 openprd visual-compare . --reference <效果图> --actual
|
|
3662
|
+
'涉及界面、页面、视觉、样式或前端体验时,已有确认参考图才运行 openprd visual-compare . --reference <效果图> --actual <实现截图> --locale <zh-CN|en>;如果一张参考图里有多个子图/对象/网格,先运行 openprd visual-prepare . --reference <效果图> --grid <列>x<行> 或 --boxes <plan.json>,确认 contact sheet 后再逐项对比。没有明确参考图时先判断新建还是修改:新建界面走实现前 3 方向方案评审;修改既有界面则动手前先截修改前截图,完成后运行 openprd visual-compare . --before <修改前截图> --after <修改后截图> --locale <zh-CN|en>;如果并行试了多个优化方向,再补一份 openprd visual-compare . --board <parallel-board.json> --locale <zh-CN|en>。普通截图、Computer/Browser/Playwright 实测截图只能作为原始素材,收口前要补 openprd visual-compare . --board <verification-board.json> --locale <zh-CN|en> 的截图实测证据板。新开发或修改同构列表、卡片、网格、表格时,或用户反馈没对齐/排版漂移时,还要补 openprd visual-compare . --board <alignment-board.json> --locale <zh-CN|en> 的对齐辅助线证据板,同时量测容器轨道和标题/副标题/描述/标签/状态/价格/按钮/图标等内部内容槽位的 x/y/宽高/baseline spread。单个素材/图标/头像/徽标/按钮图形/图片内部居中、偏心或视觉重心判断时,还要补 openprd visual-compare . --board <centering-board.json> --locale <zh-CN|en> 的内部居中证据板,同时量测画布中心、主体外接框中心和视觉重心偏移。用户后续如果说“跟效果图”“不一致”“好丑”“复刻”,不能只口头说对比过了,至少产出一份 visual-compare / focus-board / parallel-board / verification-board / alignment-board / centering-board 证据图。',
|
|
2970
3663
|
'发现可沉淀项时不要中途打断任务:代码扩展识别这类白名单工具补全会自动应用并记录;用户偏好、项目协作规矩和 OpenPrd 默认行为先记录为候选,收工时运行 openprd grow . --review 集中确认。',
|
|
2971
3664
|
'维护 OpenPrd 本身且涉及配置类能力时,先判断是否应纳入 openprd grow;高置信可成长默认纳入,不确定则主动询问用户。',
|
|
2972
3665
|
'涉及后端、脚本、Agent、工具链、服务或数据处理变更时,把 CLI 与 API 视为同级接入面:同步检查命令入口、参数、输出契约、help/doctor/dry-run/status 与接口协议、返回结构、身份边界是否受影响,并更新 docs/basic/backend-structure.md 或明确写不适用原因。',
|
|
@@ -2979,16 +3672,16 @@ function contextMessage(cwd, intent = null, gate = null, progress = null) {
|
|
|
2979
3672
|
knowledgeSkillReminder(run.parsed),
|
|
2980
3673
|
'OpenPrd 上下文只是建议,不是自动执行指令。请先判断用户当前意图。',
|
|
2981
3674
|
lightweightRequirementMessage(intent),
|
|
2982
|
-
visualMockupMessage(intent),
|
|
2983
|
-
learningReviewMessage(),
|
|
3675
|
+
visualMockupMessage(intent, cwd),
|
|
3676
|
+
learningReviewMessage(intent),
|
|
2984
3677
|
largeUiVisualDirectionMessage(),
|
|
2985
3678
|
visualLocalAdjustmentMessage(),
|
|
2986
3679
|
requirementRoutingSummary(),
|
|
2987
3680
|
'如果用户只是要求看看、规划、分析、审查、解释影响或列出文件,请保持只读并基于证据回答;不要运行 OpenPrd loop、任务推进、discovery 推进、commit 或其他写入命令。',
|
|
2988
3681
|
'只有当用户当前明确要求开发、实现、修复、继续任务、深度调研、对标复刻或提交时,才运行 openprd loop --run、openprd tasks --advance、openprd discovery --advance、commit/push 等执行命令。',
|
|
2989
|
-
'代码修改完成后、最终回复前,针对本轮实际 touched code files 运行 openprd dev-check . <file
|
|
3682
|
+
'代码修改完成后、最终回复前,针对本轮实际 touched code files 运行 openprd dev-check . <file...>;若出现需要关注的文件:自动优化开启(默认)时,先在本轮完成高内聚低耦合拆分(不丢功能、行为不变、拆完重跑测试),最终回复以 **OpenPRD 自动优化报告** 为标题直接复用 dev-check 生成的 Markdown 表格(列:影响对象、问题级别、源文件规模、优化原因、本次处理结果、后续建议),并附自动优化开关确认一句话;自动优化关闭时,以 **后续建议** 为标题复用表格(列:影响对象、关注程度、规模信号、预警原因、本次处理结果、后续建议)。两种模式都按 🔴 → 🟠 → 🟡 排序;不要把“问题级别/关注程度”列改写成纯 emoji,必须保留例如“🟠 中风险|建议优先关注”这类完整标签;如果你改写了表格文案,先用 `node scripts/dev-check-wrapup-copy.mjs --validate` 校验每格不超过 20 字;若报错,按提示缩短后重试。',
|
|
2990
3683
|
'大界面改动的效果图先当候选效果图;出图后主动确认是否符合预期、是否纳入后续效果图/实现截图对比、是否按此继续实现。只有确认后,才把选定方向、整张图或其中子图整理到 `.openprd/harness/visual-reviews/` 并进入大 UI 实现。',
|
|
2991
|
-
'涉及界面、页面、视觉、样式或前端体验时,已有确认参考图才运行 openprd visual-compare . --reference <效果图> --actual
|
|
3684
|
+
'涉及界面、页面、视觉、样式或前端体验时,已有确认参考图才运行 openprd visual-compare . --reference <效果图> --actual <实现截图> --locale <zh-CN|en>;如果一张参考图里有多个子图/对象/网格,先运行 openprd visual-prepare . --reference <效果图> --grid <列>x<行> 或 --boxes <plan.json>,确认 contact sheet 后再逐项对比。没有明确参考图时先判断新建还是修改:新建界面走实现前 3 方向方案评审;修改既有界面则动手前先截修改前截图,完成后运行 openprd visual-compare . --before <修改前截图> --after <修改后截图> --locale <zh-CN|en>;如果并行试了多个优化方向,再补一份 openprd visual-compare . --board <parallel-board.json> --locale <zh-CN|en>。普通截图、Computer/Browser/Playwright 实测截图只能作为原始素材,收口前要补 openprd visual-compare . --board <verification-board.json> --locale <zh-CN|en> 的截图实测证据板。新开发或修改同构列表、卡片、网格、表格时,或用户反馈没对齐/排版漂移时,还要补 openprd visual-compare . --board <alignment-board.json> --locale <zh-CN|en> 的对齐辅助线证据板,同时量测容器轨道和标题/副标题/描述/标签/状态/价格/按钮/图标等内部内容槽位的 x/y/宽高/baseline spread。单个素材/图标/头像/徽标/按钮图形/图片内部居中、偏心或视觉重心判断时,还要补 openprd visual-compare . --board <centering-board.json> --locale <zh-CN|en> 的内部居中证据板,同时量测画布中心、主体外接框中心和视觉重心偏移。用户后续如果说“跟效果图”“不一致”“好丑”“复刻”,不能只口头说对比过了,至少产出一份 visual-compare / focus-board / parallel-board / verification-board / alignment-board / centering-board 证据图。',
|
|
2992
3685
|
'发现可沉淀项时不要中途打断任务:代码扩展识别这类白名单工具补全会自动应用并记录;用户偏好、项目协作规矩和 OpenPrd 默认行为先记录为候选,收工时运行 openprd grow . --review 集中确认。',
|
|
2993
3686
|
'维护 OpenPrd 本身且涉及配置类能力时,先判断是否应纳入 openprd grow;高置信可成长默认纳入,不确定则主动询问用户。',
|
|
2994
3687
|
'涉及后端、脚本、Agent、工具链、服务或数据处理变更时,把 CLI 与 API 视为同级接入面:同步检查命令入口、参数、输出契约、help/doctor/dry-run/status 与接口协议、返回结构、身份边界是否受影响,并更新 docs/basic/backend-structure.md 或明确写不适用原因。',
|
|
@@ -3006,11 +3699,12 @@ function contextMessage(cwd, intent = null, gate = null, progress = null) {
|
|
|
3006
3699
|
next.ok ? next.stdout : '',
|
|
3007
3700
|
gateMessage,
|
|
3008
3701
|
lightweightRequirementMessage(intent),
|
|
3009
|
-
visualMockupMessage(intent),
|
|
3702
|
+
visualMockupMessage(intent, cwd),
|
|
3703
|
+
learningReviewMessage(intent),
|
|
3010
3704
|
largeUiVisualDirectionMessage(intent),
|
|
3011
3705
|
requirementRoutingSummary(),
|
|
3012
3706
|
'OpenPrd 下一步只是建议。规划、分析、审查类请求保持只读;只有用户当前明确要求开发、深度调研、对标复刻或继续任务时才执行。',
|
|
3013
|
-
'代码修改完成后、最终回复前,针对本轮实际 touched code files 运行 openprd dev-check . <file
|
|
3707
|
+
'代码修改完成后、最终回复前,针对本轮实际 touched code files 运行 openprd dev-check . <file...>;若出现需要关注的文件:自动优化开启(默认)时,先在本轮完成高内聚低耦合拆分(不丢功能、行为不变、拆完重跑测试),最终回复以 **OpenPRD 自动优化报告** 为标题直接复用 dev-check 生成的 Markdown 表格(列:影响对象、问题级别、源文件规模、优化原因、本次处理结果、后续建议),并附自动优化开关确认一句话;自动优化关闭时,以 **后续建议** 为标题复用表格(列:影响对象、关注程度、规模信号、预警原因、本次处理结果、后续建议)。两种模式都按 🔴 → 🟠 → 🟡 排序;不要把“问题级别/关注程度”列改写成纯 emoji,必须保留例如“🟠 中风险|建议优先关注”这类完整标签;如果你改写了表格文案,先用 `node scripts/dev-check-wrapup-copy.mjs --validate` 校验每格不超过 20 字;若报错,按提示缩短后重试。',
|
|
3014
3708
|
'发现可沉淀项时不要中途打断任务:代码扩展识别这类白名单工具补全会自动应用并记录;用户偏好、项目协作规矩和 OpenPrd 默认行为先记录为候选,收工时运行 openprd grow . --review 集中确认。',
|
|
3015
3709
|
'维护 OpenPrd 本身且涉及配置类能力时,先判断是否应纳入 openprd grow;高置信可成长默认纳入,不确定则主动询问用户。',
|
|
3016
3710
|
'涉及后端、脚本、Agent、工具链、服务或数据处理变更时,把 CLI 与 API 视为同级接入面,并同步更新 docs/basic/backend-structure.md 或明确写不适用原因。',
|
|
@@ -3037,6 +3731,7 @@ function shouldInjectOpenPrdContext(payload) {
|
|
|
3037
3731
|
&& !intent.publicRepoResearchRequest
|
|
3038
3732
|
&& !intent.externalTechResearchRequest
|
|
3039
3733
|
&& !intent.skillWorkflowEditRequest
|
|
3734
|
+
&& !intent.learningMaterialRequest
|
|
3040
3735
|
) {
|
|
3041
3736
|
return false;
|
|
3042
3737
|
}
|
|
@@ -3091,6 +3786,11 @@ function classifyRisk(payload) {
|
|
|
3091
3786
|
/yarn\s+npm\s+publish/,
|
|
3092
3787
|
/gh\s+release/,
|
|
3093
3788
|
/rm\s+-rf/,
|
|
3789
|
+
/\b(?:ssh|scp|rsync)\b[^\n]*(?:prod|production|线上|生产|云端|server|remote)/,
|
|
3790
|
+
/\b(?:kubectl|helm)\b[^\n]*(?:apply|patch|set|delete|rollout|scale|exec)[^\n]*(?:prod|production|线上|生产|云端|cluster)/,
|
|
3791
|
+
/\b(?:vercel|wrangler|fly|flyctl|railway|firebase)\b[^\n]*(?:deploy|up)[^\n]*(?:--prod|prod|production|线上|生产)/,
|
|
3792
|
+
/\bsupabase\s+db\s+push\b[^\n]*(?:--linked|--db-url|prod|production|线上|生产)/,
|
|
3793
|
+
/\bcurl\b[^\n]*(?:-x|--request)\s*(?:post|put|patch|delete)\b[^\n]*(?:prod|production|线上|生产|云端|api\.)/,
|
|
3094
3794
|
/openprd\s+freeze\b/,
|
|
3095
3795
|
/openprd\s+handoff\b/,
|
|
3096
3796
|
/openprd\s+change\s+.*--apply/,
|
|
@@ -3224,6 +3924,102 @@ function hasRecentArtifacts(root, relativeDir, turnState, matcher) {
|
|
|
3224
3924
|
return collectRecentArtifactPaths(dir, startedAtMs, matcher, []).length > 0;
|
|
3225
3925
|
}
|
|
3226
3926
|
|
|
3927
|
+
function hasRecentVisualReviewArtifact(root, turnState) {
|
|
3928
|
+
return hasRecentArtifacts(root, '.openprd/harness/visual-reviews', turnState, (artifactPath) => {
|
|
3929
|
+
const normalized = artifactPath.split(path.sep).join('/');
|
|
3930
|
+
return /\.(?:jpe?g|png|webp|json)$/i.test(normalized)
|
|
3931
|
+
&& /visual-(?:compare|before-after|focus-board|parallel-board|verification-board|alignment-board|centering-board)|reference-actual|before-after|focus-board|parallel-board|verification-board|alignment-board|centering-board|center-board/i.test(normalized);
|
|
3932
|
+
});
|
|
3933
|
+
}
|
|
3934
|
+
|
|
3935
|
+
function isVisualEvidenceIntent(intent = null) {
|
|
3936
|
+
const text = String(intent?.promptText || '');
|
|
3937
|
+
return isFrontendTaskIntent(intent)
|
|
3938
|
+
|| hasLightweightUiVisualSignal(text)
|
|
3939
|
+
|| /(界面|页面|前端|视觉|样式|布局|信息架构|效果图|设计稿|参考图|实现截图|视觉对比|视觉评审|截图|实测|Computer\s*Use|Browser|Playwright|复刻|不一致|好丑|没对齐|没有对齐|不对齐|对不齐|对不上|不像|对齐有问题|对齐问题|排版漂移|布局漂移|左右偏差|上下偏差|横向偏差|竖向偏差|同构|重复单元|列表卡片|卡片列表|卡片网格|网格卡片|网格列表|列表网格|对齐辅助线|内容槽位|内部槽位|内部居中|视觉重心|主体外接框|画布中心|偏心|不居中|没居中|图标内部|图片内部|素材内部)/i.test(text);
|
|
3940
|
+
}
|
|
3941
|
+
|
|
3942
|
+
function visualEvidenceStopReminder(root, turnState, stopIntent) {
|
|
3943
|
+
if (stopIntent?.visualMockupRequest) {
|
|
3944
|
+
return null;
|
|
3945
|
+
}
|
|
3946
|
+
const touchedFiles = Array.isArray(turnState?.touchedFiles) ? turnState.touchedFiles : [];
|
|
3947
|
+
const frontendTouched = touchedFiles.some((file) => looksLikeFrontendImplementationFile(file));
|
|
3948
|
+
const rawScreenshotObserved = hasTurnReviewSignal(turnState, 'visual-raw-screenshot');
|
|
3949
|
+
const visualArtifactObserved = hasTurnReviewSignal(turnState, 'visual-review-artifact')
|
|
3950
|
+
|| hasTurnReviewSignal(turnState, 'visual-verification-board')
|
|
3951
|
+
|| hasTurnReviewSignal(turnState, 'visual-alignment-board')
|
|
3952
|
+
|| hasTurnReviewSignal(turnState, 'visual-centering-board')
|
|
3953
|
+
|| hasRecentVisualReviewArtifact(root, turnState);
|
|
3954
|
+
const needsVisualEvidence = isVisualEvidenceIntent(stopIntent)
|
|
3955
|
+
&& (rawScreenshotObserved || frontendTouched || /截图|实测|Computer\s*Use|Browser|Playwright/i.test(String(stopIntent?.promptText || '')));
|
|
3956
|
+
if (!needsVisualEvidence || visualArtifactObserved) {
|
|
3957
|
+
return null;
|
|
3958
|
+
}
|
|
3959
|
+
const preferred = rawScreenshotObserved
|
|
3960
|
+
? '`openprd visual-compare . --board <verification-board.json> --locale <zh-CN|en>`,把普通截图、Computer/Browser/Playwright 实测路径和检查点拼成截图实测证据板'
|
|
3961
|
+
: '有参考图时用 `--reference/--actual`,无参考且改既有界面用 `--before/--after`,普通截图实测用 `--board <verification-board.json>`,同构列表/卡片/网格/表格用 `--board <alignment-board.json>`,单元素内部居中/视觉重心用 `--board <centering-board.json>`';
|
|
3962
|
+
return [
|
|
3963
|
+
'OpenPrd 在本轮收工回顾里发现 UI/视觉任务已有截图、实测或前端改动,但还没有本轮拼图证据。',
|
|
3964
|
+
'即便只是卡片宽度、间距、留白、对齐、颜色、圆角、字号这类轻量可视优化,也需要留下视觉证据;build、package 和 dev-check 不能替代视觉收口。',
|
|
3965
|
+
'如果这轮新开发或修改了同构列表、卡片、网格、表格,或用户反馈元素没对齐,要截真实页面、叠辅助线,同时量容器轨道和标题/副标题/描述/标签/状态/价格/按钮/图标等内部内容槽位的 spread,并生成 alignment-board;只量外框、列宽或行顶不算完整对齐验收。',
|
|
3966
|
+
'如果这轮判断的是单个素材、图标、头像、徽标、按钮图形或图片内部是否居中,要截取或裁出目标元素,用 centering-board 量画布中心、主体外接框中心和视觉重心偏移;单张截图或主观“看起来居中”不算完整居中验收。',
|
|
3967
|
+
'视觉证据还要检查本轮审美意图是否成立:气质、层级、颜色、字号、间距、表面角色和记忆点不能只靠口头描述。',
|
|
3968
|
+
`普通截图和 Computer 实测截图只能作为原始素材,不能单独替代视觉收口。请先运行 ${preferred}。`,
|
|
3969
|
+
'补齐前不要宣称界面视觉已经完成;可以如实说功能或代码已改,但视觉拼图证据还没补齐。',
|
|
3970
|
+
].join('\n');
|
|
3971
|
+
}
|
|
3972
|
+
|
|
3973
|
+
function recentLearningArtifactPaths(root, turnState, matcher) {
|
|
3974
|
+
const dir = path.join(root, '.openprd/learning/archive');
|
|
3975
|
+
const startedAtMs = parseShanghaiTimestamp(turnState?.startedAt);
|
|
3976
|
+
return collectRecentArtifactPaths(dir, startedAtMs, matcher, []);
|
|
3977
|
+
}
|
|
3978
|
+
|
|
3979
|
+
function hasRecentLearningSkeletonArtifact(root, turnState) {
|
|
3980
|
+
return recentLearningArtifactPaths(root, turnState, (artifactPath) => {
|
|
3981
|
+
const normalized = artifactPath.split(path.sep).join('/');
|
|
3982
|
+
return /\/(?:agent-prompt\.md|reader\.html|learning-package\.json|learning-content\.json)$/i.test(normalized);
|
|
3983
|
+
}).length > 0;
|
|
3984
|
+
}
|
|
3985
|
+
|
|
3986
|
+
function hasRecentAuthoredLearningReaderArtifact(root, turnState) {
|
|
3987
|
+
const contentPaths = recentLearningArtifactPaths(root, turnState, (artifactPath) => artifactPath.endsWith('learning-content.json'));
|
|
3988
|
+
return contentPaths.some((contentPath) => {
|
|
3989
|
+
const content = readJsonSync(contentPath, null);
|
|
3990
|
+
if (!content || content.authoringStatus !== 'agent-authored') {
|
|
3991
|
+
return false;
|
|
3992
|
+
}
|
|
3993
|
+
if (!Array.isArray(content.chapters) || content.chapters.length === 0) {
|
|
3994
|
+
return false;
|
|
3995
|
+
}
|
|
3996
|
+
const readerPath = path.join(path.dirname(contentPath), 'reader.html');
|
|
3997
|
+
return fs.existsSync(readerPath);
|
|
3998
|
+
});
|
|
3999
|
+
}
|
|
4000
|
+
|
|
4001
|
+
function learningMaterialStopReminder(root, turnState, stopIntent) {
|
|
4002
|
+
if (!stopIntent?.learningMaterialRequest) {
|
|
4003
|
+
return null;
|
|
4004
|
+
}
|
|
4005
|
+
if (hasRecentAuthoredLearningReaderArtifact(root, turnState)) {
|
|
4006
|
+
return null;
|
|
4007
|
+
}
|
|
4008
|
+
const command = learningReviewCommand(stopIntent);
|
|
4009
|
+
if (!hasRecentLearningSkeletonArtifact(root, turnState)) {
|
|
4010
|
+
return [
|
|
4011
|
+
'OpenPrd 在本轮收工回顾里识别到用户要的是学习型交付物,但还没有看到本轮新生成的学习包阅读器。',
|
|
4012
|
+
'请先运行 `' + command + '`,生成 `.openprd/learning/archive/<packageId>/` 下的证据清单、Agent 写作提示、learning-content.json 骨架和 reader.html。',
|
|
4013
|
+
'这类请求按产物形态和长期阅读/教学目的判断,不是固定关键词表;普通 Markdown 只能作为辅助讲义,不能替代 OpenPrd reader。',
|
|
4014
|
+
].join('\n');
|
|
4015
|
+
}
|
|
4016
|
+
return [
|
|
4017
|
+
'OpenPrd 在本轮收工回顾里发现学习包骨架已经出现,但还没有本轮 Agent-authored 的最终 reader 内容。',
|
|
4018
|
+
'请读取 `.openprd/learning/archive/<packageId>/agent-prompt.md`,基于证据写入 `learning-content.json`,再运行 `openprd learn . --content-json <file> --open` 重新渲染。',
|
|
4019
|
+
'完成前不要把聊天里的普通 Markdown 成稿当作学习材料交付完成。',
|
|
4020
|
+
].join('\n');
|
|
4021
|
+
}
|
|
4022
|
+
|
|
3227
4023
|
function closeoutVerificationReminder(root, turnState, stopIntent) {
|
|
3228
4024
|
const touchedFiles = Array.isArray(turnState?.touchedFiles) ? turnState.touchedFiles : [];
|
|
3229
4025
|
if (touchedFiles.length === 0) {
|
|
@@ -3287,21 +4083,32 @@ function blockHook(reason) {
|
|
|
3287
4083
|
|
|
3288
4084
|
function handle(eventName, cwd, payload) {
|
|
3289
4085
|
const root = findProjectRoot(cwd);
|
|
4086
|
+
const sessionId = sessionIdFor(payload);
|
|
4087
|
+
const automationDecision = automationBypassDecision(root, eventName, payload, sessionId);
|
|
4088
|
+
if (automationDecision.optIn) {
|
|
4089
|
+
rememberAutomationMode(root, sessionId, payload, automationDecision.signals, 'openprd-maintenance');
|
|
4090
|
+
} else if (automationDecision.bypass) {
|
|
4091
|
+
rememberAutomationMode(root, sessionId, payload, automationDecision.signals, 'automation');
|
|
4092
|
+
return allowHook();
|
|
4093
|
+
}
|
|
3290
4094
|
ensureHarness(root);
|
|
3291
4095
|
const risk = classifyRisk(payload);
|
|
3292
4096
|
const fingerprint = fingerprintFor(eventName, payload, risk);
|
|
3293
4097
|
const duplicate = isDuplicate(root, fingerprint);
|
|
3294
|
-
const
|
|
4098
|
+
const runtimeEnvironment = recordRuntimeEnvironment(root, eventName, payload, sessionId);
|
|
3295
4099
|
const baseEvent = {
|
|
3296
4100
|
eventName,
|
|
3297
4101
|
risk,
|
|
3298
4102
|
fingerprint,
|
|
3299
4103
|
duplicate,
|
|
3300
4104
|
sessionId,
|
|
4105
|
+
runtimeEnvironment: runtimeEnvironment.active,
|
|
3301
4106
|
preview: preview(payloadText(payload)),
|
|
3302
4107
|
};
|
|
3303
4108
|
|
|
3304
4109
|
if (eventName === 'SessionStart') {
|
|
4110
|
+
appendEvent(root, { ...baseEvent, outcome: 'session-start' });
|
|
4111
|
+
updateHookState(root, baseEvent);
|
|
3305
4112
|
return allowHook();
|
|
3306
4113
|
}
|
|
3307
4114
|
|
|
@@ -3371,12 +4178,45 @@ function handle(eventName, cwd, payload) {
|
|
|
3371
4178
|
updateHookState(root, baseEvent);
|
|
3372
4179
|
return allowHook(composeHookContext(root, intent, gate, progress, sessionId));
|
|
3373
4180
|
}
|
|
4181
|
+
const repeatedConfirmationSignal = intent.confirmation
|
|
4182
|
+
|| shortAffirmative
|
|
4183
|
+
|| (clarificationFollowup && /(确认|继续|落地|开始|推进|按(这个|你的)|就这么|开做)/.test(stripMarkdown(prompt)));
|
|
4184
|
+
if (
|
|
4185
|
+
repeatedConfirmationSignal
|
|
4186
|
+
&& intent.reviewDecision !== 'needs-revision'
|
|
4187
|
+
&& !intent.reviewCommand
|
|
4188
|
+
&& gateHasClarificationConfirmation(gate)
|
|
4189
|
+
&& !reviewPolicyAllowsSilentRecord(requirementApprovalPolicy(gate))
|
|
4190
|
+
) {
|
|
4191
|
+
gate = escalateRequirementGateApproval(root, prompt, gate, sessionId);
|
|
4192
|
+
progress = evaluateRequirementGateProgress(root, sessionId);
|
|
4193
|
+
if (canAutoAuthorizeRequirementExecution(gate, progress)) {
|
|
4194
|
+
gate = authorizeRequirementGateExecution(root, prompt, sessionId, {
|
|
4195
|
+
reason: 'repeated-user-confirmation-after-summary',
|
|
4196
|
+
});
|
|
4197
|
+
progress = null;
|
|
4198
|
+
}
|
|
4199
|
+
appendEvent(root, { ...baseEvent, outcome: 'requirement-gate-approval-escalated' });
|
|
4200
|
+
recordRunHook(root, baseEvent, 'requirement-gate-approval-escalated');
|
|
4201
|
+
updateHookState(root, baseEvent);
|
|
4202
|
+
return allowHook(composeHookContext(root, intent, gate, progress, sessionId));
|
|
4203
|
+
}
|
|
3374
4204
|
if (intent.reviewDecision) {
|
|
3375
4205
|
const authorization = reviewActionAuthorizationFor(intent, progress, prompt);
|
|
3376
4206
|
gate = holdRequirementGate(root, prompt, progress, sessionId, {
|
|
3377
4207
|
status: authorization ? 'review-confirmation-authorized' : progress.nextStep,
|
|
3378
4208
|
reviewActionAuthorization: authorization,
|
|
3379
4209
|
});
|
|
4210
|
+
if (
|
|
4211
|
+
authorization
|
|
4212
|
+
&& intent.reviewDecision === 'confirmed'
|
|
4213
|
+
&& (authorization.continueAfterReview || gateHasClarificationConfirmation(gate))
|
|
4214
|
+
) {
|
|
4215
|
+
// 用户确认评审稿时要么同句要求继续执行,要么此前已确认过需求摘要(重复确认):
|
|
4216
|
+
// 一次确认解锁整条链,后续 change/tasks/实现不再停下索取确认。
|
|
4217
|
+
gate = escalateRequirementGateApproval(root, prompt, gate, sessionId);
|
|
4218
|
+
progress = evaluateRequirementGateProgress(root, sessionId);
|
|
4219
|
+
}
|
|
3380
4220
|
const outcome = authorization ? 'requirement-gate-review-authorized' : 'requirement-gate-awaiting-review';
|
|
3381
4221
|
appendEvent(root, {
|
|
3382
4222
|
...baseEvent,
|
|
@@ -3453,6 +4293,14 @@ function handle(eventName, cwd, payload) {
|
|
|
3453
4293
|
].join('\n'));
|
|
3454
4294
|
}
|
|
3455
4295
|
if (researchGate?.active && isMutationPayload(payload, risk) && !authorizedReviewMutation) {
|
|
4296
|
+
const researchBlockStreak = bumpProcessGateBlockCount(root, 'external-evidence');
|
|
4297
|
+
if (researchBlockStreak > PROCESS_GATE_FUSE_LIMIT) {
|
|
4298
|
+
appendEvent(root, { ...baseEvent, outcome: 'research-gate-fuse-open' });
|
|
4299
|
+
recordRunHook(root, baseEvent, 'research-gate-fuse-open');
|
|
4300
|
+
updateHookState(root, baseEvent);
|
|
4301
|
+
recordTouchedFiles(root, payload);
|
|
4302
|
+
return allowHook(processGateFuseMessage('external-evidence', researchBlockStreak, researchGateMessage(researchGate)));
|
|
4303
|
+
}
|
|
3456
4304
|
appendEvent(root, { ...baseEvent, outcome: 'blocked-research-gate' });
|
|
3457
4305
|
recordRunHook(root, baseEvent, 'blocked-research-gate');
|
|
3458
4306
|
updateHookState(root, baseEvent);
|
|
@@ -3470,6 +4318,13 @@ function handle(eventName, cwd, payload) {
|
|
|
3470
4318
|
'Use the `secrets-vault` skill first and only read the minimum required fields; do not read the original vault file directly.',
|
|
3471
4319
|
].join('\n'));
|
|
3472
4320
|
}
|
|
4321
|
+
const autonomyRisk = detectHighRiskAutonomyMutation(root, payload, turnIntent, risk);
|
|
4322
|
+
if (autonomyRisk && !hasExplicitAutonomyRiskApproval(turnIntent)) {
|
|
4323
|
+
appendEvent(root, { ...baseEvent, outcome: 'blocked-high-risk-autonomy', autonomyRisk });
|
|
4324
|
+
recordRunHook(root, baseEvent, 'blocked-high-risk-autonomy');
|
|
4325
|
+
updateHookState(root, baseEvent);
|
|
4326
|
+
return blockHook(formatHighRiskAutonomyBlock(autonomyRisk));
|
|
4327
|
+
}
|
|
3473
4328
|
if (isBlockingRequirementGate(gate) && isMutationPayload(payload, risk) && !isAllowedDuringRequirementGate(root, payload, gate, sessionId)) {
|
|
3474
4329
|
const reviewMark = parseReviewMarkCommand(commandText(payload));
|
|
3475
4330
|
const approvalPolicy = requirementApprovalPolicy(gate);
|
|
@@ -3482,13 +4337,13 @@ function handle(eventName, cwd, payload) {
|
|
|
3482
4337
|
: 'OpenPrd blocked review status writing because the current user message did not explicitly confirm this exact PRD review artifact.',
|
|
3483
4338
|
silentRecord
|
|
3484
4339
|
? 'This lane is in silent-record mode: you may record only the exact current version, digest, and work unit, then continue without another user stop.'
|
|
3485
|
-
: '
|
|
4340
|
+
: 'A plain natural-language user confirmation of the current review (for example "确认这版,继续") is sufficient; the hook binds it to the exact version, digest, and work unit automatically. NEVER ask the user to paste a digest, work-unit id, or a full openprd command.',
|
|
3486
4341
|
progress?.review?.versionId
|
|
3487
|
-
? `Current review artifact: version ${progress.review.versionId}, digest ${progress.review.digest}, work unit ${progress.review.workUnitId}.`
|
|
4342
|
+
? `Current review artifact: version ${progress.review.versionId}, digest ${progress.review.digest}, work unit ${progress.review.workUnitId}. Fill these exact values into the review --mark command yourself; do not ask the user for them.`
|
|
3488
4343
|
: 'Current review artifact has not been synthesized yet. Run openprd synthesize . --open first.',
|
|
3489
4344
|
silentRecord
|
|
3490
4345
|
? 'Do not mark any stale or different review artifact; only the exact current artifact is allowed.'
|
|
3491
|
-
: 'Implementation approval and review confirmation are different gates; do not treat "可以开做", plain "请帮我实现/继续实现", or similar wording as permission to run openprd review --mark confirmed.',
|
|
4346
|
+
: 'Implementation approval and review confirmation are different gates; do not treat "可以开做", plain "请帮我实现/继续实现", or similar wording as permission to run openprd review --mark confirmed. But if the latest user message did confirm this review, retry the mark command with the exact current artifact values above instead of asking the user again.',
|
|
3492
4347
|
].filter(Boolean).join('\n')
|
|
3493
4348
|
: [
|
|
3494
4349
|
'OpenPrd blocked a mutating action because the requirement gate is still active.',
|
|
@@ -3512,6 +4367,14 @@ function handle(eventName, cwd, payload) {
|
|
|
3512
4367
|
? 'Decision-point order: because the user explicitly waived any confirmation stop, you may skip requirement-summary confirmation, write back requirement facts, synthesize the PRD, record the exact stable review artifact, generate the OpenPrd change, prepare the task breakdown, then implement within the confirmed scope.'
|
|
3513
4368
|
: 'Decision-point order: clarify the requirement, summarize it in chat using 需求判断 / 需求理解 / 功能范围 / 技术方案, prefer Markdown tables for 功能范围 and 技术方案, and if the ask still looks like 0-to-1 validation also surface 第一批最容易触达的社区或种子用户、你为什么算这个社区里的自己人、当前替代方案和痛点证据、先怎么手工交付、手工作战卡怎么写、能不能先用 spreadsheet / 表单 / no-code 跑起来、什么承诺才算真需求、有没有 10 个样本和更强付费信号、最低成本先验证什么、达到什么条件才允许产品化、增长阶段守什么纪律、以及验证阶段怎样先活下来; wait for the user to confirm that requirement summary, write back only confirmed facts, synthesize the PRD, wait for a human review decision on the stable artifact, generate the OpenPrd change, prepare the task breakdown, then implement within the confirmed scope.',
|
|
3514
4369
|
].join('\n');
|
|
4370
|
+
const requirementBlockStreak = bumpProcessGateBlockCount(root, 'requirement-intake');
|
|
4371
|
+
if (requirementBlockStreak > PROCESS_GATE_FUSE_LIMIT) {
|
|
4372
|
+
appendEvent(root, { ...baseEvent, outcome: 'requirement-gate-fuse-open' });
|
|
4373
|
+
recordRunHook(root, baseEvent, 'requirement-gate-fuse-open');
|
|
4374
|
+
updateHookState(root, baseEvent);
|
|
4375
|
+
recordTouchedFiles(root, payload);
|
|
4376
|
+
return allowHook(processGateFuseMessage('requirement-intake', requirementBlockStreak, reason));
|
|
4377
|
+
}
|
|
3515
4378
|
appendEvent(root, { ...baseEvent, outcome: 'blocked-requirement-intake' });
|
|
3516
4379
|
recordRunHook(root, baseEvent, 'blocked-requirement-intake');
|
|
3517
4380
|
updateHookState(root, baseEvent);
|
|
@@ -3538,6 +4401,18 @@ function handle(eventName, cwd, payload) {
|
|
|
3538
4401
|
lastFocusFiles: extractTouchedFiles(root, payload),
|
|
3539
4402
|
}, sessionId) || patchModeGate;
|
|
3540
4403
|
} else {
|
|
4404
|
+
const patchModeBlockStreak = bumpProcessGateBlockCount(root, 'patch-mode');
|
|
4405
|
+
if (patchModeBlockStreak > PROCESS_GATE_FUSE_LIMIT) {
|
|
4406
|
+
appendEvent(root, { ...baseEvent, outcome: 'patch-mode-gate-fuse-open' });
|
|
4407
|
+
recordRunHook(root, baseEvent, 'patch-mode-gate-fuse-open');
|
|
4408
|
+
updateHookState(root, baseEvent);
|
|
4409
|
+
recordTouchedFiles(root, payload);
|
|
4410
|
+
return allowHook(processGateFuseMessage(
|
|
4411
|
+
'patch-mode',
|
|
4412
|
+
patchModeBlockStreak,
|
|
4413
|
+
`下一步仍应尽快真实写入 ${patchModeGate.targetFile} 或它的 sibling draft。`,
|
|
4414
|
+
));
|
|
4415
|
+
}
|
|
3541
4416
|
appendEvent(root, {
|
|
3542
4417
|
...baseEvent,
|
|
3543
4418
|
outcome: 'blocked-patch-mode-hover',
|
|
@@ -3566,6 +4441,18 @@ function handle(eventName, cwd, payload) {
|
|
|
3566
4441
|
const frontendTargets = frontendImplementationTargets(root, payload);
|
|
3567
4442
|
const designIssues = frontendDesignPreflightIssues(root, turnIntent, frontendTargets);
|
|
3568
4443
|
if (designIssues.length > 0) {
|
|
4444
|
+
const designBlockStreak = bumpProcessGateBlockCount(root, 'frontend-design-preflight');
|
|
4445
|
+
if (designBlockStreak > PROCESS_GATE_FUSE_LIMIT) {
|
|
4446
|
+
appendEvent(root, { ...baseEvent, outcome: 'frontend-design-gate-fuse-open' });
|
|
4447
|
+
recordRunHook(root, baseEvent, 'frontend-design-gate-fuse-open');
|
|
4448
|
+
updateHookState(root, baseEvent);
|
|
4449
|
+
recordTouchedFiles(root, payload);
|
|
4450
|
+
return allowHook(processGateFuseMessage(
|
|
4451
|
+
'frontend-design-preflight',
|
|
4452
|
+
designBlockStreak,
|
|
4453
|
+
designIssues.map((issue) => `- ${issue.file}: ${issue.reason}`).join('\n'),
|
|
4454
|
+
));
|
|
4455
|
+
}
|
|
3569
4456
|
appendEvent(root, { ...baseEvent, outcome: 'blocked-frontend-design-preflight' });
|
|
3570
4457
|
recordRunHook(root, baseEvent, 'blocked-frontend-design-preflight');
|
|
3571
4458
|
updateHookState(root, baseEvent);
|
|
@@ -3598,7 +4485,7 @@ function handle(eventName, cwd, payload) {
|
|
|
3598
4485
|
recordRunHook(root, baseEvent, 'allowed-medium-risk');
|
|
3599
4486
|
updateHookState(root, baseEvent);
|
|
3600
4487
|
recordTouchedFiles(root, payload);
|
|
3601
|
-
return allowHook('OpenPrd 检测到写入动作。本轮写入完成后、最终回复前,请针对实际 touched code files 运行 openprd dev-check . <file
|
|
4488
|
+
return allowHook('OpenPrd 检测到写入动作。本轮写入完成后、最终回复前,请针对实际 touched code files 运行 openprd dev-check . <file...>;如出现需要关注的文件:自动优化开启(默认)时先在本轮完成高内聚低耦合拆分(不丢功能、行为不变),最终回复以 **OpenPRD 自动优化报告** 为标题复用 dev-check 表格(列:影响对象、问题级别、源文件规模、优化原因、本次处理结果、后续建议)并附开关确认;关闭时以 **后续建议** 为标题复用表格(列:影响对象、关注程度、规模信号、预警原因、本次处理结果、后续建议)。两种模式都按 🔴 → 🟠 → 🟡 排序;不要把“问题级别/关注程度”列改写成纯 emoji,必须保留例如“🟠 中风险|建议优先关注”这类完整标签;如果你改写了表格文案,先用 `node scripts/dev-check-wrapup-copy.mjs --validate` 校验每格不超过 20 字;若报错,按提示缩短后重试;如涉及界面视觉,包括卡片宽度、间距、留白、对齐、颜色、圆角、字号等轻量可视优化,build、package 和 dev-check 只能证明代码或构建状态,不能替代视觉证据;写入前先说明本次审美意图和记忆点,视觉证据也要检查气质、层级、颜色、字号、间距和表面角色是否成立;已有参考效果图并进入实现阶段时,阶段性完成后运行 openprd visual-compare . --reference <效果图> --actual <实现截图> --locale <zh-CN|en> 并查看 JPG 对比图;如果参考图来自本轮或前序 `imagegen`,先确认用户已接受它作为后续对比参考;若一张图里有多个子图、对象或网格,先运行 openprd visual-prepare . --reference <效果图> --grid <列>x<行> 或 --boxes <plan.json>,确认 contact sheet 后再逐项对比。若局部细节更重要,再补 openprd visual-compare . --board <focus-board.json> --locale <zh-CN|en>;如无参考图,先判断新建界面还是修改既有界面:新建界面应先完成 3 方向方案评审,修改既有界面确认已先截修改前截图,并在完成后运行 openprd visual-compare . --before <修改前截图> --after <修改后截图> --locale <zh-CN|en> 查看 JPG 自检图;若并行试了多个优化方向,再补 openprd visual-compare . --board <parallel-board.json> --locale <zh-CN|en>;若普通截图、Computer/Browser/Playwright 实测截图被用作证据,再补 openprd visual-compare . --board <verification-board.json> --locale <zh-CN|en> 的截图实测证据板;若新开发或修改同构列表、卡片、网格、表格,或用户反馈没对齐/排版漂移,再补 openprd visual-compare . --board <alignment-board.json> --locale <zh-CN|en> 的对齐辅助线证据板,截真实页面、叠辅助线,同时量容器轨道和标题/副标题/描述/标签/状态/价格/按钮/图标等内部内容槽位的 x/y/宽高/baseline spread;只量外框、列宽或行顶不算完整对齐验收;当用户说“跟效果图”“不一致”“好丑”“复刻”时,至少给出一份视觉证据图,不要只口头判断。发现可沉淀项时不要中途打断任务,代码扩展识别这类白名单工具补全会自动应用并记录,用户偏好、项目协作规矩和 OpenPrd 默认行为留到收工时用 openprd grow . --review 集中确认;维护 OpenPrd 本身且涉及配置类能力时,先判断是否应纳入 openprd grow;声明就绪前,请同步维护 docs/basic、文件说明书、文件夹 README,以及相关 OpenPrd change/task 状态;如果涉及后端、脚本、Agent、工具链、服务或数据处理变更,还要把 CLI 与 API 视为同级接入面并更新 docs/basic/backend-structure.md。');
|
|
3602
4489
|
}
|
|
3603
4490
|
return allowHook();
|
|
3604
4491
|
}
|
|
@@ -3606,6 +4493,9 @@ function handle(eventName, cwd, payload) {
|
|
|
3606
4493
|
if (eventName === 'PostToolUse') {
|
|
3607
4494
|
const text = payloadText(payload);
|
|
3608
4495
|
const failed = /command not found|no such file|permission denied|failed|error|exception/i.test(text);
|
|
4496
|
+
if (!failed) {
|
|
4497
|
+
recordVisualEvidenceSignals(root, payload);
|
|
4498
|
+
}
|
|
3609
4499
|
if (!failed) {
|
|
3610
4500
|
return allowHook();
|
|
3611
4501
|
}
|
|
@@ -3637,8 +4527,8 @@ function handle(eventName, cwd, payload) {
|
|
|
3637
4527
|
}
|
|
3638
4528
|
if (stopIntent.visualMockupRequest && (!Array.isArray(turnState.touchedFiles) || turnState.touchedFiles.length === 0)) {
|
|
3639
4529
|
return allowHook([
|
|
3640
|
-
'OpenPrd
|
|
3641
|
-
'
|
|
4530
|
+
'OpenPrd 生图事实对齐提醒:如果这轮要汇报图片结果、失败或限流,请确保它来自一次实际的生图工具调用(Codex=`imagegen`,Cursor=`GenerateImage`,或用户已确认的偏好工具)。',
|
|
4531
|
+
'未实际调用生图工具前,不要声称“生图限流”“生图失败”或“已经生成图片结果”;若本轮还没调用,请如实说明仍未开始或尚未完成生图。',
|
|
3642
4532
|
'如果这轮确实已经生图,不要默认把结果登记到 `.openprd/harness/visual-reviews/` 或直接当成验收参考;先把它当候选效果图。',
|
|
3643
4533
|
'若用户还要继续做实现,主动追问三件事:是否符合预期、是否纳入后续效果图/实现截图对比、是否按此继续后续实现;未确认前不要自动进入实现,也不要声称参考图已定。',
|
|
3644
4534
|
].join('\n'));
|
|
@@ -3664,6 +4554,14 @@ function handle(eventName, cwd, payload) {
|
|
|
3664
4554
|
].join('\n'));
|
|
3665
4555
|
}
|
|
3666
4556
|
}
|
|
4557
|
+
const visualReminder = visualEvidenceStopReminder(root, turnState, stopIntent);
|
|
4558
|
+
if (visualReminder) {
|
|
4559
|
+
return allowHook(visualReminder);
|
|
4560
|
+
}
|
|
4561
|
+
const learningReminder = learningMaterialStopReminder(root, turnState, stopIntent);
|
|
4562
|
+
if (learningReminder) {
|
|
4563
|
+
return allowHook(learningReminder);
|
|
4564
|
+
}
|
|
3667
4565
|
const devCheckMessage = devCheckWrapUpMessage(root, turnState);
|
|
3668
4566
|
if (devCheckMessage) {
|
|
3669
4567
|
return allowHook(devCheckMessage);
|