@openprd/cli 0.1.0
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/README.md +82 -0
- package/.openprd/benchmarks/evidence/milvus-io-ai-code-review-gets-better-when-models-debate-claude-vs-gemini-vs-code.md +14 -0
- package/.openprd/benchmarks/evidence/nolanlawson-com-using-ai-to-write-better-code-more-slowly.md +14 -0
- package/.openprd/benchmarks/index.md +37 -0
- package/.openprd/benchmarks/sources.yaml +56 -0
- package/.openprd/config.yaml +50 -0
- package/.openprd/discovery/config.json +21 -0
- package/.openprd/engagements/active/flows.md +30 -0
- package/.openprd/engagements/active/handoff.md +9 -0
- package/.openprd/engagements/active/intake.md +15 -0
- package/.openprd/engagements/active/prd.md +161 -0
- package/.openprd/engagements/active/review.html +61 -0
- package/.openprd/engagements/active/roles.md +21 -0
- package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +23 -0
- package/.openprd/exports/.gitkeep +0 -0
- package/.openprd/knowledge/index.json +7 -0
- package/.openprd/quality/config.json +229 -0
- package/.openprd/reviews/v0001.html +1256 -0
- package/.openprd/schema/diagram-architecture.schema.yaml +49 -0
- package/.openprd/schema/diagram-product-flow.schema.yaml +52 -0
- package/.openprd/schema/prd.schema.yaml +121 -0
- package/.openprd/sessions/.gitkeep +0 -0
- package/.openprd/standards/config.json +88 -0
- package/.openprd/standards/file-manual-template.md +28 -0
- package/.openprd/standards/folder-readme-template.md +28 -0
- package/.openprd/state/.gitkeep +0 -0
- package/.openprd/state/changes.json +12 -0
- package/.openprd/state/current.json +169 -0
- package/.openprd/state/version-index.json +15 -0
- package/.openprd/state/versions/.gitkeep +0 -0
- package/.openprd/state/versions/v0001.json +121 -0
- package/.openprd/state/versions/v0001.md +161 -0
- package/.openprd/templates/agent/intake.md +6 -0
- package/.openprd/templates/agent/prd.md +21 -0
- package/.openprd/templates/b2b/intake.md +6 -0
- package/.openprd/templates/b2b/prd.md +24 -0
- package/.openprd/templates/base/intake.md +18 -0
- package/.openprd/templates/base/prd.md +67 -0
- package/.openprd/templates/company/README.md +10 -0
- package/.openprd/templates/consumer/intake.md +6 -0
- package/.openprd/templates/consumer/prd.md +19 -0
- package/.openprd/templates/diagram/architecture.contract.json +53 -0
- package/.openprd/templates/diagram/product-flow.contract.json +76 -0
- package/.openprd/templates/industry/README.md +16 -0
- package/.openprd/templates/manifest.yaml +27 -0
- package/.openprd/templates/project/README.md +14 -0
- package/.openprd/templates/session/README.md +14 -0
- package/AGENTS.md +44 -0
- package/CONTRIBUTING.md +30 -0
- package/LICENSE +21 -0
- package/README.md +727 -0
- package/README_CN.md +583 -0
- package/SECURITY.md +23 -0
- package/bin/openprd.js +5 -0
- package/docs/assets/openprd-capability-overview-en.png +0 -0
- package/docs/assets/openprd-capability-overview-zh.png +0 -0
- package/docs/assets/openprd-learning-html.png +0 -0
- package/docs/assets/openprd-quality-html.png +0 -0
- package/docs/assets/openprd-review-html.png +0 -0
- package/docs/assets/openprd-scenario-overview.png +0 -0
- package/docs/assets/openprd-scenario-overview.svg +114 -0
- package/docs/assets/openprd-self-evolving-mechanisms-en.png +0 -0
- package/docs/assets/openprd-self-evolving-mechanisms-zh.png +0 -0
- package/docs/assets/openprd-visual-compare-case-study-en.png +0 -0
- package/docs/assets/openprd-visual-compare-case-study-zh.png +0 -0
- package/package.json +59 -0
- package/scripts/openprd-dev-check.mjs +5 -0
- package/scripts/openprd-review-presentation.mjs +82 -0
- package/skills/openprd-benchmark-router/SKILL.md +92 -0
- package/skills/openprd-benchmark-router/agents/openai.yaml +4 -0
- package/skills/openprd-benchmark-router/references/benchmark-sources.md +74 -0
- package/skills/openprd-benchmark-router/references/evaluation-lenses.md +66 -0
- package/skills/openprd-benchmark-router/references/source-policy.md +35 -0
- package/skills/openprd-diagram-review/SKILL.md +91 -0
- package/skills/openprd-diagram-review/agents/openai.yaml +4 -0
- package/skills/openprd-diagram-review/examples/architecture-zh.md +8 -0
- package/skills/openprd-diagram-review/examples/product-flow-zh.md +7 -0
- package/skills/openprd-diagram-review/references/cocoon-patterns.md +17 -0
- package/skills/openprd-diagram-review/references/diagram-contracts.md +126 -0
- package/skills/openprd-diagram-review/references/review-checklist.md +10 -0
- package/skills/openprd-discovery-loop/SKILL.md +196 -0
- package/skills/openprd-discovery-loop/agents/openai.yaml +3 -0
- package/skills/openprd-harness/SKILL.md +179 -0
- package/skills/openprd-harness/agents/openai.yaml +4 -0
- package/skills/openprd-harness/examples/full-workflow-zh.md +9 -0
- package/skills/openprd-harness/references/command-map.md +71 -0
- package/skills/openprd-harness/references/examples.md +26 -0
- package/skills/openprd-harness/references/usage-guide.md +335 -0
- package/skills/openprd-harness/references/workflow-gates.md +51 -0
- package/skills/openprd-learning-review/SKILL.md +75 -0
- package/skills/openprd-learning-review/agents/openai.yaml +4 -0
- package/skills/openprd-learning-review/references/content-contract.md +125 -0
- package/skills/openprd-learning-review/references/ebook-reader.md +46 -0
- package/skills/openprd-learning-review/references/evidence-manifest.md +55 -0
- package/skills/openprd-learning-review/references/genre-library.md +43 -0
- package/skills/openprd-learning-review/references/prompt-engineering.md +71 -0
- package/skills/openprd-learning-review/references/quality-rubric.md +28 -0
- package/skills/openprd-learning-review/references/retrieval-worked-example.md +40 -0
- package/skills/openprd-learning-review/references/style-packs/xianxia-cultivation.prompt.md +67 -0
- package/skills/openprd-quality/SKILL.md +101 -0
- package/skills/openprd-requirement-intake/SKILL.md +76 -0
- package/skills/openprd-requirement-intake/agents/openai.yaml +4 -0
- package/skills/openprd-requirement-intake/references/prd-template-lenses.md +105 -0
- package/skills/openprd-requirement-intake/references/routing-rubric.md +64 -0
- package/skills/openprd-router/SKILL.md +40 -0
- package/skills/openprd-shared/SKILL.md +142 -0
- package/skills/openprd-shared/agents/openai.yaml +4 -0
- package/skills/openprd-shared/references/language-and-review.md +50 -0
- package/skills/openprd-shared/references/operating-rules.md +65 -0
- package/skills/openprd-shared/references/skill-architecture.md +70 -0
- package/skills/openprd-standards/SKILL.md +79 -0
- package/skills/openprd-standards/agents/openai.yaml +4 -0
- package/src/agent-integration.js +1717 -0
- package/src/benchmark.js +873 -0
- package/src/cli/args.js +460 -0
- package/src/cli/print.js +1423 -0
- package/src/codex-hook-runner-template.mjs +2422 -0
- package/src/dev-standards.js +372 -0
- package/src/diagram-core.js +1047 -0
- package/src/diagram-workspace.js +262 -0
- package/src/discovery.js +709 -0
- package/src/fleet.js +531 -0
- package/src/fs-utils.js +83 -0
- package/src/growth.js +545 -0
- package/src/html-artifacts.js +3803 -0
- package/src/knowledge.js +668 -0
- package/src/language-policy.js +142 -0
- package/src/learning-review.js +1655 -0
- package/src/loop.js +1290 -0
- package/src/openprd.js +1136 -0
- package/src/openspec/change-lifecycle.js +359 -0
- package/src/openspec/change-validate.js +248 -0
- package/src/openspec/constants.js +12 -0
- package/src/openspec/execute.js +300 -0
- package/src/openspec/generate.js +692 -0
- package/src/openspec/paths.js +111 -0
- package/src/openspec/tasks.js +352 -0
- package/src/prd-core.js +656 -0
- package/src/quality-html-artifact.js +1414 -0
- package/src/quality-learning.js +658 -0
- package/src/quality.js +1262 -0
- package/src/review-presentation.js +240 -0
- package/src/run-harness.js +1470 -0
- package/src/self-update.js +329 -0
- package/src/session-binding.js +140 -0
- package/src/source-inventory.js +224 -0
- package/src/standards.js +914 -0
- package/src/time.js +33 -0
- package/src/visual-compare.js +216 -0
- package/src/work-unit-migration.js +232 -0
- package/src/work-unit.js +88 -0
- package/src/workspace-core.js +1706 -0
- package/src/workspace-registry.js +162 -0
- package/src/workspace-workflow.js +1797 -0
|
@@ -0,0 +1,2422 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
const eventName = process.argv[2] || 'Unknown';
|
|
7
|
+
let input = '';
|
|
8
|
+
process.stdin.setEncoding('utf8');
|
|
9
|
+
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
10
|
+
process.stdin.on('end', () => {
|
|
11
|
+
let payload = {};
|
|
12
|
+
try { payload = input.trim() ? JSON.parse(input) : {}; } catch {}
|
|
13
|
+
const cwd = payload.cwd || process.cwd();
|
|
14
|
+
const result = handle(eventName, cwd, payload);
|
|
15
|
+
if (result) {
|
|
16
|
+
process.stdout.write(JSON.stringify(result));
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function now() {
|
|
21
|
+
const formatter = new Intl.DateTimeFormat('zh-CN', {
|
|
22
|
+
timeZone: 'Asia/Shanghai',
|
|
23
|
+
year: 'numeric',
|
|
24
|
+
month: '2-digit',
|
|
25
|
+
day: '2-digit',
|
|
26
|
+
hour: '2-digit',
|
|
27
|
+
minute: '2-digit',
|
|
28
|
+
second: '2-digit',
|
|
29
|
+
hour12: false,
|
|
30
|
+
});
|
|
31
|
+
const parts = Object.fromEntries(formatter.formatToParts(new Date()).map((part) => [part.type, part.value]));
|
|
32
|
+
return `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute}:${parts.second}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function findProjectRoot(start) {
|
|
36
|
+
let current = path.resolve(start || process.cwd());
|
|
37
|
+
for (;;) {
|
|
38
|
+
if (fs.existsSync(path.join(current, '.openprd'))) {
|
|
39
|
+
return current;
|
|
40
|
+
}
|
|
41
|
+
const parent = path.dirname(current);
|
|
42
|
+
if (parent === current) {
|
|
43
|
+
return path.resolve(start || process.cwd());
|
|
44
|
+
}
|
|
45
|
+
current = parent;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function harnessDir(root) {
|
|
50
|
+
return path.join(root, '.openprd', 'harness');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ensureHarness(root) {
|
|
54
|
+
const dir = harnessDir(root);
|
|
55
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
56
|
+
const statePath = path.join(dir, 'hook-state.json');
|
|
57
|
+
if (!fs.existsSync(statePath)) {
|
|
58
|
+
writeJsonSync(statePath, {
|
|
59
|
+
version: 1,
|
|
60
|
+
active: true,
|
|
61
|
+
lastEventAt: null,
|
|
62
|
+
lastFingerprint: null,
|
|
63
|
+
counters: {},
|
|
64
|
+
recentFingerprints: {},
|
|
65
|
+
suppressions: { inputLock: false },
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const eventsPath = path.join(dir, 'events.jsonl');
|
|
69
|
+
if (!fs.existsSync(eventsPath)) {
|
|
70
|
+
fs.writeFileSync(eventsPath, '');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readJsonSync(filePath, fallback) {
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
77
|
+
} catch {
|
|
78
|
+
return fallback;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function writeJsonSync(filePath, value) {
|
|
83
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
84
|
+
fs.writeFileSync(filePath, JSON.stringify(value, null, 2) + '\n');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function appendEvent(root, event) {
|
|
88
|
+
ensureHarness(root);
|
|
89
|
+
fs.appendFileSync(path.join(harnessDir(root), 'events.jsonl'), JSON.stringify({ at: now(), ...event }) + '\n');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function turnStatePath(root) {
|
|
93
|
+
return path.join(harnessDir(root), 'turn-state.json');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function defaultTurnState() {
|
|
97
|
+
return {
|
|
98
|
+
version: 1,
|
|
99
|
+
id: null,
|
|
100
|
+
sessionId: null,
|
|
101
|
+
prompt: null,
|
|
102
|
+
promptPreview: null,
|
|
103
|
+
title: null,
|
|
104
|
+
status: 'needs-attention',
|
|
105
|
+
startedAt: null,
|
|
106
|
+
updatedAt: null,
|
|
107
|
+
touchedFiles: [],
|
|
108
|
+
reviewSignals: [],
|
|
109
|
+
runtimeEvents: [],
|
|
110
|
+
timeline: [],
|
|
111
|
+
lastKnowledgePromptCandidateId: null,
|
|
112
|
+
lastKnowledgePromptAt: null,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function readTurnState(root) {
|
|
117
|
+
return {
|
|
118
|
+
...defaultTurnState(),
|
|
119
|
+
...readJsonSync(turnStatePath(root), defaultTurnState()),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function writeTurnState(root, next) {
|
|
124
|
+
writeJsonSync(turnStatePath(root), {
|
|
125
|
+
...defaultTurnState(),
|
|
126
|
+
...next,
|
|
127
|
+
updatedAt: now(),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function normalizeProjectFile(root, filePath) {
|
|
132
|
+
const raw = String(filePath || '').trim();
|
|
133
|
+
if (!raw) return null;
|
|
134
|
+
const absolutePath = path.isAbsolute(raw) ? path.resolve(raw) : path.resolve(root, raw);
|
|
135
|
+
const relativePath = path.relative(root, absolutePath).split(path.sep).join('/');
|
|
136
|
+
if (!relativePath || relativePath.startsWith('..')) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
return relativePath;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function extractTouchedFiles(root, payload) {
|
|
143
|
+
const files = [];
|
|
144
|
+
const toolInput = payload?.tool_input ?? payload?.toolInput ?? payload?.input ?? null;
|
|
145
|
+
const addFile = (value) => {
|
|
146
|
+
const normalized = normalizeProjectFile(root, value);
|
|
147
|
+
if (normalized) {
|
|
148
|
+
files.push(normalized);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
if (toolInput && typeof toolInput === 'object') {
|
|
152
|
+
for (const field of ['file_path', 'filePath', 'path', 'target_path', 'targetPath']) {
|
|
153
|
+
if (typeof toolInput[field] === 'string') {
|
|
154
|
+
addFile(toolInput[field]);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const patchText = typeof toolInput === 'string'
|
|
159
|
+
? toolInput
|
|
160
|
+
: (typeof toolInput?.patch === 'string' ? toolInput.patch : '');
|
|
161
|
+
if (patchText) {
|
|
162
|
+
for (const line of patchText.split(/\r?\n/)) {
|
|
163
|
+
const match = line.match(/^\*\*\* (?:Update|Add|Delete) File: (.+)$/)
|
|
164
|
+
|| line.match(/^\*\*\* Move to: (.+)$/);
|
|
165
|
+
if (match?.[1]) {
|
|
166
|
+
addFile(match[1].trim());
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return [...new Set(files)];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function beginTurnReview(root, baseEvent, prompt) {
|
|
174
|
+
const promptPreview = preview(prompt, 240);
|
|
175
|
+
writeTurnState(root, {
|
|
176
|
+
version: 1,
|
|
177
|
+
id: `turn-${String(Date.now())}-${baseEvent.fingerprint ?? 'openprd'}`,
|
|
178
|
+
knowledgeCandidateId: null,
|
|
179
|
+
sessionId: baseEvent.sessionId,
|
|
180
|
+
prompt,
|
|
181
|
+
promptPreview,
|
|
182
|
+
title: promptPreview || '本轮项目回顾',
|
|
183
|
+
summary: {
|
|
184
|
+
title: promptPreview || '本轮项目回顾',
|
|
185
|
+
status: 'needs-attention',
|
|
186
|
+
message: '等待本轮实现和验证信号,用于 Stop 回顾生成项目经验草案。',
|
|
187
|
+
},
|
|
188
|
+
status: 'needs-attention',
|
|
189
|
+
startedAt: now(),
|
|
190
|
+
touchedFiles: [],
|
|
191
|
+
reviewSignals: [],
|
|
192
|
+
runtimeEvents: promptPreview ? [{
|
|
193
|
+
eventName: 'user-prompt',
|
|
194
|
+
status: 'pass',
|
|
195
|
+
message: promptPreview,
|
|
196
|
+
at: now(),
|
|
197
|
+
}] : [],
|
|
198
|
+
timeline: promptPreview ? [{
|
|
199
|
+
event: 'user-prompt',
|
|
200
|
+
status: 'pass',
|
|
201
|
+
message: promptPreview,
|
|
202
|
+
at: now(),
|
|
203
|
+
}] : [],
|
|
204
|
+
lastKnowledgePromptCandidateId: null,
|
|
205
|
+
lastKnowledgePromptAt: null,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function recordTouchedFiles(root, payload) {
|
|
210
|
+
const touchedFiles = extractTouchedFiles(root, payload);
|
|
211
|
+
if (touchedFiles.length === 0) {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
const state = readTurnState(root);
|
|
215
|
+
writeTurnState(root, {
|
|
216
|
+
...state,
|
|
217
|
+
touchedFiles: [...new Set([...(Array.isArray(state.touchedFiles) ? state.touchedFiles : []), ...touchedFiles])],
|
|
218
|
+
});
|
|
219
|
+
return touchedFiles;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function updateHookState(root, event) {
|
|
223
|
+
ensureHarness(root);
|
|
224
|
+
const statePath = path.join(harnessDir(root), 'hook-state.json');
|
|
225
|
+
const state = readJsonSync(statePath, {
|
|
226
|
+
version: 1,
|
|
227
|
+
counters: {},
|
|
228
|
+
recentFingerprints: {},
|
|
229
|
+
suppressions: { inputLock: false },
|
|
230
|
+
});
|
|
231
|
+
state.lastEventAt = now();
|
|
232
|
+
state.lastEvent = event.eventName;
|
|
233
|
+
state.lastFingerprint = event.fingerprint;
|
|
234
|
+
state.counters[event.eventName] = (state.counters[event.eventName] || 0) + 1;
|
|
235
|
+
state.recentFingerprints = state.recentFingerprints || {};
|
|
236
|
+
if (event.fingerprint) {
|
|
237
|
+
state.recentFingerprints[event.fingerprint] = Date.now();
|
|
238
|
+
}
|
|
239
|
+
for (const [fingerprint, seenAt] of Object.entries(state.recentFingerprints)) {
|
|
240
|
+
if (Date.now() - Number(seenAt) > 300000) {
|
|
241
|
+
delete state.recentFingerprints[fingerprint];
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
writeJsonSync(statePath, state);
|
|
245
|
+
return state;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function isDuplicate(root, fingerprint, windowMs = 15000) {
|
|
249
|
+
const state = readJsonSync(path.join(harnessDir(root), 'hook-state.json'), {});
|
|
250
|
+
const seenAt = state?.recentFingerprints?.[fingerprint];
|
|
251
|
+
return Boolean(seenAt && Date.now() - Number(seenAt) < windowMs);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function fingerprintFor(eventName, payload, risk) {
|
|
255
|
+
const tool = payload.tool_name || payload.toolName || payload.name || '';
|
|
256
|
+
const inputText = JSON.stringify(payload.tool_input || payload.toolInput || payload.input || payload || {}).slice(0, 2000);
|
|
257
|
+
return crypto.createHash('sha256').update(JSON.stringify({ eventName, tool, inputText, risk: risk.level })).digest('hex').slice(0, 16);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function sanitizeSessionId(value) {
|
|
261
|
+
const text = String(value || '').trim();
|
|
262
|
+
return /^[a-zA-Z0-9._-]{6,160}$/.test(text) ? text : null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function sessionIdFor(payload) {
|
|
266
|
+
const direct = sanitizeSessionId(payload.session_id || payload.sessionId || payload.thread_id || payload.threadId || payload.conversation_id || payload.conversationId);
|
|
267
|
+
if (direct) return direct;
|
|
268
|
+
const transcript = String(payload.transcript_path || payload.transcriptPath || '');
|
|
269
|
+
const match = transcript.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i);
|
|
270
|
+
return match ? sanitizeSessionId(match[1]) : null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function payloadText(payload) {
|
|
274
|
+
return JSON.stringify(payload.tool_input || payload.toolInput || payload.input || payload || {});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function promptText(payload) {
|
|
278
|
+
return String(payload.prompt || payload.user_prompt || payload.message || '');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function preview(text, max = 600) {
|
|
282
|
+
return String(text || '').replace(/\s+/g, ' ').trim().slice(0, max);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function commandText(payload) {
|
|
286
|
+
const direct = typeof payload?.cmd === 'string'
|
|
287
|
+
? payload.cmd
|
|
288
|
+
: typeof payload?.command === 'string'
|
|
289
|
+
? payload.command
|
|
290
|
+
: '';
|
|
291
|
+
if (direct) {
|
|
292
|
+
return direct;
|
|
293
|
+
}
|
|
294
|
+
const toolInput = payload?.tool_input ?? payload?.toolInput ?? payload?.input ?? null;
|
|
295
|
+
if (typeof toolInput === 'string') {
|
|
296
|
+
return toolInput;
|
|
297
|
+
}
|
|
298
|
+
if (toolInput && typeof toolInput.cmd === 'string') {
|
|
299
|
+
return toolInput.cmd;
|
|
300
|
+
}
|
|
301
|
+
return '';
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function toolName(payload) {
|
|
305
|
+
return String(payload?.tool_name || payload?.toolName || payload?.name || '').trim();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function namedGateDir(root, gateName) {
|
|
309
|
+
return path.join(harnessDir(root), `${gateName}-gates`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function namedGatePath(root, gateName, sessionId = null) {
|
|
313
|
+
if (sessionId) {
|
|
314
|
+
return path.join(namedGateDir(root, gateName), `${sessionId}.json`);
|
|
315
|
+
}
|
|
316
|
+
return path.join(harnessDir(root), `${gateName}-gate.json`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function readNamedGate(root, gateName, sessionId = null) {
|
|
320
|
+
return readJsonSync(namedGatePath(root, gateName, sessionId), null);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function writeNamedGate(root, gateName, value, sessionId = null) {
|
|
324
|
+
const next = sessionId ? { ...value, sessionId } : value;
|
|
325
|
+
writeJsonSync(namedGatePath(root, gateName, sessionId), next);
|
|
326
|
+
if (sessionId) {
|
|
327
|
+
writeJsonSync(namedGatePath(root, gateName), next);
|
|
328
|
+
}
|
|
329
|
+
return next;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function updateNamedGate(root, gateName, patch, sessionId = null) {
|
|
333
|
+
const current = readNamedGate(root, gateName, sessionId);
|
|
334
|
+
if (!current) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
return writeNamedGate(root, gateName, {
|
|
338
|
+
...current,
|
|
339
|
+
updatedAt: now(),
|
|
340
|
+
...patch,
|
|
341
|
+
}, sessionId);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function legacyRequirementGatePath(root) {
|
|
345
|
+
return path.join(harnessDir(root), 'requirement-gate.json');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function requirementGateDir(root) {
|
|
349
|
+
return path.join(harnessDir(root), 'requirement-gates');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function requirementGatePath(root, sessionId = null) {
|
|
353
|
+
if (sessionId) {
|
|
354
|
+
return path.join(requirementGateDir(root), `${sessionId}.json`);
|
|
355
|
+
}
|
|
356
|
+
return legacyRequirementGatePath(root);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function sessionBindingDir(root) {
|
|
360
|
+
return path.join(harnessDir(root), 'session-bindings');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function sessionBindingPath(root, sessionId = null) {
|
|
364
|
+
if (!sessionId) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
return path.join(sessionBindingDir(root), `${String(sessionId).replace(/[^A-Za-z0-9._-]/g, '_')}.json`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function currentStatePath(root) {
|
|
371
|
+
return path.join(root, '.openprd', 'state', 'current.json');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function normalizeReviewStatus(value) {
|
|
375
|
+
return ['pending-confirmation', 'confirmed', 'needs-revision'].includes(value) ? value : 'missing';
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function normalizeRequirementApprovalPolicy(policy = {}) {
|
|
379
|
+
const reviewPolicy = ['required', 'silent-record'].includes(policy?.reviewPolicy)
|
|
380
|
+
? policy.reviewPolicy
|
|
381
|
+
: 'required';
|
|
382
|
+
const maxClarificationQuestionsRaw = Number(policy?.maxClarificationQuestions);
|
|
383
|
+
const maxClarificationQuestions = Number.isFinite(maxClarificationQuestionsRaw) && maxClarificationQuestionsRaw > 0
|
|
384
|
+
? Math.max(1, Math.floor(maxClarificationQuestionsRaw))
|
|
385
|
+
: null;
|
|
386
|
+
return {
|
|
387
|
+
mode: 'decision-points',
|
|
388
|
+
reviewPolicy,
|
|
389
|
+
executionMode: policy?.executionMode === 'auto-after-review-and-tasks'
|
|
390
|
+
? 'auto-after-review-and-tasks'
|
|
391
|
+
: 'manual-after-review-and-tasks',
|
|
392
|
+
suppressExtraConfirmation: Boolean(policy?.suppressExtraConfirmation),
|
|
393
|
+
maxClarificationQuestions,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function requirementApprovalPolicy(gate) {
|
|
398
|
+
return normalizeRequirementApprovalPolicy(gate?.approvalPolicy);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function reviewPolicyRequiresHumanConfirmation(policy) {
|
|
402
|
+
return normalizeRequirementApprovalPolicy(policy).reviewPolicy === 'required';
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function reviewPolicyAllowsSilentRecord(policy) {
|
|
406
|
+
return normalizeRequirementApprovalPolicy(policy).reviewPolicy === 'silent-record';
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function resolveRequirementApprovalPolicy(prompt, intent = {}) {
|
|
410
|
+
const suppressExtraConfirmation = Boolean(intent?.noReviewRequested || intent?.noConfirmationRequested);
|
|
411
|
+
const explicitExecution = Boolean(intent?.explicitExecution);
|
|
412
|
+
return normalizeRequirementApprovalPolicy({
|
|
413
|
+
mode: 'decision-points',
|
|
414
|
+
reviewPolicy: explicitExecution && suppressExtraConfirmation ? 'silent-record' : 'required',
|
|
415
|
+
executionMode: explicitExecution ? 'auto-after-review-and-tasks' : 'manual-after-review-and-tasks',
|
|
416
|
+
suppressExtraConfirmation,
|
|
417
|
+
maxClarificationQuestions: explicitExecution
|
|
418
|
+
? (suppressExtraConfirmation ? 1 : 2)
|
|
419
|
+
: null,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function readCliFlagValue(command, flag) {
|
|
424
|
+
const quotedPattern = new RegExp(`${flag}\\s+(?:'([^']+)'|"([^"]+)"|([^\\s]+))`, 'i');
|
|
425
|
+
const match = String(command || '').match(quotedPattern);
|
|
426
|
+
return match?.[1] ?? match?.[2] ?? match?.[3] ?? null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function parseReviewMarkCommand(text) {
|
|
430
|
+
const command = String(text || '');
|
|
431
|
+
if (!/openprd\s+review\b/i.test(command) || !/--mark\b/i.test(command)) {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
const mark = readCliFlagValue(command, '--mark')?.toLowerCase() ?? null;
|
|
435
|
+
const versionId = readCliFlagValue(command, '--version');
|
|
436
|
+
const digest = readCliFlagValue(command, '--digest');
|
|
437
|
+
const workUnitId = readCliFlagValue(command, '--work-unit');
|
|
438
|
+
if (!mark) {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
return { mark, versionId, digest, workUnitId };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function readSessionBinding(root, sessionId = null) {
|
|
445
|
+
return readJsonSync(sessionBindingPath(root, sessionId), null);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function writeSessionBinding(root, sessionId, patch = {}) {
|
|
449
|
+
if (!sessionId) {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
const filePath = sessionBindingPath(root, sessionId);
|
|
453
|
+
const previous = readJsonSync(filePath, null);
|
|
454
|
+
const next = {
|
|
455
|
+
...(previous ?? {}),
|
|
456
|
+
version: 1,
|
|
457
|
+
sessionId,
|
|
458
|
+
...patch,
|
|
459
|
+
createdAt: previous?.createdAt ?? patch.createdAt ?? now(),
|
|
460
|
+
updatedAt: patch.updatedAt ?? now(),
|
|
461
|
+
};
|
|
462
|
+
writeJsonSync(filePath, next);
|
|
463
|
+
return next;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function readCurrentPrdReview(root) {
|
|
467
|
+
const currentState = readJsonSync(currentStatePath(root), {}) || {};
|
|
468
|
+
const reviewStatus = currentState.reviewStatus ?? {};
|
|
469
|
+
const versionId = String(reviewStatus.versionId || currentState.latestVersionId || '').trim() || null;
|
|
470
|
+
const digest = String(currentState.latestVersionDigest || '').trim() || null;
|
|
471
|
+
const workUnitId = String(reviewStatus.workUnitId || currentState.activeWorkUnitId || '').trim() || null;
|
|
472
|
+
return {
|
|
473
|
+
versionId,
|
|
474
|
+
digest,
|
|
475
|
+
workUnitId,
|
|
476
|
+
status: versionId ? normalizeReviewStatus(reviewStatus.status) : 'missing',
|
|
477
|
+
artifact: reviewStatus.reviewPath || reviewStatus.stableArtifact || reviewStatus.entryPath || reviewStatus.artifact || null,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function readRequirementLaneReview(root, sessionId = null) {
|
|
482
|
+
const binding = readSessionBinding(root, sessionId);
|
|
483
|
+
if (binding?.versionId || binding?.digest || binding?.workUnitId) {
|
|
484
|
+
return {
|
|
485
|
+
versionId: binding.versionId ?? null,
|
|
486
|
+
digest: binding.digest ?? null,
|
|
487
|
+
workUnitId: binding.workUnitId ?? null,
|
|
488
|
+
status: binding.versionId ? normalizeReviewStatus(binding.reviewStatus) : 'missing',
|
|
489
|
+
artifact: binding.reviewPath || binding.stableReviewArtifact || binding.activeReviewPath || binding.reviewArtifact || null,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
return readCurrentPrdReview(root);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function reviewCommandMatchesReview(command, review) {
|
|
496
|
+
if (!command || !review) {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
return Boolean(
|
|
500
|
+
command.versionId
|
|
501
|
+
&& command.digest
|
|
502
|
+
&& command.workUnitId
|
|
503
|
+
&& review.versionId
|
|
504
|
+
&& review.digest
|
|
505
|
+
&& review.workUnitId
|
|
506
|
+
&& command.versionId === review.versionId
|
|
507
|
+
&& command.digest === review.digest
|
|
508
|
+
&& command.workUnitId === review.workUnitId
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function readTextSync(filePath, maxLength = 20000) {
|
|
513
|
+
try {
|
|
514
|
+
return fs.readFileSync(filePath, 'utf8').slice(0, maxLength);
|
|
515
|
+
} catch {
|
|
516
|
+
return '';
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function findChangeDir(root, changeId) {
|
|
521
|
+
const candidates = [
|
|
522
|
+
path.join(root, 'openprd', 'changes', changeId),
|
|
523
|
+
path.join(root, 'openspec', 'changes', changeId),
|
|
524
|
+
path.join(root, 'openprd', 'archive', 'changes', changeId),
|
|
525
|
+
];
|
|
526
|
+
return candidates.find((candidate) => fs.existsSync(candidate)) || candidates[0];
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function stripMarkdown(text) {
|
|
530
|
+
return String(text || '')
|
|
531
|
+
.replace(/^\s*[-*]\s+/, '')
|
|
532
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
533
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
534
|
+
.replace(/\s+/g, ' ')
|
|
535
|
+
.trim();
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function humanizeChangeId(changeId) {
|
|
539
|
+
return String(changeId || '未命名需求').replace(/[-_]+/g, ' ');
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function firstHeading(text, fallback = '') {
|
|
543
|
+
const match = String(text || '').match(/^#\s+(.+)$/m);
|
|
544
|
+
return stripMarkdown(match?.[1] || fallback);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function specRequirementTitle(text) {
|
|
548
|
+
const match = String(text || '').match(/^###\s+需求[::]\s*(.+)$/m);
|
|
549
|
+
return stripMarkdown(match?.[1] || '');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function collectSectionSummary(text, headingPatterns, maxItems = 3) {
|
|
553
|
+
const lines = String(text || '').split(/\r?\n/);
|
|
554
|
+
const collected = [];
|
|
555
|
+
let inSection = false;
|
|
556
|
+
for (const line of lines) {
|
|
557
|
+
const heading = line.match(/^#{1,3}\s+(.+)$/);
|
|
558
|
+
if (heading) {
|
|
559
|
+
const headingText = stripMarkdown(heading[1]);
|
|
560
|
+
if (inSection && !headingPatterns.some((pattern) => pattern.test(headingText))) {
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
inSection = headingPatterns.some((pattern) => pattern.test(headingText));
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (!inSection) {
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
const item = stripMarkdown(line);
|
|
570
|
+
if (item) {
|
|
571
|
+
collected.push(item);
|
|
572
|
+
}
|
|
573
|
+
if (collected.length >= maxItems) {
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return collected.join(';');
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function cleanTaskTitle(title) {
|
|
581
|
+
return stripMarkdown(title)
|
|
582
|
+
.replace(/^T\d+\.\d+\s+/, '')
|
|
583
|
+
.replace(/^(实现主流程|实现需求|验证验收目标)[::]\s*/, '')
|
|
584
|
+
.trim();
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function pendingTaskTitles(root, changeId, maxItems = 3) {
|
|
588
|
+
const dir = findChangeDir(root, changeId);
|
|
589
|
+
let files = [];
|
|
590
|
+
try {
|
|
591
|
+
files = fs.readdirSync(dir).filter((name) => /^tasks(?:-\d+)?\.md$/.test(name)).sort();
|
|
592
|
+
} catch {
|
|
593
|
+
return [];
|
|
594
|
+
}
|
|
595
|
+
const tasks = [];
|
|
596
|
+
for (const file of files) {
|
|
597
|
+
const text = readTextSync(path.join(dir, file));
|
|
598
|
+
for (const line of text.split(/\r?\n/)) {
|
|
599
|
+
const match = line.match(/^- \[ \]\s+(.+)$/);
|
|
600
|
+
if (!match) {
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
const title = cleanTaskTitle(match[1]);
|
|
604
|
+
if (title) {
|
|
605
|
+
tasks.push(title);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
const productTasks = tasks.filter((title) => !/(spec|validate|校验|评审|测试|文档|打包)/i.test(title));
|
|
610
|
+
return (productTasks.length > 0 ? productTasks : tasks).slice(0, maxItems);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function readLocalTaskSummary(root, changeId) {
|
|
614
|
+
if (!changeId) {
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
const dir = findChangeDir(root, changeId);
|
|
618
|
+
let files = [];
|
|
619
|
+
try {
|
|
620
|
+
files = fs.readdirSync(dir).filter((name) => /^tasks(?:-\d+)?\.md$/.test(name)).sort();
|
|
621
|
+
} catch {
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
let total = 0;
|
|
625
|
+
let completed = 0;
|
|
626
|
+
let pending = 0;
|
|
627
|
+
for (const file of files) {
|
|
628
|
+
const text = readTextSync(path.join(dir, file));
|
|
629
|
+
for (const line of text.split(/\r?\n/)) {
|
|
630
|
+
if (/^- \[x\]\s+/i.test(line)) {
|
|
631
|
+
total += 1;
|
|
632
|
+
completed += 1;
|
|
633
|
+
} else if (/^- \[ \]\s+/.test(line)) {
|
|
634
|
+
total += 1;
|
|
635
|
+
pending += 1;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (total === 0) {
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
return {
|
|
643
|
+
total,
|
|
644
|
+
completed,
|
|
645
|
+
pending,
|
|
646
|
+
blocked: 0,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function changeRequirementSummary(root, changeId) {
|
|
651
|
+
const dir = findChangeDir(root, changeId);
|
|
652
|
+
const proposal = readTextSync(path.join(dir, 'proposal.md'));
|
|
653
|
+
const design = readTextSync(path.join(dir, 'design.md'));
|
|
654
|
+
let spec = '';
|
|
655
|
+
try {
|
|
656
|
+
const specsRoot = path.join(dir, 'specs');
|
|
657
|
+
const capability = fs.readdirSync(specsRoot).find((name) => !name.startsWith('.'));
|
|
658
|
+
if (capability) {
|
|
659
|
+
spec = readTextSync(path.join(specsRoot, capability, 'spec.md'));
|
|
660
|
+
}
|
|
661
|
+
} catch {}
|
|
662
|
+
|
|
663
|
+
const title = firstHeading(proposal)
|
|
664
|
+
|| specRequirementTitle(spec)
|
|
665
|
+
|| firstHeading(design)
|
|
666
|
+
|| humanizeChangeId(changeId);
|
|
667
|
+
const summary = collectSectionSummary(proposal, [/背景/, /原因/, /为什么/], 2)
|
|
668
|
+
|| collectSectionSummary(spec, [/新增需求/, /需求/], 2)
|
|
669
|
+
|| collectSectionSummary(design, [/目标/, /背景/], 3);
|
|
670
|
+
|
|
671
|
+
return {
|
|
672
|
+
title,
|
|
673
|
+
summary,
|
|
674
|
+
pendingTasks: pendingTaskTitles(root, changeId),
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function runOpenPrdContext(cwd, prompt = null) {
|
|
679
|
+
const args = ['run', '.', '--context', '--json'];
|
|
680
|
+
if (String(prompt || '').trim()) {
|
|
681
|
+
args.push('--message', String(prompt).trim());
|
|
682
|
+
}
|
|
683
|
+
const json = runOpenPrd(args, cwd);
|
|
684
|
+
if (json.stdout) {
|
|
685
|
+
try {
|
|
686
|
+
const parsed = JSON.parse(json.stdout);
|
|
687
|
+
return {
|
|
688
|
+
ok: true,
|
|
689
|
+
commandOk: json.ok,
|
|
690
|
+
status: json.status,
|
|
691
|
+
parsed,
|
|
692
|
+
stdout: renderRunContextText(parsed),
|
|
693
|
+
};
|
|
694
|
+
} catch {}
|
|
695
|
+
}
|
|
696
|
+
const fallbackArgs = ['run', '.', '--context'];
|
|
697
|
+
if (String(prompt || '').trim()) {
|
|
698
|
+
fallbackArgs.push('--message', String(prompt).trim());
|
|
699
|
+
}
|
|
700
|
+
const text = runOpenPrd(fallbackArgs, cwd);
|
|
701
|
+
return {
|
|
702
|
+
ok: text.ok,
|
|
703
|
+
parsed: null,
|
|
704
|
+
stdout: text.stdout,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function renderRunContextText(result) {
|
|
709
|
+
const lines = [
|
|
710
|
+
'OpenPrd 运行上下文',
|
|
711
|
+
'项目: ' + result.projectRoot,
|
|
712
|
+
'验证: ' + (result.validation?.valid ? '通过' : '失败'),
|
|
713
|
+
];
|
|
714
|
+
if (result.activeChange) {
|
|
715
|
+
lines.push('激活变更: ' + result.activeChange);
|
|
716
|
+
}
|
|
717
|
+
if (result.lane?.summary) {
|
|
718
|
+
lines.push('执行流: ' + result.lane.summary);
|
|
719
|
+
}
|
|
720
|
+
if (result.taskSummary) {
|
|
721
|
+
lines.push('任务: ' + result.taskSummary.completed + '/' + result.taskSummary.total + ' 完成,' + result.taskSummary.pending + ' 待处理,' + result.taskSummary.blocked + ' 阻塞');
|
|
722
|
+
}
|
|
723
|
+
if (result.discovery) {
|
|
724
|
+
lines.push('持续发现: ' + result.discovery.runId + ' 已覆盖 ' + result.discovery.summary.covered + '/' + result.discovery.summary.total + ',待处理 ' + result.discovery.summary.pending);
|
|
725
|
+
}
|
|
726
|
+
const recommendation = result.recommendation || {};
|
|
727
|
+
lines.push('下一步类型: ' + recommendation.type);
|
|
728
|
+
lines.push('下一步: ' + recommendation.title);
|
|
729
|
+
lines.push('原因: ' + recommendation.reason);
|
|
730
|
+
lines.push('建议只读命令: ' + recommendation.command);
|
|
731
|
+
if (recommendation.preparationCommand || recommendation.executionCommand || recommendation.commitCommand) {
|
|
732
|
+
lines.push('执行门槛: 仅当用户当前明确要求开发、实现、继续任务、深度调研、深度对标、复刻落地或提交时使用;规划、梳理、分析、审查类请求保持只读。');
|
|
733
|
+
}
|
|
734
|
+
if (recommendation.preparationCommand) {
|
|
735
|
+
lines.push('准备命令: ' + recommendation.preparationCommand);
|
|
736
|
+
}
|
|
737
|
+
if (recommendation.executionCommand) {
|
|
738
|
+
lines.push('执行命令: ' + recommendation.executionCommand);
|
|
739
|
+
}
|
|
740
|
+
if (recommendation.commitCommand) {
|
|
741
|
+
lines.push('提交命令: ' + recommendation.commitCommand);
|
|
742
|
+
}
|
|
743
|
+
lines.push('验证命令: ' + recommendation.verifyCommand);
|
|
744
|
+
lines.push('状态文件: ' + (result.files?.runState || '.openprd/harness/run-state.json'));
|
|
745
|
+
return lines.filter(Boolean).join('\n');
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function analyzePromptIntent(prompt) {
|
|
749
|
+
const text = String(prompt || '').trim();
|
|
750
|
+
const normalized = text.toLowerCase();
|
|
751
|
+
const continuationSessionId = text.match(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i)?.[0] ?? null;
|
|
752
|
+
const continuationTaskHandle = text.match(/\b[a-z0-9._-]+:T\d{3}\.\d{2}:[a-z0-9._-]+\b/i)?.[0] ?? null;
|
|
753
|
+
const continuationWorkUnitId = text.match(/\bwu-[a-z0-9._-]+\b/i)?.[0] ?? null;
|
|
754
|
+
const continuationRequest = /(继续(这个|这条|当前)?(对话|任务|会话|记录|历史)?|续做|接着做|继续执行|继续推进)/i.test(text)
|
|
755
|
+
|| Boolean(continuationSessionId || continuationTaskHandle || continuationWorkUnitId);
|
|
756
|
+
const githubRepoPattern = /(?:https?:\/\/)?github\.com\/[^\s/]+\/[^\s/#?]+|(?:^|[\s(])[\w.-]+\/[\w.-]+(?=$|[\s)#?])/i;
|
|
757
|
+
const internalOpenPrdExecution = /^#\s*OpenPrd\s+长程单任务执行会话/m.test(text)
|
|
758
|
+
|| /模式:\s*loop-run\b/i.test(text)
|
|
759
|
+
|| /模式:\s*loop-finish\b/i.test(text);
|
|
760
|
+
// These signals only decide when to inject/open the requirement-intake lane.
|
|
761
|
+
// The L0/L1/L2 decision itself belongs to openprd-requirement-intake, not to
|
|
762
|
+
// this matcher.
|
|
763
|
+
const requirementRoutingSignals = [
|
|
764
|
+
/新增/,
|
|
765
|
+
/增加/,
|
|
766
|
+
/新建/,
|
|
767
|
+
/我希望/,
|
|
768
|
+
/用户反馈/,
|
|
769
|
+
/需求/,
|
|
770
|
+
/功能/,
|
|
771
|
+
/模块/,
|
|
772
|
+
/页面/,
|
|
773
|
+
/界面/,
|
|
774
|
+
/视觉/,
|
|
775
|
+
/入口/,
|
|
776
|
+
/流程/,
|
|
777
|
+
/编排/,
|
|
778
|
+
/一站式/,
|
|
779
|
+
/体验/,
|
|
780
|
+
/信息架构/,
|
|
781
|
+
/团队搭建/,
|
|
782
|
+
/agent\s*市场/i,
|
|
783
|
+
/skill\s*library/i,
|
|
784
|
+
/UI/i,
|
|
785
|
+
/cli\s*库/i,
|
|
786
|
+
/workflow/i,
|
|
787
|
+
/wizard/i,
|
|
788
|
+
];
|
|
789
|
+
const tinyEditPatterns = [
|
|
790
|
+
/加(一个|个)?空格/,
|
|
791
|
+
/增加(一个|个)?空格/,
|
|
792
|
+
/删(一个|个)?空格/,
|
|
793
|
+
/去掉(一个|个)?空格/,
|
|
794
|
+
/空格|标点|错别字|拼写|大小写/,
|
|
795
|
+
/文案.{0,8}(改短|调整|替换)/,
|
|
796
|
+
/按钮文案/,
|
|
797
|
+
/typo/i,
|
|
798
|
+
/spacing/i,
|
|
799
|
+
/copy/i,
|
|
800
|
+
];
|
|
801
|
+
const simpleConcretePatterns = [
|
|
802
|
+
/按钮|文案|颜色|圆角|位置|间距|字号|图标|标题|空格|标点|label|copy/i,
|
|
803
|
+
/从.+(改到|移到|移动到|换到|变成|改成|改为).+/,
|
|
804
|
+
];
|
|
805
|
+
const complexScopePatterns = [
|
|
806
|
+
/新增|新建|模块|流程|编排|一站式|权限|审批|团队|agent\s*市场|AI/i,
|
|
807
|
+
];
|
|
808
|
+
const explicitExecutionPatterns = [
|
|
809
|
+
/直接(帮我|给我)?(改|做|实现|落地|修|修复|处理|解决)/,
|
|
810
|
+
/如果.{0,24}(定位|确认|找到).{0,12}(原因|根因).{0,24}直接.{0,12}(帮我|给我)?(修|修复|改|处理|解决)/,
|
|
811
|
+
/开始(改|做|实现|开发|落地)/,
|
|
812
|
+
/请(直接)?(实现|落地|修改|修复|处理|解决)/,
|
|
813
|
+
/可以(执行|落地|实现|开发)/,
|
|
814
|
+
];
|
|
815
|
+
const implementationConfirmationPatterns = [
|
|
816
|
+
/确认.*(执行|落地|实现|继续|开发|修复|修改|处理|解决|改)/,
|
|
817
|
+
/按(这个|刚才|上面|已确认)?.{0,12}(方案|计划|拆解).{0,12}(执行|落地|实现|开发|修复|修改|处理|解决|改)/,
|
|
818
|
+
/按.{0,12}(思路|方案|计划|拆解).{0,12}(来吧|来|走|做|执行|落地|实现|开发|修复|修改|处理|解决|改)/,
|
|
819
|
+
];
|
|
820
|
+
const noReviewRequestedPatterns = [
|
|
821
|
+
/不需要评审/,
|
|
822
|
+
/不用评审/,
|
|
823
|
+
/无需评审/,
|
|
824
|
+
/不要评审/,
|
|
825
|
+
/别再评审/,
|
|
826
|
+
];
|
|
827
|
+
const noConfirmationRequestedPatterns = [
|
|
828
|
+
/不需要(再)?确认/,
|
|
829
|
+
/不用(再)?确认/,
|
|
830
|
+
/无需(再)?确认/,
|
|
831
|
+
/不要(再)?确认/,
|
|
832
|
+
/别再确认/,
|
|
833
|
+
/不需要再跟我确认/,
|
|
834
|
+
];
|
|
835
|
+
const promptReviewCommand = parseReviewMarkCommand(text);
|
|
836
|
+
const reviewConfirmPatterns = [
|
|
837
|
+
/认可方案/,
|
|
838
|
+
/确认(?:当前|这个|这版|该)?(?:PRD|评审稿|评审页|review|需求稿|版本)/i,
|
|
839
|
+
/(?:PRD|评审稿|评审页|review|需求稿|版本).{0,12}(确认|通过|没问题|认可|可以)/i,
|
|
840
|
+
/按这版(?:PRD|评审|需求稿)/i,
|
|
841
|
+
];
|
|
842
|
+
const reviewNeedsRevisionPatterns = [
|
|
843
|
+
/(?:PRD|评审稿|评审页|review|需求稿|版本).{0,12}(需要调整|需要修改|要调整|要修改)/i,
|
|
844
|
+
];
|
|
845
|
+
const readOnlyPatterns = [
|
|
846
|
+
/看看/,
|
|
847
|
+
/规划/,
|
|
848
|
+
/分析/,
|
|
849
|
+
/梳理/,
|
|
850
|
+
/评估/,
|
|
851
|
+
/怎么改/,
|
|
852
|
+
/预计动哪些文件/,
|
|
853
|
+
/review/i,
|
|
854
|
+
/explain/i,
|
|
855
|
+
];
|
|
856
|
+
const bugfixOrDiagnostic = /诊断包|报错|错误|异常|崩溃|bug|问题|排查|定位|根因|复现|日志|故障/i.test(text)
|
|
857
|
+
|| /失败.{0,20}(原因|根因|排查|定位|修|修复|处理|解决)|(?:原因|根因|排查|定位).{0,20}失败/.test(text);
|
|
858
|
+
const simpleConcrete = text.length <= 80
|
|
859
|
+
&& simpleConcretePatterns.some((pattern) => pattern.test(text))
|
|
860
|
+
&& !complexScopePatterns.some((pattern) => pattern.test(text));
|
|
861
|
+
const explicitExecution = internalOpenPrdExecution || continuationRequest || explicitExecutionPatterns.some((pattern) => pattern.test(text));
|
|
862
|
+
const implementationConfirmation = implementationConfirmationPatterns.some((pattern) => pattern.test(text));
|
|
863
|
+
const noReviewRequested = noReviewRequestedPatterns.some((pattern) => pattern.test(text));
|
|
864
|
+
const noConfirmationRequested = noConfirmationRequestedPatterns.some((pattern) => pattern.test(text));
|
|
865
|
+
const reviewDecision = promptReviewCommand?.mark
|
|
866
|
+
?? (reviewConfirmPatterns.some((pattern) => pattern.test(text)) ? 'confirmed' : null)
|
|
867
|
+
?? (reviewNeedsRevisionPatterns.some((pattern) => pattern.test(text)) ? 'needs-revision' : null);
|
|
868
|
+
const confirmation = implementationConfirmation || Boolean(reviewDecision);
|
|
869
|
+
const readOnly = readOnlyPatterns.some((pattern) => pattern.test(text));
|
|
870
|
+
const codeVisualArtifactRequested = /HTML|SVG|CSS|Canvas|代码稿|源码|source artifact|可编辑矢量|可编辑稿/i.test(text);
|
|
871
|
+
const imageGenerationTerms = /图片|封面图|封面|配图|海报|插画|图标|贴纸|头像|banner|横幅|KV|主视觉|运营图|宣传图|商品图|背景图|壁纸|位图资产|效果图|视觉稿|mockup|样子|设计方向|设计预览/i;
|
|
872
|
+
const imageGenerationAction = /生成|出一张|做一张|做个|做一个|画|绘制|设计|产出|给我|来一张|先看|确认|预览|看看|截图/i;
|
|
873
|
+
const visualMockupRequest = imageGenerationTerms.test(text)
|
|
874
|
+
&& imageGenerationAction.test(text)
|
|
875
|
+
&& !codeVisualArtifactRequested;
|
|
876
|
+
const visualReview = /效果图|实现截图|视觉对比|视觉评审|对标效果图|复刻/i.test(text);
|
|
877
|
+
const directBugfixExecution = explicitExecution && bugfixOrDiagnostic;
|
|
878
|
+
const publicRepoResearchRequest = githubRepoPattern.test(text)
|
|
879
|
+
&& /(github|仓库|repo|项目|参考|对标|复刻|review|学习|架构|模块|流程|构建|测试|扩展点)/i.test(text);
|
|
880
|
+
const externalTechResearchRequest = /(第三方|library|framework|sdk|api|mcp|cli|依赖|包|版本|迁移|弃用|官方文档|参数|返回值|生命周期)/i.test(text)
|
|
881
|
+
&& /(怎么用|用法|配置|限制|版本|迁移|报错|集成|接入|最佳实践|示例|安装|参数|返回值|生命周期)/i.test(text);
|
|
882
|
+
const skillWorkflowEditRequest = /SKILL\.md|AGENTS\.md/i.test(text)
|
|
883
|
+
|| (/(^|[^a-z])(skill|skills)([^a-z]|$)/i.test(text) && /(创建|修改|优化|重构|合并|拆分|更新|工作流|workflow|流程|路由|router|提示词|规则)/i.test(text))
|
|
884
|
+
|| (/AGENTS\.md/i.test(text) && /(创建|修改|优化|精简|收薄|重构|更新)/i.test(text));
|
|
885
|
+
const secretsRequest = /(api\s*key|token|secret|credential|password|凭证|密钥|密码|账号信息|第三方服务凭证|个人信息|登录信息)/i.test(text);
|
|
886
|
+
const weappValidationRequest = /(微信小程序|miniprogram|weapp|微信开发者工具|weapp-dev-mcp)/i.test(text);
|
|
887
|
+
const browserSafetyRequest = /(computer use|browser use|浏览器|browser|网页|页面|窗口|标签页|tab|profile)/i.test(text)
|
|
888
|
+
&& /(点击|输入|提交|登录|注销|退出|支付|关闭|send|submit|type|click|switch account|切换账号)/i.test(text);
|
|
889
|
+
const productCopyRequest = /(文案|copy|错误文案|空状态|成功提示|按钮文案|提示语|toast|placeholder|设置项文案|国际化|i18n|locales|translations|localizable)/i.test(text);
|
|
890
|
+
const requiresIntake = !internalOpenPrdExecution
|
|
891
|
+
&& requirementRoutingSignals.some((pattern) => pattern.test(text))
|
|
892
|
+
&& !tinyEditPatterns.some((pattern) => pattern.test(text))
|
|
893
|
+
&& !simpleConcrete
|
|
894
|
+
&& !visualMockupRequest
|
|
895
|
+
&& !directBugfixExecution
|
|
896
|
+
&& !(readOnly && !explicitExecution);
|
|
897
|
+
return {
|
|
898
|
+
promptText: text,
|
|
899
|
+
requiresIntake,
|
|
900
|
+
explicitExecution,
|
|
901
|
+
confirmation,
|
|
902
|
+
implementationConfirmation,
|
|
903
|
+
noReviewRequested,
|
|
904
|
+
noConfirmationRequested,
|
|
905
|
+
reviewDecision,
|
|
906
|
+
reviewCommand: promptReviewCommand,
|
|
907
|
+
readOnly,
|
|
908
|
+
simpleConcrete,
|
|
909
|
+
visualMockupRequest,
|
|
910
|
+
continuationRequest,
|
|
911
|
+
continuationSessionId,
|
|
912
|
+
continuationTaskHandle,
|
|
913
|
+
continuationWorkUnitId,
|
|
914
|
+
publicRepoResearchRequest,
|
|
915
|
+
externalTechResearchRequest,
|
|
916
|
+
skillWorkflowEditRequest,
|
|
917
|
+
secretsRequest,
|
|
918
|
+
weappValidationRequest,
|
|
919
|
+
browserSafetyRequest,
|
|
920
|
+
productCopyRequest,
|
|
921
|
+
shouldInject: requiresIntake
|
|
922
|
+
|| explicitExecution
|
|
923
|
+
|| confirmation
|
|
924
|
+
|| readOnly
|
|
925
|
+
|| visualMockupRequest
|
|
926
|
+
|| continuationRequest
|
|
927
|
+
|| visualReview
|
|
928
|
+
|| publicRepoResearchRequest
|
|
929
|
+
|| externalTechResearchRequest
|
|
930
|
+
|| skillWorkflowEditRequest
|
|
931
|
+
|| secretsRequest
|
|
932
|
+
|| weappValidationRequest
|
|
933
|
+
|| browserSafetyRequest
|
|
934
|
+
|| productCopyRequest
|
|
935
|
+
|| /openprd/i.test(normalized)
|
|
936
|
+
|| /\bprd\b/i.test(normalized),
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function isShortAffirmativeConfirmation(prompt) {
|
|
941
|
+
const text = stripMarkdown(prompt).trim();
|
|
942
|
+
return /^(可以|好|行|确认|没问题|OK|ok|yes|Yes|yep|Yep)[。!!,.,s]*$/.test(text);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function detectRequirementIntakeMode(prompt) {
|
|
946
|
+
const text = String(prompt || '');
|
|
947
|
+
const deep = text.length >= 80
|
|
948
|
+
|| /新增|新建|模块|流程|编排|一站式|信息架构|工作流|workflow|wizard/i.test(text)
|
|
949
|
+
|| /多角色|权限|审批|协作|团队|客户|后台|管理/.test(text)
|
|
950
|
+
|| /AI|agent|模型|生成|自动化|集成|第三方/i.test(text)
|
|
951
|
+
|| /体验|优化|提升|更好|智能|自动|高效|完整|体系|平台/.test(text);
|
|
952
|
+
return deep ? 'deep-reflection' : 'focused-reflection';
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function readRequirementGate(root, sessionId = null) {
|
|
956
|
+
return readJsonSync(requirementGatePath(root, sessionId), null);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function writeRequirementGate(root, value, sessionId = null) {
|
|
960
|
+
const next = sessionId ? { ...value, sessionId } : value;
|
|
961
|
+
writeJsonSync(requirementGatePath(root, sessionId), next);
|
|
962
|
+
if (sessionId) {
|
|
963
|
+
writeJsonSync(legacyRequirementGatePath(root), next);
|
|
964
|
+
writeSessionBinding(root, sessionId, {
|
|
965
|
+
promptPreview: next.promptPreview ?? null,
|
|
966
|
+
gateStatus: next.status ?? null,
|
|
967
|
+
gateActive: Boolean(next.active),
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function mirrorRequirementGate(root, sessionId) {
|
|
973
|
+
if (!sessionId) return null;
|
|
974
|
+
const gate = readRequirementGate(root, sessionId);
|
|
975
|
+
if (gate) {
|
|
976
|
+
writeJsonSync(legacyRequirementGatePath(root), gate);
|
|
977
|
+
}
|
|
978
|
+
return gate;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function openRequirementGate(root, prompt, intent, sessionId = null) {
|
|
982
|
+
const current = readRequirementGate(root, sessionId);
|
|
983
|
+
const approvalPolicy = resolveRequirementApprovalPolicy(prompt, intent);
|
|
984
|
+
const gate = {
|
|
985
|
+
version: 1,
|
|
986
|
+
active: true,
|
|
987
|
+
status: 'requires-clarification',
|
|
988
|
+
openedAt: current?.openedAt || now(),
|
|
989
|
+
updatedAt: now(),
|
|
990
|
+
promptPreview: preview(prompt, 500),
|
|
991
|
+
reason: 'requirement-intake routing candidate',
|
|
992
|
+
requiredFlow: ['requirement-intake', 'clarify', 'capture', 'synthesize', 'review', 'change-generate', 'tasks', 'implementation'],
|
|
993
|
+
intakeMode: detectRequirementIntakeMode(prompt),
|
|
994
|
+
intent,
|
|
995
|
+
executionIntent: {
|
|
996
|
+
explicitRequested: Boolean(intent?.explicitExecution),
|
|
997
|
+
suppressExtraConfirmation: approvalPolicy.suppressExtraConfirmation,
|
|
998
|
+
source: approvalPolicy.suppressExtraConfirmation ? 'user-opt-out' : 'default',
|
|
999
|
+
latchedAt: now(),
|
|
1000
|
+
},
|
|
1001
|
+
approvalPolicy,
|
|
1002
|
+
reviewActionAuthorization: null,
|
|
1003
|
+
};
|
|
1004
|
+
writeRequirementGate(root, gate, sessionId);
|
|
1005
|
+
return gate;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function confirmRequirementGate(root, prompt, sessionId = null) {
|
|
1009
|
+
const current = readRequirementGate(root, sessionId);
|
|
1010
|
+
if (!current?.active) {
|
|
1011
|
+
return current;
|
|
1012
|
+
}
|
|
1013
|
+
const next = {
|
|
1014
|
+
...current,
|
|
1015
|
+
active: false,
|
|
1016
|
+
status: 'user-confirmed-for-execution',
|
|
1017
|
+
confirmedAt: now(),
|
|
1018
|
+
confirmationPreview: preview(prompt, 500),
|
|
1019
|
+
reviewActionAuthorization: null,
|
|
1020
|
+
};
|
|
1021
|
+
writeRequirementGate(root, next, sessionId);
|
|
1022
|
+
return next;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function authorizeRequirementGateExecution(root, prompt, sessionId = null, options = {}) {
|
|
1026
|
+
const current = readRequirementGate(root, sessionId);
|
|
1027
|
+
if (!current?.active) {
|
|
1028
|
+
return current;
|
|
1029
|
+
}
|
|
1030
|
+
const next = {
|
|
1031
|
+
...current,
|
|
1032
|
+
active: false,
|
|
1033
|
+
status: options.status ?? 'execution-authorized',
|
|
1034
|
+
confirmedAt: now(),
|
|
1035
|
+
confirmationPreview: preview(prompt || current.promptPreview || '', 500),
|
|
1036
|
+
authorizationReason: options.reason ?? null,
|
|
1037
|
+
reviewActionAuthorization: null,
|
|
1038
|
+
};
|
|
1039
|
+
writeRequirementGate(root, next, sessionId);
|
|
1040
|
+
return next;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function updateRequirementGate(root, patch, sessionId = null) {
|
|
1044
|
+
const current = readRequirementGate(root, sessionId);
|
|
1045
|
+
if (!current?.active) {
|
|
1046
|
+
return current;
|
|
1047
|
+
}
|
|
1048
|
+
const next = {
|
|
1049
|
+
...current,
|
|
1050
|
+
active: true,
|
|
1051
|
+
updatedAt: now(),
|
|
1052
|
+
...patch,
|
|
1053
|
+
};
|
|
1054
|
+
writeRequirementGate(root, next, sessionId);
|
|
1055
|
+
return next;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function openResearchGate(root, prompt, intent, sessionId = null) {
|
|
1059
|
+
const kind = intent?.publicRepoResearchRequest ? 'deepwiki' : 'context7';
|
|
1060
|
+
return writeNamedGate(root, 'research', {
|
|
1061
|
+
version: 1,
|
|
1062
|
+
active: true,
|
|
1063
|
+
kind,
|
|
1064
|
+
status: kind === 'deepwiki' ? 'needs-deepwiki-evidence' : 'needs-context7-evidence',
|
|
1065
|
+
openedAt: now(),
|
|
1066
|
+
updatedAt: now(),
|
|
1067
|
+
promptPreview: preview(prompt, 500),
|
|
1068
|
+
localEvidenceSeen: false,
|
|
1069
|
+
externalEvidence: {
|
|
1070
|
+
readWikiStructure: false,
|
|
1071
|
+
askQuestion: false,
|
|
1072
|
+
resolveLibraryId: false,
|
|
1073
|
+
queryDocs: false,
|
|
1074
|
+
},
|
|
1075
|
+
}, sessionId);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function closeResearchGate(root, sessionId = null, patch = {}) {
|
|
1079
|
+
const current = readNamedGate(root, 'research', sessionId);
|
|
1080
|
+
if (!current) {
|
|
1081
|
+
return null;
|
|
1082
|
+
}
|
|
1083
|
+
return writeNamedGate(root, 'research', {
|
|
1084
|
+
...current,
|
|
1085
|
+
active: false,
|
|
1086
|
+
status: patch.status || 'closed',
|
|
1087
|
+
closedAt: patch.closedAt || now(),
|
|
1088
|
+
...patch,
|
|
1089
|
+
}, sessionId);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function openSkillVisualizationGate(root, prompt, sessionId = null) {
|
|
1093
|
+
return writeNamedGate(root, 'skill-visualization', {
|
|
1094
|
+
version: 1,
|
|
1095
|
+
active: true,
|
|
1096
|
+
status: 'needs-visual-confirmation',
|
|
1097
|
+
openedAt: now(),
|
|
1098
|
+
updatedAt: now(),
|
|
1099
|
+
promptPreview: preview(prompt, 500),
|
|
1100
|
+
}, sessionId);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function confirmSkillVisualizationGate(root, prompt, sessionId = null) {
|
|
1104
|
+
const current = readNamedGate(root, 'skill-visualization', sessionId);
|
|
1105
|
+
if (!current) {
|
|
1106
|
+
return null;
|
|
1107
|
+
}
|
|
1108
|
+
return writeNamedGate(root, 'skill-visualization', {
|
|
1109
|
+
...current,
|
|
1110
|
+
active: false,
|
|
1111
|
+
status: 'user-confirmed-after-visualization',
|
|
1112
|
+
confirmedAt: now(),
|
|
1113
|
+
confirmationPreview: preview(prompt, 500),
|
|
1114
|
+
}, sessionId);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function closeSkillVisualizationGate(root, sessionId = null, patch = {}) {
|
|
1118
|
+
const current = readNamedGate(root, 'skill-visualization', sessionId);
|
|
1119
|
+
if (!current) {
|
|
1120
|
+
return null;
|
|
1121
|
+
}
|
|
1122
|
+
return writeNamedGate(root, 'skill-visualization', {
|
|
1123
|
+
...current,
|
|
1124
|
+
active: false,
|
|
1125
|
+
status: patch.status || 'closed',
|
|
1126
|
+
closedAt: patch.closedAt || now(),
|
|
1127
|
+
...patch,
|
|
1128
|
+
}, sessionId);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function openWeappGate(root, prompt, sessionId = null) {
|
|
1132
|
+
return writeNamedGate(root, 'weapp', {
|
|
1133
|
+
version: 1,
|
|
1134
|
+
active: true,
|
|
1135
|
+
status: 'needs-weapp-mcp-validation',
|
|
1136
|
+
openedAt: now(),
|
|
1137
|
+
updatedAt: now(),
|
|
1138
|
+
promptPreview: preview(prompt, 500),
|
|
1139
|
+
validationSignals: {
|
|
1140
|
+
ensureConnection: false,
|
|
1141
|
+
runtimeAction: false,
|
|
1142
|
+
},
|
|
1143
|
+
}, sessionId);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function closeWeappGate(root, sessionId = null, patch = {}) {
|
|
1147
|
+
const current = readNamedGate(root, 'weapp', sessionId);
|
|
1148
|
+
if (!current) {
|
|
1149
|
+
return null;
|
|
1150
|
+
}
|
|
1151
|
+
return writeNamedGate(root, 'weapp', {
|
|
1152
|
+
...current,
|
|
1153
|
+
active: false,
|
|
1154
|
+
status: patch.status || 'validated',
|
|
1155
|
+
closedAt: patch.closedAt || now(),
|
|
1156
|
+
...patch,
|
|
1157
|
+
}, sessionId);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function evaluateRequirementGateProgress(root, sessionId = null) {
|
|
1161
|
+
const gate = readRequirementGate(root, sessionId);
|
|
1162
|
+
const binding = readSessionBinding(root, sessionId);
|
|
1163
|
+
const promptPreview = gate?.promptPreview ?? binding?.promptPreview ?? null;
|
|
1164
|
+
const run = runOpenPrdContext(root, promptPreview);
|
|
1165
|
+
const parsed = run.parsed ?? {};
|
|
1166
|
+
const review = readRequirementLaneReview(root, sessionId);
|
|
1167
|
+
const targetedFocusChangeId = parsed?.lane?.kind === 'targeted'
|
|
1168
|
+
? (parsed.focus?.changeId ?? null)
|
|
1169
|
+
: null;
|
|
1170
|
+
const activeChange = binding?.changeId
|
|
1171
|
+
?? (sessionId
|
|
1172
|
+
? (targetedFocusChangeId ?? null)
|
|
1173
|
+
: (targetedFocusChangeId ?? parsed.focus?.changeId ?? parsed.activeChange ?? null));
|
|
1174
|
+
const taskSummary = activeChange ? readLocalTaskSummary(root, activeChange) : null;
|
|
1175
|
+
const hasTaskBreakdown = Boolean(activeChange && Number(taskSummary?.total ?? 0) > 0);
|
|
1176
|
+
const approvalPolicy = requirementApprovalPolicy(gate);
|
|
1177
|
+
let nextStep = 'implementation-ready';
|
|
1178
|
+
let reason = 'PRD 评审与任务拆解已就绪;如果当前需求原本就明确要求实现,可直接继续执行,否则等待一句明确的执行指令。';
|
|
1179
|
+
if (!review.versionId) {
|
|
1180
|
+
nextStep = 'prd-review-required';
|
|
1181
|
+
reason = '当前还没有本轮最新 PRD 评审产物,先 synthesize 出稳定 review artifact,再等待用户评审。';
|
|
1182
|
+
} else if (review.status === 'needs-revision') {
|
|
1183
|
+
nextStep = 'prd-review-required';
|
|
1184
|
+
reason = '当前 PRD review artifact 已标记为需要调整,不能继续生成 change 或实现。';
|
|
1185
|
+
} else if (review.status !== 'confirmed') {
|
|
1186
|
+
nextStep = reviewPolicyAllowsSilentRecord(approvalPolicy)
|
|
1187
|
+
? 'review-recording-required'
|
|
1188
|
+
: 'prd-review-required';
|
|
1189
|
+
reason = reviewPolicyAllowsSilentRecord(approvalPolicy)
|
|
1190
|
+
? '当前稳定 PRD 评审稿已经生成。由于用户一开始已明确要求直接做且不再额外评审/确认,本轮可以直接记录这版稳定 review artifact,再继续 change 和 tasks。'
|
|
1191
|
+
: '当前 PRD review artifact 还没有被用户确认,不能把实现授权当成 review 确认。';
|
|
1192
|
+
} else if (!activeChange) {
|
|
1193
|
+
nextStep = 'change-generation-required';
|
|
1194
|
+
reason = 'PRD 评审已确认,下一步先生成 OpenPrd change。';
|
|
1195
|
+
} else if (!hasTaskBreakdown) {
|
|
1196
|
+
nextStep = 'task-breakdown-required';
|
|
1197
|
+
reason = 'OpenPrd change 已存在,但还缺任务拆解,不能直接进入实现。';
|
|
1198
|
+
}
|
|
1199
|
+
return {
|
|
1200
|
+
runContext: parsed,
|
|
1201
|
+
binding,
|
|
1202
|
+
review,
|
|
1203
|
+
activeChange,
|
|
1204
|
+
taskSummary,
|
|
1205
|
+
recommendation: parsed.recommendation ?? null,
|
|
1206
|
+
hasTaskBreakdown,
|
|
1207
|
+
approvalPolicy,
|
|
1208
|
+
nextStep,
|
|
1209
|
+
reason,
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
function reviewActionAuthorizationFor(intent, progress, prompt) {
|
|
1214
|
+
const review = progress?.review ?? null;
|
|
1215
|
+
const requested = intent?.reviewCommand ?? null;
|
|
1216
|
+
const mark = requested?.mark ?? intent?.reviewDecision ?? null;
|
|
1217
|
+
if (!mark || !review?.versionId || !review?.digest || !review?.workUnitId) {
|
|
1218
|
+
return null;
|
|
1219
|
+
}
|
|
1220
|
+
if (requested && !reviewCommandMatchesReview(requested, review)) {
|
|
1221
|
+
return null;
|
|
1222
|
+
}
|
|
1223
|
+
return {
|
|
1224
|
+
mark,
|
|
1225
|
+
versionId: review.versionId,
|
|
1226
|
+
digest: review.digest,
|
|
1227
|
+
workUnitId: review.workUnitId,
|
|
1228
|
+
promptPreview: preview(prompt, 500),
|
|
1229
|
+
grantedAt: now(),
|
|
1230
|
+
source: 'explicit-user-review-decision',
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function silentReviewActionAuthorizationFor(gate, progress, prompt) {
|
|
1235
|
+
const policy = requirementApprovalPolicy(gate);
|
|
1236
|
+
const review = progress?.review ?? null;
|
|
1237
|
+
if (!reviewPolicyAllowsSilentRecord(policy) || !review?.versionId || !review?.digest || !review?.workUnitId) {
|
|
1238
|
+
return null;
|
|
1239
|
+
}
|
|
1240
|
+
if (review.status === 'needs-revision') {
|
|
1241
|
+
return null;
|
|
1242
|
+
}
|
|
1243
|
+
return {
|
|
1244
|
+
mark: 'confirmed',
|
|
1245
|
+
versionId: review.versionId,
|
|
1246
|
+
digest: review.digest,
|
|
1247
|
+
workUnitId: review.workUnitId,
|
|
1248
|
+
promptPreview: preview(prompt || gate?.promptPreview || '', 500),
|
|
1249
|
+
grantedAt: now(),
|
|
1250
|
+
source: 'silent-record-policy',
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
function holdRequirementGate(root, prompt, progress, sessionId = null, extra = {}) {
|
|
1255
|
+
const reviewActionAuthorization = extra.reviewActionAuthorization ?? null;
|
|
1256
|
+
return updateRequirementGate(root, {
|
|
1257
|
+
status: extra.status ?? progress.nextStep,
|
|
1258
|
+
confirmationPreview: preview(prompt, 500),
|
|
1259
|
+
reviewActionAuthorization,
|
|
1260
|
+
}, sessionId);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function isImplementationAdvanceIntent(intent) {
|
|
1264
|
+
return Boolean(intent?.explicitExecution || intent?.implementationConfirmation);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
function gateHasConfirmedCurrentReview(gate, progress) {
|
|
1268
|
+
const authorization = gate?.reviewActionAuthorization;
|
|
1269
|
+
const review = progress?.review;
|
|
1270
|
+
if (review?.status !== 'confirmed') {
|
|
1271
|
+
return false;
|
|
1272
|
+
}
|
|
1273
|
+
if (authorization) {
|
|
1274
|
+
return Boolean(
|
|
1275
|
+
authorization.versionId === review.versionId
|
|
1276
|
+
&& authorization.digest === review.digest
|
|
1277
|
+
&& authorization.workUnitId === review.workUnitId
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
return reviewPolicyAllowsSilentRecord(requirementApprovalPolicy(gate))
|
|
1281
|
+
&& Boolean(review.versionId && review.digest && review.workUnitId);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function canAutoAuthorizeRequirementExecution(gate, progress) {
|
|
1285
|
+
return Boolean(
|
|
1286
|
+
isBlockingRequirementGate(gate)
|
|
1287
|
+
&& gate?.intent?.explicitExecution
|
|
1288
|
+
&& progress?.nextStep === 'implementation-ready'
|
|
1289
|
+
&& gateHasConfirmedCurrentReview(gate, progress)
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function isAuthorizedReviewMarkCommand(payload, gate) {
|
|
1294
|
+
const authorization = gate?.reviewActionAuthorization;
|
|
1295
|
+
const command = parseReviewMarkCommand(commandText(payload));
|
|
1296
|
+
if (!authorization || !command) {
|
|
1297
|
+
return false;
|
|
1298
|
+
}
|
|
1299
|
+
return authorization.mark === command.mark
|
|
1300
|
+
&& authorization.versionId === command.versionId
|
|
1301
|
+
&& authorization.digest === command.digest
|
|
1302
|
+
&& authorization.workUnitId === command.workUnitId;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function isSilentRecordReviewMarkCommand(payload, gate, progress) {
|
|
1306
|
+
const authorization = silentReviewActionAuthorizationFor(gate, progress, gate?.promptPreview || '');
|
|
1307
|
+
const command = parseReviewMarkCommand(commandText(payload));
|
|
1308
|
+
if (!authorization || !command) {
|
|
1309
|
+
return false;
|
|
1310
|
+
}
|
|
1311
|
+
return authorization.mark === command.mark
|
|
1312
|
+
&& authorization.versionId === command.versionId
|
|
1313
|
+
&& authorization.digest === command.digest
|
|
1314
|
+
&& authorization.workUnitId === command.workUnitId;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function isReadOnlyRequirementGate(gate) {
|
|
1318
|
+
return Boolean(gate?.active && gate?.intent?.readOnly && !gate?.intent?.explicitExecution);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
function isBlockingRequirementGate(gate) {
|
|
1322
|
+
return Boolean(gate?.active && !isReadOnlyRequirementGate(gate));
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
function isRequirementGateActive(root, sessionId = null) {
|
|
1326
|
+
return isBlockingRequirementGate(readRequirementGate(root, sessionId));
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
function isMutationPayload(payload, risk) {
|
|
1330
|
+
const text = payloadText(payload);
|
|
1331
|
+
const tool = String(payload.tool_name || payload.toolName || payload.name || '');
|
|
1332
|
+
return risk.level === 'medium'
|
|
1333
|
+
|| risk.level === 'high'
|
|
1334
|
+
|| /apply_patch/i.test(tool)
|
|
1335
|
+
|| /apply_patch/i.test(text)
|
|
1336
|
+
|| /\*\*\* Begin Patch/.test(text);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
function isAllowedDuringRequirementGate(root, payload, gate, sessionId = null) {
|
|
1340
|
+
const text = payloadText(payload);
|
|
1341
|
+
const command = commandText(payload);
|
|
1342
|
+
const progress = evaluateRequirementGateProgress(root, sessionId);
|
|
1343
|
+
const alwaysAllowed = [
|
|
1344
|
+
/openprd\s+status\b/i,
|
|
1345
|
+
/openprd\s+next\b/i,
|
|
1346
|
+
/openprd\s+run\s+\.\s+--context\b/i,
|
|
1347
|
+
/openprd\s+run\s+\.\s+--verify\b/i,
|
|
1348
|
+
/openprd\s+clarify\b/i,
|
|
1349
|
+
/openprd\s+capture\b/i,
|
|
1350
|
+
/openprd\s+classify\b/i,
|
|
1351
|
+
/openprd\s+interview\b/i,
|
|
1352
|
+
/openprd\s+synthesize\b/i,
|
|
1353
|
+
/openprd\s+diagram\b/i,
|
|
1354
|
+
/openprd\s+review-presentation\b/i,
|
|
1355
|
+
/openprd\s+standards\s+.*--verify/i,
|
|
1356
|
+
/openprd\s+quality\s+.*--verify/i,
|
|
1357
|
+
/openprd\s+doctor\b/i,
|
|
1358
|
+
];
|
|
1359
|
+
if (alwaysAllowed.some((pattern) => pattern.test(text))) {
|
|
1360
|
+
return true;
|
|
1361
|
+
}
|
|
1362
|
+
if (/openprd\s+review\b/i.test(command)) {
|
|
1363
|
+
if (!/--mark\b/i.test(command)) {
|
|
1364
|
+
return true;
|
|
1365
|
+
}
|
|
1366
|
+
return isAuthorizedReviewMarkCommand(payload, gate)
|
|
1367
|
+
|| isSilentRecordReviewMarkCommand(payload, gate, progress);
|
|
1368
|
+
}
|
|
1369
|
+
if (/openprd\s+change\s+.*--generate/i.test(command)) {
|
|
1370
|
+
return progress.review.status === 'confirmed';
|
|
1371
|
+
}
|
|
1372
|
+
if (/openprd\s+change\s+.*--validate/i.test(command)) {
|
|
1373
|
+
return true;
|
|
1374
|
+
}
|
|
1375
|
+
if (/openprd\s+tasks\b/i.test(command) || /openprd\s+loop\s+.*--plan/i.test(command)) {
|
|
1376
|
+
return progress.review.status === 'confirmed' && Boolean(progress.activeChange);
|
|
1377
|
+
}
|
|
1378
|
+
return false;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
function toolProbe(payload) {
|
|
1382
|
+
return `${toolName(payload)}\n${payloadText(payload)}\n${commandText(payload)}`.toLowerCase();
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
function looksLikeLocalEvidenceRead(payload) {
|
|
1386
|
+
const probe = toolProbe(payload);
|
|
1387
|
+
return /\b(rg|grep|cat|sed|head|tail|less|more|wc|ls|find|git show|read_file|open_file|view_file)\b/.test(probe)
|
|
1388
|
+
|| /openprd\s+(status|next|doctor|run\s+\.\s+--context|run\s+\.\s+--verify)/.test(probe);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
function applyResearchToolSignal(root, payload, sessionId = null) {
|
|
1392
|
+
const current = readNamedGate(root, 'research', sessionId);
|
|
1393
|
+
if (!current?.active) {
|
|
1394
|
+
return current;
|
|
1395
|
+
}
|
|
1396
|
+
const probe = toolProbe(payload);
|
|
1397
|
+
const externalEvidence = { ...(current.externalEvidence || {}) };
|
|
1398
|
+
if (looksLikeLocalEvidenceRead(payload)) {
|
|
1399
|
+
current.localEvidenceSeen = true;
|
|
1400
|
+
}
|
|
1401
|
+
if (/read[_-]?wiki[_-]?structure|readwikistructure/.test(probe)) {
|
|
1402
|
+
externalEvidence.readWikiStructure = true;
|
|
1403
|
+
}
|
|
1404
|
+
if (/ask[_-]?question|askquestion/.test(probe)) {
|
|
1405
|
+
externalEvidence.askQuestion = true;
|
|
1406
|
+
}
|
|
1407
|
+
if (/resolve[_-]?library[_-]?id|resolvelibraryid/.test(probe)) {
|
|
1408
|
+
externalEvidence.resolveLibraryId = true;
|
|
1409
|
+
}
|
|
1410
|
+
if (/query[_-]?docs|querydocs/.test(probe)) {
|
|
1411
|
+
externalEvidence.queryDocs = true;
|
|
1412
|
+
}
|
|
1413
|
+
const satisfied = current.kind === 'deepwiki'
|
|
1414
|
+
? externalEvidence.readWikiStructure && externalEvidence.askQuestion
|
|
1415
|
+
: externalEvidence.resolveLibraryId && externalEvidence.queryDocs;
|
|
1416
|
+
if (satisfied) {
|
|
1417
|
+
return closeResearchGate(root, sessionId, {
|
|
1418
|
+
status: 'evidence-collected',
|
|
1419
|
+
externalEvidence,
|
|
1420
|
+
localEvidenceSeen: current.localEvidenceSeen,
|
|
1421
|
+
satisfiedAt: now(),
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
return updateNamedGate(root, 'research', {
|
|
1425
|
+
externalEvidence,
|
|
1426
|
+
localEvidenceSeen: current.localEvidenceSeen,
|
|
1427
|
+
}, sessionId);
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
function looksLikeSkillContractMutation(root, payload) {
|
|
1431
|
+
const files = extractTouchedFiles(root, payload);
|
|
1432
|
+
if (files.some((file) => /(^|\/)(AGENTS\.md|SKILL\.md)$/i.test(file) || /(^|\/)skills\/.+/i.test(file))) {
|
|
1433
|
+
return true;
|
|
1434
|
+
}
|
|
1435
|
+
const probe = toolProbe(payload);
|
|
1436
|
+
return /AGENTS\.md|SKILL\.md|\/skills\//i.test(probe);
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
function isRawVaultReadAttempt(payload) {
|
|
1440
|
+
const probe = toolProbe(payload);
|
|
1441
|
+
const readLike = /\b(cat|sed|head|tail|less|more|rg|grep|read_file|open_file)\b/.test(probe);
|
|
1442
|
+
return readLike && /\bvault\b/.test(probe);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
function applyWeappToolSignal(root, payload, sessionId = null) {
|
|
1446
|
+
const current = readNamedGate(root, 'weapp', sessionId);
|
|
1447
|
+
if (!current?.active) {
|
|
1448
|
+
return current;
|
|
1449
|
+
}
|
|
1450
|
+
const probe = toolProbe(payload);
|
|
1451
|
+
const validationSignals = { ...(current.validationSignals || {}) };
|
|
1452
|
+
if (/mp[_-]?ensureconnection|ensureconnection/.test(probe)) {
|
|
1453
|
+
validationSignals.ensureConnection = true;
|
|
1454
|
+
}
|
|
1455
|
+
if (/mp[_-]|weapp|miniprogram|page_|element_|mp_screenshot|network\b|evaluate\b/.test(probe)) {
|
|
1456
|
+
validationSignals.runtimeAction = true;
|
|
1457
|
+
}
|
|
1458
|
+
const satisfied = validationSignals.ensureConnection && validationSignals.runtimeAction;
|
|
1459
|
+
if (satisfied) {
|
|
1460
|
+
return closeWeappGate(root, sessionId, {
|
|
1461
|
+
status: 'validated-through-weapp-mcp',
|
|
1462
|
+
validationSignals,
|
|
1463
|
+
validatedAt: now(),
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
return updateNamedGate(root, 'weapp', { validationSignals }, sessionId);
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
function isHighRiskBrowserAction(payload) {
|
|
1470
|
+
const probe = toolProbe(payload);
|
|
1471
|
+
const browserTool = /(computer|browser|chrome|playwright)/.test(probe);
|
|
1472
|
+
const dangerousAction = /(submit|send|delete|remove|logout|sign out|switch account|pay|purchase|close tab|close window|关闭标签页|关闭窗口|退出登录|切换账号|支付|删除|发送|提交)/.test(probe);
|
|
1473
|
+
return browserTool && dangerousAction;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
function researchGateMessage(gate) {
|
|
1477
|
+
if (!gate?.active) {
|
|
1478
|
+
return null;
|
|
1479
|
+
}
|
|
1480
|
+
if (gate.kind === 'deepwiki') {
|
|
1481
|
+
return [
|
|
1482
|
+
'OpenPrd 外部仓库调研门禁: active。',
|
|
1483
|
+
'当前请求涉及公开 GitHub 仓库的架构、模块、流程或对标判断;在修改实现或把结论表述为已确认之前,先读本地证据,再使用 DeepWiki。',
|
|
1484
|
+
'最小动作: `read_wiki_structure` 一次,再用 `ask_question` 聚焦 1-2 个关键问题;证据够用后立即停止扩展。',
|
|
1485
|
+
gate.externalEvidence?.readWikiStructure ? '已记录: read_wiki_structure。' : '缺少: read_wiki_structure。',
|
|
1486
|
+
gate.externalEvidence?.askQuestion ? '已记录: ask_question。' : '缺少: ask_question。',
|
|
1487
|
+
].join('\n');
|
|
1488
|
+
}
|
|
1489
|
+
return [
|
|
1490
|
+
'OpenPrd 外部技术调研门禁: active。',
|
|
1491
|
+
'当前请求涉及项目外技术事实、第三方库、框架、API、SDK、MCP 或 CLI 用法;在修改实现或输出配置/代码结论前,先检查本地代码、锁文件、README、类型定义是否足够,不足时再使用 Context7。',
|
|
1492
|
+
'最小动作: `resolve_library_id` 一次,再 `query_docs` 1-2 次;若覆盖不足,明确缺口后再补官方文档或源码。',
|
|
1493
|
+
gate.externalEvidence?.resolveLibraryId ? '已记录: resolve_library_id。' : '缺少: resolve_library_id。',
|
|
1494
|
+
gate.externalEvidence?.queryDocs ? '已记录: query_docs。' : '缺少: query_docs。',
|
|
1495
|
+
].join('\n');
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
function skillVisualizationGateMessage(gate) {
|
|
1499
|
+
if (!gate?.active) {
|
|
1500
|
+
return null;
|
|
1501
|
+
}
|
|
1502
|
+
return [
|
|
1503
|
+
'OpenPrd skill/AGENTS 可视化确认门禁: active。',
|
|
1504
|
+
'当前请求涉及 skill、SKILL.md、AGENTS.md 或相关 workflow 规则变更。编辑前必须先读取现状,输出彩色 Mermaid 方案图,再用简短文字说明新增、修改、保持不变和删除/阻断项。',
|
|
1505
|
+
'Mermaid 必须包含 `unchanged`、`added`、`changed`、`removed` 四种 classDef,并等待用户明确确认后才能修改相关文件。',
|
|
1506
|
+
].join('\n');
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
function credentialMessage(intent) {
|
|
1510
|
+
if (!intent?.secretsRequest) {
|
|
1511
|
+
return null;
|
|
1512
|
+
}
|
|
1513
|
+
return [
|
|
1514
|
+
'OpenPrd 敏感信息规则:',
|
|
1515
|
+
'如果任务需要 API key、token、账号信息、第三方服务凭证或个人信息,先使用 `secrets-vault` skill 获取已有凭证,不要立即向用户索要。',
|
|
1516
|
+
'不要直接读取原始 vault 文件,也不要在日志、代码或回复里暴露完整密钥。',
|
|
1517
|
+
].join('\n');
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
function weappGateMessage(gate) {
|
|
1521
|
+
if (!gate?.active) {
|
|
1522
|
+
return null;
|
|
1523
|
+
}
|
|
1524
|
+
return [
|
|
1525
|
+
'OpenPrd 微信小程序验证门禁: active。',
|
|
1526
|
+
'当前任务涉及微信小程序测试、验证、截图、日志、网络请求、微信开发者工具自动化,或可能影响小程序运行态的代码修改。',
|
|
1527
|
+
'请先使用 `weapp-dev-mcp` skill,并通过本地 `weapp-dev-mcp` MCP 完成运行态验证;未通过本地 MCP 实际验证时,不要宣称“小程序已验证”。',
|
|
1528
|
+
].join('\n');
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
function browserSafetyMessage(intent) {
|
|
1532
|
+
if (!intent?.browserSafetyRequest) {
|
|
1533
|
+
return null;
|
|
1534
|
+
}
|
|
1535
|
+
return [
|
|
1536
|
+
'OpenPrd 浏览器安全提醒:',
|
|
1537
|
+
'用户明确要求 Computer Use 时,优先使用 Computer Use,并尽量在 Codex-owned browser window 中操作。',
|
|
1538
|
+
'执行点击、输入、提交、关闭、切换账号、退出登录、支付等高风险动作前,先确认窗口归属,检查当前窗口标题、目标页面和可见内容仍属于本任务。',
|
|
1539
|
+
].join('\n');
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
function productCopyMessage(intent) {
|
|
1543
|
+
if (!intent?.productCopyRequest) {
|
|
1544
|
+
return null;
|
|
1545
|
+
}
|
|
1546
|
+
return [
|
|
1547
|
+
'OpenPrd 产品内文案提醒:',
|
|
1548
|
+
'先检查项目是否已有 i18n、locales、translations、Localizable 或其他语言资源;若已有,多语言要同步维护。',
|
|
1549
|
+
'用户可见文案默认面向普通用户,优先写结果和下一步,不要把 API、SDK、模型、数据库、缓存、错误码或其他实现细节直接暴露给用户。',
|
|
1550
|
+
].join('\n');
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
function composeHookContext(root, intent = null, gate = null, progress = null, sessionId = null) {
|
|
1554
|
+
return [
|
|
1555
|
+
contextMessage(root, intent, gate, progress),
|
|
1556
|
+
researchGateMessage(readNamedGate(root, 'research', sessionId)),
|
|
1557
|
+
skillVisualizationGateMessage(readNamedGate(root, 'skill-visualization', sessionId)),
|
|
1558
|
+
credentialMessage(intent),
|
|
1559
|
+
weappGateMessage(readNamedGate(root, 'weapp', sessionId)),
|
|
1560
|
+
browserSafetyMessage(intent),
|
|
1561
|
+
productCopyMessage(intent),
|
|
1562
|
+
].filter(Boolean).join('\n');
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
function runOpenPrd(args, cwd) {
|
|
1566
|
+
const command = process.env.OPENPRD_CLI || 'openprd';
|
|
1567
|
+
const result = spawnSync(command, args, {
|
|
1568
|
+
cwd,
|
|
1569
|
+
encoding: 'utf8',
|
|
1570
|
+
timeout: 15000,
|
|
1571
|
+
env: process.env,
|
|
1572
|
+
});
|
|
1573
|
+
return {
|
|
1574
|
+
ok: result.status === 0,
|
|
1575
|
+
status: result.status,
|
|
1576
|
+
stdout: (result.stdout || '').trim(),
|
|
1577
|
+
stderr: (result.stderr || '').trim(),
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
function parseJsonOutput(text) {
|
|
1582
|
+
const source = String(text || '').trim();
|
|
1583
|
+
if (!source) {
|
|
1584
|
+
return null;
|
|
1585
|
+
}
|
|
1586
|
+
try {
|
|
1587
|
+
return JSON.parse(source);
|
|
1588
|
+
} catch {
|
|
1589
|
+
return null;
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
function shouldRunDoctorForHighRisk(payload) {
|
|
1594
|
+
const text = commandText(payload) || payloadText(payload);
|
|
1595
|
+
return /(git\s+(commit|push)\b|npm\s+publish|pnpm\s+publish|yarn\s+npm\s+publish|gh\s+release|openprd\s+(freeze|handoff|setup|update|fleet|doctor)\b|openprd\s+change\s+.*--(apply|archive)\b|release|publish)/i.test(text);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
function summarizeRunVerifyCheck(parsed, fallbackText = '') {
|
|
1599
|
+
if (!parsed) {
|
|
1600
|
+
return fallbackText || 'run verify result unavailable';
|
|
1601
|
+
}
|
|
1602
|
+
const readiness = parsed.readiness ?? {};
|
|
1603
|
+
const failedTaskChecks = Array.isArray(parsed.checks)
|
|
1604
|
+
? parsed.checks.filter((check) => check.scope !== 'workspace' && !check.ok).map((check) => check.name)
|
|
1605
|
+
: [];
|
|
1606
|
+
const workspaceWarnings = Array.isArray(parsed.warnings) ? parsed.warnings : [];
|
|
1607
|
+
if (readiness.taskReady === false) {
|
|
1608
|
+
return `run-verify: taskReady=no${failedTaskChecks.length ? ` (${failedTaskChecks.join(', ')})` : ''}`;
|
|
1609
|
+
}
|
|
1610
|
+
if (readiness.workspaceReady === false) {
|
|
1611
|
+
return `run-verify: taskReady=yes, workspaceReady=no${workspaceWarnings.length ? ` (${workspaceWarnings[0]})` : ''}`;
|
|
1612
|
+
}
|
|
1613
|
+
return 'run-verify: taskReady=yes, workspaceReady=yes';
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
function summarizeDoctorCheck(parsed, fallbackText = '') {
|
|
1617
|
+
if (!parsed?.agentIntegration && !parsed?.standards && !parsed?.validation) {
|
|
1618
|
+
return fallbackText || 'doctor result unavailable';
|
|
1619
|
+
}
|
|
1620
|
+
const parts = [];
|
|
1621
|
+
if (parsed.agentIntegration) {
|
|
1622
|
+
parts.push(`agentIntegration=${parsed.agentIntegration.ok ? 'ok' : 'failed'}`);
|
|
1623
|
+
}
|
|
1624
|
+
if (parsed.standards) {
|
|
1625
|
+
parts.push(`standards=${parsed.standards.ok ? 'ok' : 'failed'}`);
|
|
1626
|
+
}
|
|
1627
|
+
if (parsed.validation) {
|
|
1628
|
+
parts.push(`validation=${parsed.validation.valid ? 'ok' : 'failed'}`);
|
|
1629
|
+
}
|
|
1630
|
+
return `doctor: ${parts.join(', ')}`;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
function buildGateFailureEnvelope(result) {
|
|
1634
|
+
const runCheck = result.checks.find((check) => check.name === 'run-verify') ?? null;
|
|
1635
|
+
const doctorCheck = result.checks.find((check) => check.name === 'doctor' && check.ok === false && check.agentIntegrationOk === false) ?? null;
|
|
1636
|
+
const changeCheck = result.checks.find((check) => check.name === 'change-validate' && check.ok === false) ?? null;
|
|
1637
|
+
if (doctorCheck) {
|
|
1638
|
+
return {
|
|
1639
|
+
kind: 'integration-drift',
|
|
1640
|
+
details: [doctorCheck.summary, ...(doctorCheck.details ?? [])].filter(Boolean),
|
|
1641
|
+
repair: 'Repair path: run openprd doctor . to inspect drift, then use openprd update . or the targeted repair command before retrying this high-risk action.',
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
if (runCheck?.taskReady === false || changeCheck) {
|
|
1645
|
+
return {
|
|
1646
|
+
kind: 'task-failure',
|
|
1647
|
+
details: [
|
|
1648
|
+
runCheck?.summary,
|
|
1649
|
+
...(runCheck?.errors ?? []),
|
|
1650
|
+
...(changeCheck?.details ?? []),
|
|
1651
|
+
].filter(Boolean),
|
|
1652
|
+
repair: 'Repair path: fix the task-scoped failure, rerun the relevant verification command, then retry this high-risk action.',
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
if (runCheck?.workspaceReady === false) {
|
|
1656
|
+
return {
|
|
1657
|
+
kind: 'workspace-debt',
|
|
1658
|
+
details: [runCheck.summary, ...(runCheck.warnings ?? [])].filter(Boolean),
|
|
1659
|
+
repair: 'Repair path: resolve the workspace-level debt from openprd run . --verify or openprd quality . --verify, then retry this high-risk action.',
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
return {
|
|
1663
|
+
kind: 'task-failure',
|
|
1664
|
+
details: result.checks.filter((check) => !check.ok).flatMap((check) => [check.summary, ...(check.details ?? [])]).filter(Boolean),
|
|
1665
|
+
repair: 'Repair path: rerun the relevant verification command, fix the failing check, then retry.',
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
function formatHighRiskGateBlock(result) {
|
|
1670
|
+
const envelope = result.envelope ?? buildGateFailureEnvelope(result);
|
|
1671
|
+
const headline = envelope.kind === 'workspace-debt'
|
|
1672
|
+
? 'OpenPrd blocked a high-risk action because the current task is done but the workspace is not fully ready.'
|
|
1673
|
+
: envelope.kind === 'integration-drift'
|
|
1674
|
+
? 'OpenPrd blocked a high-risk action because the integration health gate failed.'
|
|
1675
|
+
: 'OpenPrd blocked a high-risk action because the current task is not ready.';
|
|
1676
|
+
return [
|
|
1677
|
+
headline,
|
|
1678
|
+
result.summary,
|
|
1679
|
+
...envelope.details,
|
|
1680
|
+
envelope.repair,
|
|
1681
|
+
].filter(Boolean).join('\n');
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
function classifyToolFailure(text) {
|
|
1685
|
+
const normalized = String(text || '').toLowerCase();
|
|
1686
|
+
if (/doctor|hook|hook-profile|hooks\.json|config\.toml|command-catalog|openprd-router|skill|integration|drift/.test(normalized)) {
|
|
1687
|
+
return 'integration-drift';
|
|
1688
|
+
}
|
|
1689
|
+
if (/production-ready|needs-attention|smoke evidence|feature coverage|workspace ready|workspace debt|quality/.test(normalized)) {
|
|
1690
|
+
return 'workspace-debt';
|
|
1691
|
+
}
|
|
1692
|
+
return 'task-failure';
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
function toolFailureMessage(kind) {
|
|
1696
|
+
if (kind === 'integration-drift') {
|
|
1697
|
+
return 'A tool command failed in a way that looks like integration drift. Use openprd doctor . first; if drift is confirmed, repair it with openprd update . or the targeted fix before continuing.';
|
|
1698
|
+
}
|
|
1699
|
+
if (kind === 'workspace-debt') {
|
|
1700
|
+
return 'A tool command failed against workspace-level readiness. Re-run openprd run . --verify or openprd quality . --verify, separate current-task status from historical workspace debt, and only then choose the repair path.';
|
|
1701
|
+
}
|
|
1702
|
+
return 'A tool command failed against the current task. Use openprd next . and the relevant verification command to identify the smallest task-scoped repair path.';
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
function recordRunHook(cwd, baseEvent, outcome) {
|
|
1706
|
+
const args = [
|
|
1707
|
+
'run',
|
|
1708
|
+
'.',
|
|
1709
|
+
'--record-hook',
|
|
1710
|
+
'--event',
|
|
1711
|
+
baseEvent.eventName,
|
|
1712
|
+
'--risk',
|
|
1713
|
+
baseEvent.risk.level,
|
|
1714
|
+
'--outcome',
|
|
1715
|
+
outcome,
|
|
1716
|
+
];
|
|
1717
|
+
if (baseEvent.preview) {
|
|
1718
|
+
args.push('--preview', baseEvent.preview.slice(0, 300));
|
|
1719
|
+
}
|
|
1720
|
+
runOpenPrd(args, cwd);
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
function requirementGateMessage(intent, gate) {
|
|
1724
|
+
const gateBlocksImplementation = isBlockingRequirementGate(gate);
|
|
1725
|
+
const approvalPolicy = requirementApprovalPolicy(gate);
|
|
1726
|
+
if (!intent?.requiresIntake && !gateBlocksImplementation) {
|
|
1727
|
+
return null;
|
|
1728
|
+
}
|
|
1729
|
+
if (intent?.visualMockupRequest) {
|
|
1730
|
+
return [
|
|
1731
|
+
'OpenPrd requirement intake gate is active only for implementation writes.',
|
|
1732
|
+
'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.',
|
|
1733
|
+
'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.',
|
|
1734
|
+
'Do not create temporary HTML/SVG/CSS files for this image unless the user explicitly requested that format.',
|
|
1735
|
+
'Use Codex native Image 2 to generate the image; keep implementation, PRD review, and visual-compare for later explicit confirmation.',
|
|
1736
|
+
].join('\n');
|
|
1737
|
+
}
|
|
1738
|
+
const status = gateBlocksImplementation ? 'active' : 'opened';
|
|
1739
|
+
return [
|
|
1740
|
+
'OpenPrd requirement intake gate: ' + status + '.',
|
|
1741
|
+
'This prompt matched the requirement-intake safety lane. Do not decide from fixed keywords; first use $openprd-requirement-intake to classify L0/L1/L2 by impact, unknowns, decision cost, and validation cost.',
|
|
1742
|
+
'If the intake decision is L2, do not edit implementation files yet and proceed through PRD/review/change/tasks with the appropriate base/consumer/b2b/agent PRD lens.',
|
|
1743
|
+
reviewPolicyAllowsSilentRecord(approvalPolicy)
|
|
1744
|
+
? 'Decision-point policy: clarify the requirement, capture user answers, synthesize the PRD, record the exact current stable review artifact, generate the OpenPrd change, prepare the task breakdown, then implement within the confirmed scope.'
|
|
1745
|
+
: 'Decision-point policy: clarify the requirement, capture user answers, synthesize the PRD, wait for a human decision on the stable review artifact, generate the OpenPrd change, prepare the task breakdown, then implement within the confirmed scope.',
|
|
1746
|
+
reviewPolicyAllowsSilentRecord(approvalPolicy)
|
|
1747
|
+
? 'This lane is in silent-record mode because the user upfront asked to implement without another review stop. You may record only the exact current version, digest, and work unit.'
|
|
1748
|
+
: 'Review-artifact confirmation and implementation authorization are different gates: do not treat "可以开做" as permission to run openprd review --mark confirmed.',
|
|
1749
|
+
'If the original request already asked to implement, execution can continue once the active approval policy and tasks are ready; otherwise wait for a clear execution request.',
|
|
1750
|
+
'Recommended next action: write a short Intake Decision in chat, then for L2 run openprd clarify ., summarize target, scope, out-of-scope, and acceptance in chat, then ask for confirmation. Do not open a clarification HTML page; the formal HTML review happens after synthesize/review.',
|
|
1751
|
+
].join('\n');
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
function visualMockupMessage(intent) {
|
|
1755
|
+
if (!intent?.visualMockupRequest) {
|
|
1756
|
+
return null;
|
|
1757
|
+
}
|
|
1758
|
+
return [
|
|
1759
|
+
'当前用户要的是图片内容生成,例如图片、封面图、配图、海报、插画、图标、贴纸、头像、banner、主视觉/KV、运营图、效果图、视觉稿或 mockup。',
|
|
1760
|
+
'默认直接调用 Codex 原生 Image 2 生成图片;除非用户明确指定 HTML、SVG、CSS、Canvas、代码稿或可编辑矢量/source artifact,不要改用临时 HTML/SVG/CSS 再截图。',
|
|
1761
|
+
'对 logo、icon、avatar、badge 等开发素材,如果用户没有明确要求 mockup、场景图、设备框、卡片承载、名片/包装展示或参考界面复刻,默认按独立素材输出(standalone asset)处理:使用全画布单主体,不额外添加 UI frame、卡片、设备壳、名片、桌面陈列、手持实拍或其他展示容器。',
|
|
1762
|
+
'只有当用户明确要求 mockup、场景化效果图、容器化呈现,或参考图本身就包含这些承载结构时,才生成对应的容器或场景。',
|
|
1763
|
+
'OpenPrd review.html 只用于需求评审,visual-compare 只用于实现阶段已有参考图后的实现截图对比。',
|
|
1764
|
+
].join('\n');
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
function codexConfirmationReplyRule() {
|
|
1768
|
+
return 'Codex UI 规则: 只有当前 approval policy 仍然需要人类对稳定 review artifact 做决定时,才在 final answer 里停下来请求确认;如果当前 lane 已进入 silent-record 策略,就继续记录精确 review artifact 并推进,不要为了同一个需求再额外停顿。';
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
function confirmationGateMessage(gate) {
|
|
1772
|
+
if (!gate || gate.active) {
|
|
1773
|
+
return null;
|
|
1774
|
+
}
|
|
1775
|
+
const intro = gate.status === 'execution-authorized'
|
|
1776
|
+
? 'OpenPrd requirement intake gate auto-authorized execution after review confirmation and task preparation because the original user request already asked to implement.'
|
|
1777
|
+
: 'OpenPrd requirement intake gate was explicitly confirmed by the user.';
|
|
1778
|
+
return [
|
|
1779
|
+
intro,
|
|
1780
|
+
'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.',
|
|
1781
|
+
'For UI or visual work with an existing reference image, capture the implemented UI and run openprd visual-compare . --reference <effect-image> --actual <implementation-screenshot>; inspect the generated JPG before claiming the visual work is complete.',
|
|
1782
|
+
].join('\n');
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
function currentRequirementStatusLine(gate, progress) {
|
|
1786
|
+
if (gate?.status === 'review-confirmation-authorized') {
|
|
1787
|
+
return '用户刚刚确认了当前稳定 PRD 评审稿;本回合只允许记录这一个版本的 review 状态,不能把它直接扩展成实现确认。';
|
|
1788
|
+
}
|
|
1789
|
+
if (gate?.status === 'review-recording-authorized') {
|
|
1790
|
+
return '当前稳定 PRD 评审稿已经按 silent-record 策略授权记录;只允许写回这一个版本,随后继续 change 和 tasks。';
|
|
1791
|
+
}
|
|
1792
|
+
switch (progress?.nextStep) {
|
|
1793
|
+
case 'prd-review-required':
|
|
1794
|
+
return progress?.review?.versionId
|
|
1795
|
+
? '当前卡点: 先等用户确认当前稳定 PRD 评审稿;不要把“继续做”或“开落地吧”当成 review 确认。'
|
|
1796
|
+
: '当前卡点: 先 synthesize 出本轮稳定 PRD 评审稿,再等待用户评审。';
|
|
1797
|
+
case 'review-recording-required':
|
|
1798
|
+
return '当前卡点: 稳定 PRD 评审稿已经生成;按当前 approval policy 可直接记录这版 review artifact,不需要再额外追问用户。';
|
|
1799
|
+
case 'change-generation-required':
|
|
1800
|
+
return '当前卡点: PRD 评审已确认,下一步先生成 OpenPrd change。';
|
|
1801
|
+
case 'task-breakdown-required':
|
|
1802
|
+
return '当前卡点: change 已存在,但还缺任务拆解,不能直接进入实现。';
|
|
1803
|
+
case 'implementation-ready':
|
|
1804
|
+
return '当前卡点: review 已确认且 tasks 已就绪;如果当前需求原本就明确要求实现,可直接进入实现,否则等待一句明确的执行指令。';
|
|
1805
|
+
default:
|
|
1806
|
+
return '当前卡点: 继续按“澄清 -> 评审 -> change -> tasks -> 实现”的顺序推进。';
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
function currentRequirementMessage(intent, gate, progress) {
|
|
1811
|
+
const prompt = stripMarkdown(gate?.promptPreview || '');
|
|
1812
|
+
const approvalPolicy = requirementApprovalPolicy(gate);
|
|
1813
|
+
const gateStatus = isImplementationAdvanceIntent(intent)
|
|
1814
|
+
? 'OpenPrd requirement intake gate was explicitly confirmed by the user.'
|
|
1815
|
+
: 'OpenPrd requirement intake gate: active.';
|
|
1816
|
+
const lines = [
|
|
1817
|
+
'OpenPrd 当前需求入口',
|
|
1818
|
+
gateStatus,
|
|
1819
|
+
'当前输入命中了需求入口安全通道。不要按固定关键词判断 L2;先用 $openprd-requirement-intake 按影响面、未知数、决策成本和验证成本做语义分流。',
|
|
1820
|
+
'如果分流结果是 L2,本轮只围绕这个新需求推进 PRD/review/change/tasks,并选择 base/consumer/b2b/agent PRD lens,不自动继续历史 active change。',
|
|
1821
|
+
prompt ? '本轮需求: ' + prompt : '',
|
|
1822
|
+
gate?.intakeMode === 'deep-reflection'
|
|
1823
|
+
? '需求入口: 先运行需求自省,再输出对话内澄清摘要或简短清单。'
|
|
1824
|
+
: '需求入口: 先做轻量项目映射,再确认影响点和验收方式。',
|
|
1825
|
+
reviewPolicyAllowsSilentRecord(approvalPolicy)
|
|
1826
|
+
? '当前 approval policy: decision-points / silent-record。保留稳定 review artifact,但在版本、digest、work unit 精确匹配时不再额外停下来追问用户。'
|
|
1827
|
+
: '当前 approval policy: decision-points / human-review。稳定 review artifact 仍需要一次明确的人类决策。',
|
|
1828
|
+
currentRequirementStatusLine(gate, progress),
|
|
1829
|
+
reviewPolicyAllowsSilentRecord(approvalPolicy)
|
|
1830
|
+
? 'Decision-point order: clarify the requirement, capture user answers, synthesize the PRD, record the exact stable review artifact, generate the OpenPrd change, prepare the task breakdown, then implement within the confirmed scope.'
|
|
1831
|
+
: 'Decision-point order: clarify the requirement, capture user answers, 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.',
|
|
1832
|
+
'Recommended next action: 先在 chat 输出 Intake Decision;若为 L2,再运行 openprd clarify .,并在十句话左右回答 target、scope、out-of-scope、acceptance 后请求确认。Do not open clarification HTML; use review.html only after synthesize/review.',
|
|
1833
|
+
];
|
|
1834
|
+
if (isImplementationAdvanceIntent(intent)) {
|
|
1835
|
+
lines.splice(2, 1, gate?.active
|
|
1836
|
+
? '用户表达了继续实现的意图;但在 review 评审、change 和任务拆解走完之前,仍然阻断实现写入。'
|
|
1837
|
+
: '用户已确认当前需求可以进入执行范围;仍需保持实现范围与已确认的需求入口一致。');
|
|
1838
|
+
}
|
|
1839
|
+
lines.push(codexConfirmationReplyRule());
|
|
1840
|
+
return lines.filter(Boolean).join('\n');
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
function historicalRequirementReminder(root, runContext, intent, gate) {
|
|
1844
|
+
const activeChange = runContext?.activeChange;
|
|
1845
|
+
if (!activeChange || (!intent?.requiresIntake && !(intent?.confirmation && gate?.promptPreview))) {
|
|
1846
|
+
return null;
|
|
1847
|
+
}
|
|
1848
|
+
const summary = changeRequirementSummary(root, activeChange);
|
|
1849
|
+
const taskSummary = runContext.taskSummary;
|
|
1850
|
+
const status = taskSummary
|
|
1851
|
+
? '当前状态: OpenPrd 仍显示 ' + taskSummary.completed + '/' + taskSummary.total + ' 项完成,' + taskSummary.pending + ' 项待处理,' + taskSummary.blocked + ' 项阻塞。它可能是真的未完成、已经开发完但忘记更新状态,或只是项目状态未收口。'
|
|
1852
|
+
: '当前状态: OpenPrd 仍把它标记为 active。它可能是真的未完成、已经开发完但忘记更新状态,或只是项目状态未收口。';
|
|
1853
|
+
const pending = summary.pendingTasks.length > 0
|
|
1854
|
+
? '待判断的需求点: ' + summary.pendingTasks.join(';')
|
|
1855
|
+
: '';
|
|
1856
|
+
return [
|
|
1857
|
+
'OpenPrd 历史需求提醒',
|
|
1858
|
+
'检测到一个未收口的历史需求,但它和本轮新需求分开处理,本轮不会自动继续它。',
|
|
1859
|
+
'历史需求: ' + summary.title,
|
|
1860
|
+
summary.summary ? '需求说明: ' + summary.summary : '',
|
|
1861
|
+
status,
|
|
1862
|
+
pending,
|
|
1863
|
+
'可选处理: 继续当前新需求;查看这个历史需求还差什么;如果已完成则运行验证并收口;如果未完成再切回继续。',
|
|
1864
|
+
].filter(Boolean).join('\n');
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
function contextMessage(cwd, intent = null, gate = null, progress = null) {
|
|
1868
|
+
const run = progress?.runContext
|
|
1869
|
+
? { ok: true, parsed: progress.runContext, stdout: renderRunContextText(progress.runContext) }
|
|
1870
|
+
: runOpenPrdContext(cwd, intent?.promptText ?? null);
|
|
1871
|
+
const effectiveProgress = progress ?? evaluateRequirementGateProgress(cwd);
|
|
1872
|
+
const gateMessage = requirementGateMessage(intent, gate) || confirmationGateMessage(gate);
|
|
1873
|
+
if (run.ok) {
|
|
1874
|
+
const separateCurrentRequirement = Boolean(intent?.requiresIntake || ((intent?.confirmation || intent?.reviewDecision) && gate?.promptPreview));
|
|
1875
|
+
if (separateCurrentRequirement) {
|
|
1876
|
+
return [
|
|
1877
|
+
currentRequirementMessage(intent, gate, effectiveProgress),
|
|
1878
|
+
historicalRequirementReminder(cwd, run.parsed, intent, gate),
|
|
1879
|
+
'OpenPrd 上下文只是建议,不是自动执行指令。请先判断用户当前意图。',
|
|
1880
|
+
visualMockupMessage(intent),
|
|
1881
|
+
'需求复杂度由 $openprd-requirement-intake 按影响面、未知数、决策成本和验证成本判断:L0 直接处理并事后说明,L1 给对话内 mini-plan,L2 先走 PRD/review/change/tasks 并选择 base/consumer/b2b/agent PRD lens。只有在用户原始意图已明确要求实现,或后续明确发出执行指令时,才进入实现。',
|
|
1882
|
+
'如果用户只是要求看看、规划、分析、审查、解释影响或列出文件,请保持只读并基于证据回答;不要运行 OpenPrd loop、任务推进、discovery 推进、commit 或其他写入命令。',
|
|
1883
|
+
'只有当用户当前明确要求开发、实现、修复、继续任务、深度调研、对标复刻或提交时,才运行 openprd loop --run、openprd tasks --advance、openprd discovery --advance、commit/push 等执行命令。',
|
|
1884
|
+
'代码修改完成后、最终回复前,针对本轮实际 touched code files 运行 openprd dev-check . <file...>;attention 需说明局部职责,warning 需判断本轮是否扩大职责,扩大则先重构/拆分/解耦并复查,窄修暂不拆时说明原因和后续拆分建议。',
|
|
1885
|
+
'涉及界面、页面、视觉、样式或前端体验,且已经有效果图/设计稿/用户给图并进入实现阶段时,阶段性完成后必须截图并运行 openprd visual-compare . --reference <效果图> --actual <实现截图>;默认输出 JPG 到 .openprd/harness/visual-reviews/。查看合成图后继续对标,直到没有明显视觉差异。',
|
|
1886
|
+
'发现可沉淀项时不要中途打断任务:工具识别补全和减少重复打扰的高置信低风险项可自动补齐;用户偏好、项目协作规矩和 OpenPrd 默认行为先记录为候选,收工时运行 openprd grow . --review 集中确认。',
|
|
1887
|
+
'维护 OpenPrd 本身且涉及配置类能力时,先判断是否应纳入 openprd grow;高置信可成长默认纳入,不确定则主动询问用户。',
|
|
1888
|
+
'涉及后端、脚本、Agent、工具链、服务或数据处理变更时,把 CLI 与 API 视为同级接入面:同步检查命令入口、参数、输出契约、help/doctor/dry-run/status 与接口协议、返回结构、身份边界是否受影响,并更新 docs/basic/backend-structure.md 或明确写不适用原因。',
|
|
1889
|
+
'声明实现就绪前,先运行 openprd standards . --verify 和 openprd run . --verify。',
|
|
1890
|
+
].filter(Boolean).join('\n');
|
|
1891
|
+
}
|
|
1892
|
+
return [
|
|
1893
|
+
run.stdout,
|
|
1894
|
+
gateMessage,
|
|
1895
|
+
'OpenPrd 上下文只是建议,不是自动执行指令。请先判断用户当前意图。',
|
|
1896
|
+
visualMockupMessage(intent),
|
|
1897
|
+
'需求复杂度由 $openprd-requirement-intake 按影响面、未知数、决策成本和验证成本判断:L0 直接处理并事后说明,L1 给对话内 mini-plan,L2 先走 PRD/review/change/tasks 并选择 base/consumer/b2b/agent PRD lens。只有在用户原始意图已明确要求实现,或后续明确发出执行指令时,才进入实现。',
|
|
1898
|
+
'如果用户只是要求看看、规划、分析、审查、解释影响或列出文件,请保持只读并基于证据回答;不要运行 OpenPrd loop、任务推进、discovery 推进、commit 或其他写入命令。',
|
|
1899
|
+
'只有当用户当前明确要求开发、实现、修复、继续任务、深度调研、对标复刻或提交时,才运行 openprd loop --run、openprd tasks --advance、openprd discovery --advance、commit/push 等执行命令。',
|
|
1900
|
+
'代码修改完成后、最终回复前,针对本轮实际 touched code files 运行 openprd dev-check . <file...>;attention 需说明局部职责,warning 需判断本轮是否扩大职责,扩大则先重构/拆分/解耦并复查,窄修暂不拆时说明原因和后续拆分建议。',
|
|
1901
|
+
'涉及界面、页面、视觉、样式或前端体验,且已经有效果图/设计稿/用户给图并进入实现阶段时,阶段性完成后必须截图并运行 openprd visual-compare . --reference <效果图> --actual <实现截图>;默认输出 JPG 到 .openprd/harness/visual-reviews/。查看合成图后继续对标,直到没有明显视觉差异。',
|
|
1902
|
+
'发现可沉淀项时不要中途打断任务:工具识别补全和减少重复打扰的高置信低风险项可自动补齐;用户偏好、项目协作规矩和 OpenPrd 默认行为先记录为候选,收工时运行 openprd grow . --review 集中确认。',
|
|
1903
|
+
'维护 OpenPrd 本身且涉及配置类能力时,先判断是否应纳入 openprd grow;高置信可成长默认纳入,不确定则主动询问用户。',
|
|
1904
|
+
'涉及后端、脚本、Agent、工具链、服务或数据处理变更时,把 CLI 与 API 视为同级接入面:同步检查命令入口、参数、输出契约、help/doctor/dry-run/status 与接口协议、返回结构、身份边界是否受影响,并更新 docs/basic/backend-structure.md 或明确写不适用原因。',
|
|
1905
|
+
'声明实现就绪前,先运行 openprd standards . --verify 和 openprd run . --verify。',
|
|
1906
|
+
].filter(Boolean).join('\n');
|
|
1907
|
+
}
|
|
1908
|
+
const status = runOpenPrd(['status', '.'], cwd);
|
|
1909
|
+
const next = runOpenPrd(['next', '.'], cwd);
|
|
1910
|
+
if (!status.ok && !next.ok) {
|
|
1911
|
+
return '已安装 OpenPrd harness,但本轮无法读取工作区状态。声明就绪前请先运行 openprd doctor .。';
|
|
1912
|
+
}
|
|
1913
|
+
return [
|
|
1914
|
+
'OpenPrd harness 上下文:',
|
|
1915
|
+
status.ok ? status.stdout : '',
|
|
1916
|
+
next.ok ? next.stdout : '',
|
|
1917
|
+
gateMessage,
|
|
1918
|
+
visualMockupMessage(intent),
|
|
1919
|
+
'需求复杂度由 $openprd-requirement-intake 按影响面、未知数、决策成本和验证成本判断:L0 直接处理并事后说明,L1 给对话内 mini-plan,L2 先走 PRD/review/change/tasks 并选择 base/consumer/b2b/agent PRD lens。只有在用户原始意图已明确要求实现,或后续明确发出执行指令时,才进入实现。',
|
|
1920
|
+
'OpenPrd 下一步只是建议。规划、分析、审查类请求保持只读;只有用户当前明确要求开发、深度调研、对标复刻或继续任务时才执行。',
|
|
1921
|
+
'代码修改完成后、最终回复前,针对本轮实际 touched code files 运行 openprd dev-check . <file...>;attention 需说明局部职责,warning 需判断本轮是否扩大职责,扩大则先重构/拆分/解耦并复查,窄修暂不拆时说明原因和后续拆分建议。',
|
|
1922
|
+
'发现可沉淀项时不要中途打断任务:工具识别补全和减少重复打扰的高置信低风险项可自动补齐;用户偏好、项目协作规矩和 OpenPrd 默认行为先记录为候选,收工时运行 openprd grow . --review 集中确认。',
|
|
1923
|
+
'维护 OpenPrd 本身且涉及配置类能力时,先判断是否应纳入 openprd grow;高置信可成长默认纳入,不确定则主动询问用户。',
|
|
1924
|
+
'涉及后端、脚本、Agent、工具链、服务或数据处理变更时,把 CLI 与 API 视为同级接入面,并同步更新 docs/basic/backend-structure.md 或明确写不适用原因。',
|
|
1925
|
+
'声明就绪前请验证 docs/basic 标准。',
|
|
1926
|
+
].filter(Boolean).join('\n');
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
function shouldInjectOpenPrdContext(payload) {
|
|
1930
|
+
const prompt = promptText(payload);
|
|
1931
|
+
if (!prompt.trim()) {
|
|
1932
|
+
return false;
|
|
1933
|
+
}
|
|
1934
|
+
const intent = analyzePromptIntent(prompt);
|
|
1935
|
+
if (
|
|
1936
|
+
intent.simpleConcrete
|
|
1937
|
+
&& !intent.productCopyRequest
|
|
1938
|
+
&& !intent.secretsRequest
|
|
1939
|
+
&& !intent.weappValidationRequest
|
|
1940
|
+
&& !intent.browserSafetyRequest
|
|
1941
|
+
&& !intent.publicRepoResearchRequest
|
|
1942
|
+
&& !intent.externalTechResearchRequest
|
|
1943
|
+
&& !intent.skillWorkflowEditRequest
|
|
1944
|
+
) {
|
|
1945
|
+
return false;
|
|
1946
|
+
}
|
|
1947
|
+
if (intent.shouldInject) {
|
|
1948
|
+
return true;
|
|
1949
|
+
}
|
|
1950
|
+
const normalized = prompt.toLowerCase();
|
|
1951
|
+
const triggers = [
|
|
1952
|
+
/openprd/i,
|
|
1953
|
+
/opens*prd/i,
|
|
1954
|
+
/\bprd\b/i,
|
|
1955
|
+
/openprd\s+(run|loop|fleet|doctor|standards|change|discovery|handoff|freeze)/i,
|
|
1956
|
+
/\b(fleet|standards)\b/i,
|
|
1957
|
+
/深度调研/,
|
|
1958
|
+
/深度对标/,
|
|
1959
|
+
/持续调研/,
|
|
1960
|
+
/复刻/,
|
|
1961
|
+
/对标/,
|
|
1962
|
+
/文件说明书/,
|
|
1963
|
+
/文件夹说明书/,
|
|
1964
|
+
/基础文档/,
|
|
1965
|
+
/docs\/basic/i,
|
|
1966
|
+
/standards/i,
|
|
1967
|
+
/handoff/i,
|
|
1968
|
+
/freeze/i,
|
|
1969
|
+
/新增/,
|
|
1970
|
+
/增加/,
|
|
1971
|
+
/新建/,
|
|
1972
|
+
/我希望/,
|
|
1973
|
+
/用户反馈/,
|
|
1974
|
+
/需求/,
|
|
1975
|
+
/功能/,
|
|
1976
|
+
/模块/,
|
|
1977
|
+
/页面/,
|
|
1978
|
+
/入口/,
|
|
1979
|
+
/流程/,
|
|
1980
|
+
/编排/,
|
|
1981
|
+
/一站式/,
|
|
1982
|
+
/体验/,
|
|
1983
|
+
];
|
|
1984
|
+
return triggers.some((pattern) => pattern.test(normalized));
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
function classifyRisk(payload) {
|
|
1988
|
+
const text = payloadText(payload);
|
|
1989
|
+
const normalized = text.toLowerCase();
|
|
1990
|
+
const highPatterns = [
|
|
1991
|
+
/git\s+push/,
|
|
1992
|
+
/git\s+commit/,
|
|
1993
|
+
/npm\s+publish/,
|
|
1994
|
+
/pnpm\s+publish/,
|
|
1995
|
+
/yarn\s+npm\s+publish/,
|
|
1996
|
+
/gh\s+release/,
|
|
1997
|
+
/rm\s+-rf/,
|
|
1998
|
+
/openprd\s+freeze\b/,
|
|
1999
|
+
/openprd\s+handoff\b/,
|
|
2000
|
+
/openprd\s+change\s+.*--apply/,
|
|
2001
|
+
/openprd\s+change\s+.*--archive/,
|
|
2002
|
+
];
|
|
2003
|
+
const mediumPatterns = [
|
|
2004
|
+
/apply_patch/,
|
|
2005
|
+
/npm\s+install/,
|
|
2006
|
+
/npm\s+i\s/,
|
|
2007
|
+
/pnpm\s+add/,
|
|
2008
|
+
/yarn\s+add/,
|
|
2009
|
+
/bun\s+add/,
|
|
2010
|
+
/openprd\s+setup\b/,
|
|
2011
|
+
/openprd\s+update\b/,
|
|
2012
|
+
/openprd\s+standards\s+.*--init/,
|
|
2013
|
+
/openprd\s+change\s+.*--generate/,
|
|
2014
|
+
/openprd\s+review\s+.*--mark\s+(pending-confirmation|confirmed|needs-revision)/,
|
|
2015
|
+
/openprd\s+tasks\s+.*--advance/,
|
|
2016
|
+
/openprd\s+discovery\s+.*--advance/,
|
|
2017
|
+
/openprd\s+(capture|classify|synthesize|diagram)\b/,
|
|
2018
|
+
];
|
|
2019
|
+
if (highPatterns.some((pattern) => pattern.test(normalized))) {
|
|
2020
|
+
return { level: 'high', reason: 'release, history, freeze, handoff, destructive, or accepted-change action' };
|
|
2021
|
+
}
|
|
2022
|
+
if (mediumPatterns.some((pattern) => pattern.test(normalized))) {
|
|
2023
|
+
return { level: 'medium', reason: 'workspace mutation or dependency/configuration change' };
|
|
2024
|
+
}
|
|
2025
|
+
return { level: 'low', reason: 'read-only or local exploratory action' };
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
function extractChangeId(text) {
|
|
2029
|
+
const match = String(text || '').match(/--change\s+([a-zA-Z0-9._-]+)/);
|
|
2030
|
+
return match ? match[1] : null;
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
function runGateChecks(cwd, payload, risk) {
|
|
2034
|
+
const checks = [];
|
|
2035
|
+
const run = runOpenPrd(['run', '.', '--verify', '--json'], cwd);
|
|
2036
|
+
const runParsed = parseJsonOutput(run.stdout);
|
|
2037
|
+
const runTaskReady = runParsed?.readiness?.taskReady ?? run.ok;
|
|
2038
|
+
const runWorkspaceReady = runParsed?.readiness?.workspaceReady ?? run.ok;
|
|
2039
|
+
checks.push({
|
|
2040
|
+
name: 'run-verify',
|
|
2041
|
+
ok: runWorkspaceReady,
|
|
2042
|
+
taskReady: runTaskReady,
|
|
2043
|
+
workspaceReady: runWorkspaceReady,
|
|
2044
|
+
summary: summarizeRunVerifyCheck(runParsed, run.stdout || run.stderr),
|
|
2045
|
+
warnings: Array.isArray(runParsed?.warnings) ? runParsed.warnings : [],
|
|
2046
|
+
errors: Array.isArray(runParsed?.errors) ? runParsed.errors : [],
|
|
2047
|
+
details: [
|
|
2048
|
+
...(Array.isArray(runParsed?.errors) ? runParsed.errors : []),
|
|
2049
|
+
...(Array.isArray(runParsed?.warnings) ? runParsed.warnings : []),
|
|
2050
|
+
],
|
|
2051
|
+
output: run.stdout || run.stderr,
|
|
2052
|
+
});
|
|
2053
|
+
const text = payloadText(payload);
|
|
2054
|
+
const changeId = extractChangeId(text);
|
|
2055
|
+
if (changeId && /openprd\s+change\s+.*--(apply|archive|validate)/i.test(text)) {
|
|
2056
|
+
const change = runOpenPrd(['change', '.', '--validate', '--change', changeId], cwd);
|
|
2057
|
+
checks.push({
|
|
2058
|
+
name: 'change-validate',
|
|
2059
|
+
ok: change.ok,
|
|
2060
|
+
summary: `change-validate: ${change.ok ? 'ok' : 'failed'}`,
|
|
2061
|
+
details: [change.stdout || change.stderr].filter(Boolean),
|
|
2062
|
+
output: change.stdout || change.stderr,
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
if (risk.level === 'high' && shouldRunDoctorForHighRisk(payload)) {
|
|
2066
|
+
const doctor = runOpenPrd(['doctor', '.', '--tools', 'codex', '--json'], cwd);
|
|
2067
|
+
const doctorParsed = parseJsonOutput(doctor.stdout);
|
|
2068
|
+
checks.push({
|
|
2069
|
+
name: 'doctor',
|
|
2070
|
+
ok: doctor.ok,
|
|
2071
|
+
agentIntegrationOk: doctorParsed?.agentIntegration?.ok ?? null,
|
|
2072
|
+
standardsOk: doctorParsed?.standards?.ok ?? null,
|
|
2073
|
+
validationOk: doctorParsed?.validation?.valid ?? null,
|
|
2074
|
+
summary: summarizeDoctorCheck(doctorParsed, doctor.stdout || doctor.stderr),
|
|
2075
|
+
details: [doctor.stderr || doctor.stdout].filter(Boolean),
|
|
2076
|
+
output: doctor.stdout || doctor.stderr,
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
2079
|
+
const envelope = buildGateFailureEnvelope({ checks });
|
|
2080
|
+
return {
|
|
2081
|
+
ok: checks.every((check) => check.ok),
|
|
2082
|
+
checks,
|
|
2083
|
+
envelope,
|
|
2084
|
+
summary: checks.map((check) => check.summary || `${check.name}: ${check.ok ? 'ok' : 'failed'}`).join(', '),
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
function hookSuppressed(root) {
|
|
2089
|
+
const state = readJsonSync(path.join(harnessDir(root), 'hook-state.json'), {});
|
|
2090
|
+
const lockPath = path.join(harnessDir(root), 'input-lock.json');
|
|
2091
|
+
const lock = readJsonSync(lockPath, null);
|
|
2092
|
+
return Boolean(state?.suppressions?.inputLock || (lock && lock.active));
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
function allowHook(additionalContext = null, outputEventName = eventName) {
|
|
2096
|
+
const result = { continue: true };
|
|
2097
|
+
if (additionalContext) {
|
|
2098
|
+
result.hookSpecificOutput = {
|
|
2099
|
+
hookEventName: outputEventName,
|
|
2100
|
+
additionalContext,
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
2103
|
+
return result;
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
function blockHook(reason) {
|
|
2107
|
+
return {
|
|
2108
|
+
decision: 'block',
|
|
2109
|
+
reason,
|
|
2110
|
+
systemMessage: reason,
|
|
2111
|
+
};
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
function handle(eventName, cwd, payload) {
|
|
2115
|
+
const root = findProjectRoot(cwd);
|
|
2116
|
+
ensureHarness(root);
|
|
2117
|
+
const risk = classifyRisk(payload);
|
|
2118
|
+
const fingerprint = fingerprintFor(eventName, payload, risk);
|
|
2119
|
+
const duplicate = isDuplicate(root, fingerprint);
|
|
2120
|
+
const sessionId = sessionIdFor(payload);
|
|
2121
|
+
const baseEvent = {
|
|
2122
|
+
eventName,
|
|
2123
|
+
risk,
|
|
2124
|
+
fingerprint,
|
|
2125
|
+
duplicate,
|
|
2126
|
+
sessionId,
|
|
2127
|
+
preview: preview(payloadText(payload)),
|
|
2128
|
+
};
|
|
2129
|
+
|
|
2130
|
+
if (eventName === 'SessionStart') {
|
|
2131
|
+
return allowHook();
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
if (eventName === 'UserPromptSubmit') {
|
|
2135
|
+
if (duplicate) {
|
|
2136
|
+
return allowHook();
|
|
2137
|
+
}
|
|
2138
|
+
const prompt = promptText(payload);
|
|
2139
|
+
beginTurnReview(root, baseEvent, prompt);
|
|
2140
|
+
let intent = analyzePromptIntent(prompt);
|
|
2141
|
+
let gate = readRequirementGate(root, sessionId);
|
|
2142
|
+
let skillGate = readNamedGate(root, 'skill-visualization', sessionId);
|
|
2143
|
+
let researchGate = readNamedGate(root, 'research', sessionId);
|
|
2144
|
+
let weappGate = readNamedGate(root, 'weapp', sessionId);
|
|
2145
|
+
const shortAffirmative = isShortAffirmativeConfirmation(prompt);
|
|
2146
|
+
let progress = isBlockingRequirementGate(gate) ? evaluateRequirementGateProgress(root, sessionId) : null;
|
|
2147
|
+
if (isBlockingRequirementGate(gate) && shortAffirmative) {
|
|
2148
|
+
intent = {
|
|
2149
|
+
...intent,
|
|
2150
|
+
shouldInject: true,
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
if (!intent.confirmation && isBlockingRequirementGate(gate) && shortAffirmative && progress?.nextStep === 'implementation-ready') {
|
|
2154
|
+
intent = {
|
|
2155
|
+
...intent,
|
|
2156
|
+
confirmation: true,
|
|
2157
|
+
implementationConfirmation: true,
|
|
2158
|
+
shouldInject: true,
|
|
2159
|
+
};
|
|
2160
|
+
}
|
|
2161
|
+
if (skillGate?.active && (shortAffirmative || intent.confirmation)) {
|
|
2162
|
+
skillGate = confirmSkillVisualizationGate(root, prompt, sessionId);
|
|
2163
|
+
} else if (intent.skillWorkflowEditRequest) {
|
|
2164
|
+
skillGate = openSkillVisualizationGate(root, prompt, sessionId);
|
|
2165
|
+
} else if (skillGate?.active) {
|
|
2166
|
+
skillGate = closeSkillVisualizationGate(root, sessionId, { status: 'superseded-by-new-prompt' });
|
|
2167
|
+
}
|
|
2168
|
+
if (intent.publicRepoResearchRequest || intent.externalTechResearchRequest) {
|
|
2169
|
+
researchGate = openResearchGate(root, prompt, intent, sessionId);
|
|
2170
|
+
} else if (researchGate?.active) {
|
|
2171
|
+
researchGate = closeResearchGate(root, sessionId, { status: 'superseded-by-new-prompt' });
|
|
2172
|
+
}
|
|
2173
|
+
if (intent.weappValidationRequest) {
|
|
2174
|
+
weappGate = openWeappGate(root, prompt, sessionId);
|
|
2175
|
+
} else if (weappGate?.active) {
|
|
2176
|
+
weappGate = closeWeappGate(root, sessionId, { status: 'superseded-by-new-prompt' });
|
|
2177
|
+
}
|
|
2178
|
+
if (canAutoAuthorizeRequirementExecution(gate, progress)) {
|
|
2179
|
+
gate = authorizeRequirementGateExecution(root, prompt, sessionId, {
|
|
2180
|
+
reason: 'original-execution-intent-after-reviewed-task-ready',
|
|
2181
|
+
});
|
|
2182
|
+
progress = null;
|
|
2183
|
+
}
|
|
2184
|
+
if (isBlockingRequirementGate(gate)) {
|
|
2185
|
+
if (intent.reviewDecision) {
|
|
2186
|
+
const authorization = reviewActionAuthorizationFor(intent, progress, prompt);
|
|
2187
|
+
gate = holdRequirementGate(root, prompt, progress, sessionId, {
|
|
2188
|
+
status: authorization ? 'review-confirmation-authorized' : progress.nextStep,
|
|
2189
|
+
reviewActionAuthorization: authorization,
|
|
2190
|
+
});
|
|
2191
|
+
const outcome = authorization ? 'requirement-gate-review-authorized' : 'requirement-gate-awaiting-review';
|
|
2192
|
+
appendEvent(root, {
|
|
2193
|
+
...baseEvent,
|
|
2194
|
+
outcome,
|
|
2195
|
+
reviewDecision: intent.reviewDecision,
|
|
2196
|
+
reviewVersionId: progress?.review?.versionId ?? null,
|
|
2197
|
+
});
|
|
2198
|
+
recordRunHook(root, baseEvent, outcome);
|
|
2199
|
+
updateHookState(root, baseEvent);
|
|
2200
|
+
return allowHook(composeHookContext(root, intent, gate, progress, sessionId));
|
|
2201
|
+
}
|
|
2202
|
+
if (isImplementationAdvanceIntent(intent)) {
|
|
2203
|
+
if (progress?.nextStep !== 'implementation-ready') {
|
|
2204
|
+
gate = holdRequirementGate(root, prompt, progress, sessionId);
|
|
2205
|
+
appendEvent(root, { ...baseEvent, outcome: 'requirement-gate-held', progress });
|
|
2206
|
+
recordRunHook(root, baseEvent, 'requirement-gate-held');
|
|
2207
|
+
updateHookState(root, baseEvent);
|
|
2208
|
+
return allowHook(composeHookContext(root, intent, gate, progress, sessionId));
|
|
2209
|
+
}
|
|
2210
|
+
gate = confirmRequirementGate(root, prompt, sessionId);
|
|
2211
|
+
appendEvent(root, { ...baseEvent, outcome: 'requirement-gate-confirmed', progress });
|
|
2212
|
+
recordRunHook(root, baseEvent, 'requirement-gate-confirmed');
|
|
2213
|
+
updateHookState(root, baseEvent);
|
|
2214
|
+
return allowHook(composeHookContext(root, intent, gate, progress, sessionId));
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
if (intent.requiresIntake) {
|
|
2218
|
+
gate = openRequirementGate(root, prompt, intent, sessionId);
|
|
2219
|
+
const result = allowHook(composeHookContext(root, intent, gate, evaluateRequirementGateProgress(root, sessionId), sessionId));
|
|
2220
|
+
appendEvent(root, { ...baseEvent, outcome: 'requirement-gate-opened' });
|
|
2221
|
+
recordRunHook(root, baseEvent, 'requirement-gate-opened');
|
|
2222
|
+
updateHookState(root, baseEvent);
|
|
2223
|
+
return result;
|
|
2224
|
+
}
|
|
2225
|
+
if (!shouldInjectOpenPrdContext(payload)) {
|
|
2226
|
+
return allowHook();
|
|
2227
|
+
}
|
|
2228
|
+
const result = allowHook(composeHookContext(root, intent, gate, progress, sessionId));
|
|
2229
|
+
appendEvent(root, { ...baseEvent, outcome: 'context-injected' });
|
|
2230
|
+
recordRunHook(root, baseEvent, 'context-injected');
|
|
2231
|
+
updateHookState(root, baseEvent);
|
|
2232
|
+
return result;
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
if (eventName === 'PreToolUse') {
|
|
2236
|
+
const turnIntent = analyzePromptIntent(readTurnState(root).prompt || '');
|
|
2237
|
+
let gate = readRequirementGate(root, sessionId);
|
|
2238
|
+
const skillGate = readNamedGate(root, 'skill-visualization', sessionId);
|
|
2239
|
+
const researchGate = applyResearchToolSignal(root, payload, sessionId);
|
|
2240
|
+
const weappGate = applyWeappToolSignal(root, payload, sessionId);
|
|
2241
|
+
let progress = isBlockingRequirementGate(gate) ? evaluateRequirementGateProgress(root, sessionId) : null;
|
|
2242
|
+
if (canAutoAuthorizeRequirementExecution(gate, progress)) {
|
|
2243
|
+
gate = authorizeRequirementGateExecution(root, readTurnState(root).prompt || '', sessionId, {
|
|
2244
|
+
reason: 'original-execution-intent-after-reviewed-task-ready',
|
|
2245
|
+
});
|
|
2246
|
+
progress = null;
|
|
2247
|
+
}
|
|
2248
|
+
if (sessionId && isAllowedDuringRequirementGate(root, payload, gate, sessionId)) {
|
|
2249
|
+
mirrorRequirementGate(root, sessionId);
|
|
2250
|
+
}
|
|
2251
|
+
if (skillGate?.active && looksLikeSkillContractMutation(root, payload)) {
|
|
2252
|
+
appendEvent(root, { ...baseEvent, outcome: 'blocked-skill-visualization-gate' });
|
|
2253
|
+
recordRunHook(root, baseEvent, 'blocked-skill-visualization-gate');
|
|
2254
|
+
updateHookState(root, baseEvent);
|
|
2255
|
+
return blockHook([
|
|
2256
|
+
'OpenPrd blocked a skill/AGENTS mutation because the visualization-confirmation gate is still active.',
|
|
2257
|
+
'Before editing SKILL.md, AGENTS.md, or related skill workflow files, first output a color-coded Mermaid plan, summarize added/changed/unchanged/removed items, then wait for explicit user confirmation.',
|
|
2258
|
+
].join('\n'));
|
|
2259
|
+
}
|
|
2260
|
+
if (researchGate?.active && isMutationPayload(payload, risk)) {
|
|
2261
|
+
appendEvent(root, { ...baseEvent, outcome: 'blocked-research-gate' });
|
|
2262
|
+
recordRunHook(root, baseEvent, 'blocked-research-gate');
|
|
2263
|
+
updateHookState(root, baseEvent);
|
|
2264
|
+
return blockHook([
|
|
2265
|
+
'OpenPrd blocked a mutating action because the external-evidence gate is still active.',
|
|
2266
|
+
researchGateMessage(researchGate),
|
|
2267
|
+
].filter(Boolean).join('\n'));
|
|
2268
|
+
}
|
|
2269
|
+
if (turnIntent.secretsRequest && isRawVaultReadAttempt(payload)) {
|
|
2270
|
+
appendEvent(root, { ...baseEvent, outcome: 'blocked-raw-vault-read' });
|
|
2271
|
+
recordRunHook(root, baseEvent, 'blocked-raw-vault-read');
|
|
2272
|
+
updateHookState(root, baseEvent);
|
|
2273
|
+
return blockHook([
|
|
2274
|
+
'OpenPrd blocked a raw vault read attempt.',
|
|
2275
|
+
'Use the `secrets-vault` skill first and only read the minimum required fields; do not read the original vault file directly.',
|
|
2276
|
+
].join('\n'));
|
|
2277
|
+
}
|
|
2278
|
+
if (isBlockingRequirementGate(gate) && isMutationPayload(payload, risk) && !isAllowedDuringRequirementGate(root, payload, gate, sessionId)) {
|
|
2279
|
+
const reviewMark = parseReviewMarkCommand(commandText(payload));
|
|
2280
|
+
const approvalPolicy = requirementApprovalPolicy(gate);
|
|
2281
|
+
const silentRecord = reviewPolicyAllowsSilentRecord(approvalPolicy);
|
|
2282
|
+
const reason = reviewMark
|
|
2283
|
+
? [
|
|
2284
|
+
silentRecord
|
|
2285
|
+
? 'OpenPrd blocked review status writing because this command does not match the exact stable PRD review artifact allowed by the current approval policy.'
|
|
2286
|
+
: 'OpenPrd blocked review status writing because the current user message did not explicitly confirm this exact PRD review artifact.',
|
|
2287
|
+
silentRecord
|
|
2288
|
+
? '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.'
|
|
2289
|
+
: 'Only record PRD review confirmation from the copied review command or an explicit review approval tied to the current version, digest, and work unit.',
|
|
2290
|
+
progress?.review?.versionId
|
|
2291
|
+
? `Current review artifact: version ${progress.review.versionId}, digest ${progress.review.digest}, work unit ${progress.review.workUnitId}.`
|
|
2292
|
+
: 'Current review artifact has not been synthesized yet. Run openprd synthesize . --open first.',
|
|
2293
|
+
silentRecord
|
|
2294
|
+
? 'Do not mark any stale or different review artifact; only the exact current artifact is allowed.'
|
|
2295
|
+
: 'Implementation approval and review confirmation are different gates; do not treat "可以开做" or similar wording as permission to run openprd review --mark confirmed.',
|
|
2296
|
+
].filter(Boolean).join('\n')
|
|
2297
|
+
: [
|
|
2298
|
+
'OpenPrd blocked a mutating action because the requirement gate is still active.',
|
|
2299
|
+
progress?.reason || 'The requirement still needs PRD review, change generation, or task preparation.',
|
|
2300
|
+
progress?.nextStep === 'implementation-ready'
|
|
2301
|
+
? 'Do not edit implementation files until the user clearly asks to execute this reviewed requirement.'
|
|
2302
|
+
: progress?.nextStep === 'review-recording-required'
|
|
2303
|
+
? 'Do not edit implementation files yet. First record the exact current stable review artifact, then generate change and tasks.'
|
|
2304
|
+
: 'Do not edit implementation files until the active approval policy is satisfied and the OpenPrd change has generated tasks.',
|
|
2305
|
+
silentRecord
|
|
2306
|
+
? 'Decision-point order: clarify the requirement, capture user answers, synthesize the PRD, record the exact stable review artifact, generate the OpenPrd change, prepare the task breakdown, then implement within the confirmed scope.'
|
|
2307
|
+
: 'Decision-point order: clarify the requirement, capture user answers, 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.',
|
|
2308
|
+
].join('\n');
|
|
2309
|
+
appendEvent(root, { ...baseEvent, outcome: 'blocked-requirement-intake' });
|
|
2310
|
+
recordRunHook(root, baseEvent, 'blocked-requirement-intake');
|
|
2311
|
+
updateHookState(root, baseEvent);
|
|
2312
|
+
return blockHook(reason);
|
|
2313
|
+
}
|
|
2314
|
+
if (turnIntent.browserSafetyRequest && isHighRiskBrowserAction(payload)) {
|
|
2315
|
+
appendEvent(root, { ...baseEvent, outcome: 'browser-safety-reminder' });
|
|
2316
|
+
recordRunHook(root, baseEvent, 'browser-safety-reminder');
|
|
2317
|
+
updateHookState(root, baseEvent);
|
|
2318
|
+
return allowHook(browserSafetyMessage(turnIntent));
|
|
2319
|
+
}
|
|
2320
|
+
if (risk.level === 'high') {
|
|
2321
|
+
const gates = runGateChecks(root, payload, risk);
|
|
2322
|
+
appendEvent(root, { ...baseEvent, gates, outcome: gates.ok ? 'allowed-high-risk' : 'blocked-high-risk' });
|
|
2323
|
+
recordRunHook(root, baseEvent, gates.ok ? 'allowed-high-risk' : 'blocked-high-risk');
|
|
2324
|
+
updateHookState(root, baseEvent);
|
|
2325
|
+
if (!gates.ok) {
|
|
2326
|
+
return blockHook(formatHighRiskGateBlock(gates));
|
|
2327
|
+
}
|
|
2328
|
+
recordTouchedFiles(root, payload);
|
|
2329
|
+
return allowHook(`OpenPrd high-risk gate passed: ${gates.summary}.`);
|
|
2330
|
+
}
|
|
2331
|
+
if (risk.level === 'medium') {
|
|
2332
|
+
appendEvent(root, { ...baseEvent, outcome: 'allowed-medium-risk' });
|
|
2333
|
+
recordRunHook(root, baseEvent, 'allowed-medium-risk');
|
|
2334
|
+
updateHookState(root, baseEvent);
|
|
2335
|
+
recordTouchedFiles(root, payload);
|
|
2336
|
+
return allowHook('OpenPrd 检测到写入动作。本轮写入完成后、最终回复前,请针对实际 touched code files 运行 openprd dev-check . <file...>;如出现 warning,判断本轮是否扩大职责,扩大则先重构/拆分/解耦并复查,窄修暂不拆时说明原因和后续拆分建议;如涉及界面视觉且已有参考效果图并进入实现阶段,阶段性完成后运行 openprd visual-compare . --reference <效果图> --actual <实现截图> 并查看 JPG 对比图;发现可沉淀项时不要中途打断任务,工具识别补全和减少重复打扰的高置信低风险项可自动补齐,用户偏好、项目协作规矩和 OpenPrd 默认行为留到收工时用 openprd grow . --review 集中确认;维护 OpenPrd 本身且涉及配置类能力时,先判断是否应纳入 openprd grow;声明就绪前,请同步维护 docs/basic、文件说明书、文件夹 README,以及相关 OpenPrd change/task 状态;如果涉及后端、脚本、Agent、工具链、服务或数据处理变更,还要把 CLI 与 API 视为同级接入面并更新 docs/basic/backend-structure.md。');
|
|
2337
|
+
}
|
|
2338
|
+
return allowHook();
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
if (eventName === 'PostToolUse') {
|
|
2342
|
+
const text = payloadText(payload);
|
|
2343
|
+
const failed = /command not found|no such file|permission denied|failed|error|exception/i.test(text);
|
|
2344
|
+
if (!failed) {
|
|
2345
|
+
return allowHook();
|
|
2346
|
+
}
|
|
2347
|
+
appendEvent(root, { ...baseEvent, outcome: failed ? 'tool-failure-detected' : 'tool-complete' });
|
|
2348
|
+
recordRunHook(root, baseEvent, failed ? 'tool-failure-detected' : 'tool-complete');
|
|
2349
|
+
updateHookState(root, baseEvent);
|
|
2350
|
+
if (failed && !duplicate) {
|
|
2351
|
+
return allowHook(toolFailureMessage(classifyToolFailure(text)));
|
|
2352
|
+
}
|
|
2353
|
+
return allowHook();
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
if (eventName === 'Stop') {
|
|
2357
|
+
appendEvent(root, { ...baseEvent, outcome: 'stop-check' });
|
|
2358
|
+
recordRunHook(root, baseEvent, 'stop-check');
|
|
2359
|
+
updateHookState(root, baseEvent);
|
|
2360
|
+
if (hookSuppressed(root)) {
|
|
2361
|
+
return allowHook();
|
|
2362
|
+
}
|
|
2363
|
+
const turnState = readTurnState(root);
|
|
2364
|
+
const stopIntent = analyzePromptIntent(turnState.prompt || '');
|
|
2365
|
+
const weappGate = readNamedGate(root, 'weapp', sessionId);
|
|
2366
|
+
if (weappGate?.active && (stopIntent.weappValidationRequest || (Array.isArray(turnState.touchedFiles) && turnState.touchedFiles.length > 0))) {
|
|
2367
|
+
return allowHook([
|
|
2368
|
+
'OpenPrd 在本轮收工回顾里发现微信小程序验证仍未完成。',
|
|
2369
|
+
'如果这次任务涉及微信小程序测试、验证、截图、日志、网络请求、微信开发者工具自动化,或修改了可能影响运行态的代码,请先用 `weapp-dev-mcp` skill 和本地 `weapp-dev-mcp` MCP 做实际验证。',
|
|
2370
|
+
'在没有本地 MCP 验证证据前,不要宣称“小程序已验证”。',
|
|
2371
|
+
].join('\n'));
|
|
2372
|
+
}
|
|
2373
|
+
if (Array.isArray(turnState.touchedFiles) && turnState.touchedFiles.length > 0) {
|
|
2374
|
+
const review = runOpenPrd(['quality', '.', '--learn', '--review', '--from', '.openprd/harness/turn-state.json', '--json'], root);
|
|
2375
|
+
if (review.ok) {
|
|
2376
|
+
try {
|
|
2377
|
+
const parsedReview = JSON.parse(review.stdout);
|
|
2378
|
+
if (
|
|
2379
|
+
parsedReview
|
|
2380
|
+
&& !parsedReview.skipped
|
|
2381
|
+
&& parsedReview.ok !== false
|
|
2382
|
+
&& parsedReview.candidateId
|
|
2383
|
+
&& parsedReview.candidateId !== turnState.lastKnowledgePromptCandidateId
|
|
2384
|
+
) {
|
|
2385
|
+
writeTurnState(root, {
|
|
2386
|
+
...turnState,
|
|
2387
|
+
lastKnowledgePromptCandidateId: parsedReview.candidateId,
|
|
2388
|
+
lastKnowledgePromptAt: now(),
|
|
2389
|
+
});
|
|
2390
|
+
return allowHook([
|
|
2391
|
+
'OpenPrd 在本轮 Stop 回顾里发现了可沉淀的项目经验草案。',
|
|
2392
|
+
`Draft Skill: ${parsedReview.files?.draftSkill ?? 'unknown'}`,
|
|
2393
|
+
`候选目录: ${parsedReview.files?.candidateDir ?? 'unknown'}`,
|
|
2394
|
+
parsedReview.suggestedLearnCommand
|
|
2395
|
+
? `后续 promote: ${parsedReview.suggestedLearnCommand}`
|
|
2396
|
+
: null,
|
|
2397
|
+
'在最终回复里说明这次修复是否值得沉淀,以及草案已写到哪里。',
|
|
2398
|
+
].filter(Boolean).join('\n'));
|
|
2399
|
+
}
|
|
2400
|
+
} catch {}
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
const run = runOpenPrd(['run', '.', '--context', '--json'], root);
|
|
2404
|
+
if (run.ok) {
|
|
2405
|
+
try {
|
|
2406
|
+
const parsed = JSON.parse(run.stdout);
|
|
2407
|
+
const command = parsed?.recommendation?.command || '';
|
|
2408
|
+
if (command && !/openprd\s+next\s+\./.test(command)) {
|
|
2409
|
+
return {
|
|
2410
|
+
continue: true,
|
|
2411
|
+
systemMessage: `OpenPrd still has a hook-driven next action:\n${parsed.recommendation.title}\nSuggested command: ${command}`,
|
|
2412
|
+
};
|
|
2413
|
+
}
|
|
2414
|
+
} catch {}
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
appendEvent(root, { ...baseEvent, outcome: 'noop' });
|
|
2419
|
+
recordRunHook(root, baseEvent, 'noop');
|
|
2420
|
+
updateHookState(root, baseEvent);
|
|
2421
|
+
return allowHook();
|
|
2422
|
+
}
|