@openprd/cli 0.1.8 → 0.1.10
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/benchmarks/index.md +15 -1
- package/.openprd/config.yaml +10 -0
- package/.openprd/design/README.md +47 -0
- package/.openprd/design/README_EN.md +25 -0
- package/.openprd/design/active/asset-spec.md +15 -0
- package/.openprd/design/active/direction-plan.md +7 -0
- package/.openprd/design/active/facts-sheet.md +13 -0
- package/.openprd/design/active/image-preflight.md +9 -0
- package/.openprd/design/active/selected-direction.md +11 -0
- package/.openprd/design/anti-slop.md +16 -0
- package/.openprd/design/assets/dark-mesh.svg +14 -0
- package/.openprd/design/assets/paper-grid.svg +14 -0
- package/.openprd/design/assets/surface-presets.md +21 -0
- package/.openprd/design/assets/workbench-grid.svg +16 -0
- package/.openprd/design/checklists/ui-quality-gate.md +22 -0
- package/.openprd/design/components/component-catalog.md +42 -0
- package/.openprd/design/layouts/layout-catalog.json +45 -0
- package/.openprd/design/lenses/frontend-lenses.md +31 -0
- package/.openprd/design/recipes/content-experience.md +26 -0
- package/.openprd/design/recipes/operational-tool.md +25 -0
- package/.openprd/design/recipes/product-launch.md +26 -0
- package/.openprd/design/templates/README.md +36 -0
- package/.openprd/design/templates/README_EN.md +31 -0
- package/.openprd/design/templates/content-home.html +415 -0
- package/.openprd/design/templates/ops-dashboard.html +340 -0
- package/.openprd/design/templates/product-launch.html +360 -0
- package/.openprd/design/themes/theme-catalog.json +77 -0
- package/.openprd/engagements/active/prd.md +111 -100
- package/.openprd/schema/prd.schema.yaml +10 -0
- package/.openprd/templates/base/intake.md +4 -0
- package/.openprd/templates/base/prd.md +16 -6
- package/AGENTS.md +10 -7
- package/README.md +28 -10
- package/README_EN.md +23 -3
- package/package.json +41 -2
- package/scripts/openprd-codex-isolated-worker.mjs +799 -0
- package/skills/openprd-benchmark-router/SKILL.md +4 -0
- package/skills/openprd-frontend-design/SKILL.md +161 -0
- package/skills/openprd-frontend-design/references/design-asset-contract.md +34 -0
- package/skills/openprd-frontend-design/references/direction-engine.md +46 -0
- package/skills/openprd-harness/SKILL.md +38 -21
- package/skills/openprd-learning-review/SKILL.md +2 -1
- package/skills/openprd-learning-review/references/style-packs/xianxia-cultivation.prompt.md +1 -1
- package/skills/openprd-quality/SKILL.md +6 -4
- package/skills/openprd-requirement-intake/SKILL.md +27 -5
- package/skills/openprd-requirement-intake/references/prd-template-lenses.md +6 -3
- package/skills/openprd-requirement-intake/references/routing-rubric.md +9 -5
- package/skills/openprd-requirement-intake/references/startup-validation-lens.md +23 -0
- package/skills/openprd-router/SKILL.md +13 -5
- package/skills/openprd-shared/SKILL.md +38 -25
- package/src/agent-integration.js +126 -75
- package/src/brainstorm-artifacts.js +1657 -0
- package/src/brainstorm-presentation.js +223 -0
- package/src/brainstorm.js +603 -0
- package/src/cli/args.js +51 -3
- package/src/cli/doctor-print.js +34 -8
- package/src/cli/print.js +6 -0
- package/src/cli/quality-print.js +70 -0
- package/src/cli/run-print.js +43 -1
- package/src/cli/shared-print.js +33 -2
- package/src/cli/workflow-print.js +21 -0
- package/src/codex-hook-runner-template.mjs +858 -65
- package/src/design-starter-support.js +462 -0
- package/src/design-starter.js +1207 -0
- package/src/knowledge.js +377 -21
- package/src/learning-review.js +3 -3
- package/src/loop.js +839 -101
- package/src/openprd.js +86 -1
- package/src/openspec/change-validate.js +1 -0
- package/src/prd-core.js +18 -0
- package/src/quality-learning.js +50 -6
- package/src/quality.js +2 -2
- package/src/run-harness.js +191 -29
- package/src/self-update.js +301 -4
- package/src/standards.js +46 -5
- package/src/visual-prepare.js +940 -0
- package/src/workspace-core.js +137 -0
- package/src/workspace-workflow.js +190 -7
- package/.openprd/benchmarks/evidence/milvus-io-ai-code-review-gets-better-when-models-debate-claude-vs-gemini-vs-code.md +0 -14
- package/.openprd/benchmarks/evidence/nolanlawson-com-using-ai-to-write-better-code-more-slowly.md +0 -14
- package/.openprd/discovery/config.json +0 -35
- package/.openprd/engagements/active/flows.md +0 -35
- package/.openprd/engagements/active/handoff.md +0 -16
- package/.openprd/engagements/active/review.html +0 -61
- package/.openprd/engagements/active/roles.md +0 -22
- package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +0 -23
- package/.openprd/engagements/work-units/wu-20260602113956-a99b5b88.json +0 -18
- package/.openprd/engagements/work-units/wu-20260602122244-78656aaf.json +0 -18
- package/.openprd/engagements/work-units/wu-20260602122442-e96489e2.json +0 -18
- package/.openprd/engagements/work-units/wu-20260602132835-695429e8.json +0 -18
- package/.openprd/exports/.gitkeep +0 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/candidate.json +0 -78
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/diagnostic-report.json +0 -129
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/root-cause-candidates.json +0 -41
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/timeline.json +0 -14
- package/.openprd/knowledge/drafts/openprd-experience-diagnostic-candidate-turn-1780116203372-5f266a79e968c758/SKILL.md +0 -49
- package/.openprd/knowledge/index.json +0 -47
- package/.openprd/reviews/v0001.html +0 -1322
- package/.openprd/reviews/v0002.html +0 -1150
- package/.openprd/reviews/v0003.html +0 -1150
- package/.openprd/reviews/v0004.html +0 -1150
- package/.openprd/reviews/v0005.html +0 -1150
- package/.openprd/sessions/.gitkeep +0 -0
- package/.openprd/state/changes.json +0 -27
- package/.openprd/state/current.json +0 -505
- package/.openprd/state/release-ledger.json +0 -344
- package/.openprd/state/version-index.json +0 -67
- package/.openprd/state/versions/.gitkeep +0 -0
- package/.openprd/state/versions/v0001.json +0 -121
- package/.openprd/state/versions/v0001.md +0 -161
- package/.openprd/state/versions/v0002.json +0 -264
- package/.openprd/state/versions/v0002.md +0 -183
- package/.openprd/state/versions/v0003.json +0 -269
- package/.openprd/state/versions/v0003.md +0 -188
- package/.openprd/state/versions/v0004.json +0 -274
- package/.openprd/state/versions/v0004.md +0 -193
- package/.openprd/state/versions/v0005.json +0 -299
- package/.openprd/state/versions/v0005.md +0 -189
- 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-requirement-routing-en.png +0 -0
- package/docs/assets/openprd-requirement-routing-en.svg +0 -102
- package/docs/assets/openprd-requirement-routing-zh-refined.png +0 -0
- package/docs/assets/openprd-requirement-routing-zh.png +0 -0
- package/docs/assets/openprd-requirement-routing-zh.svg +0 -102
- 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 +0 -114
- 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
|
@@ -3,15 +3,25 @@ import path from 'node:path';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import crypto from 'node:crypto';
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
6
7
|
|
|
7
8
|
const eventName = process.argv[2] || 'Unknown';
|
|
9
|
+
const hookScriptFilePath = fileURLToPath(import.meta.url);
|
|
10
|
+
const hookScriptWorkspaceRoot = (() => {
|
|
11
|
+
const candidate = path.resolve(path.dirname(hookScriptFilePath), '..', '..');
|
|
12
|
+
return fs.existsSync(path.join(candidate, '.openprd')) ? candidate : null;
|
|
13
|
+
})();
|
|
8
14
|
let input = '';
|
|
9
15
|
process.stdin.setEncoding('utf8');
|
|
10
16
|
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
11
17
|
process.stdin.on('end', () => {
|
|
12
18
|
let payload = {};
|
|
13
19
|
try { payload = input.trim() ? JSON.parse(input) : {}; } catch {}
|
|
14
|
-
const cwd = payload.cwd
|
|
20
|
+
const cwd = payload.cwd
|
|
21
|
+
|| payload.workspace_root
|
|
22
|
+
|| payload.workspaceRoot
|
|
23
|
+
|| hookScriptWorkspaceRoot
|
|
24
|
+
|| process.cwd();
|
|
15
25
|
const result = handle(eventName, cwd, payload);
|
|
16
26
|
if (result) {
|
|
17
27
|
process.stdout.write(JSON.stringify(result));
|
|
@@ -98,6 +108,7 @@ function defaultTurnState() {
|
|
|
98
108
|
return {
|
|
99
109
|
version: 1,
|
|
100
110
|
id: null,
|
|
111
|
+
knowledgeCandidateId: null,
|
|
101
112
|
sessionId: null,
|
|
102
113
|
prompt: null,
|
|
103
114
|
promptPreview: null,
|
|
@@ -296,6 +307,9 @@ function commandText(payload) {
|
|
|
296
307
|
if (typeof toolInput === 'string') {
|
|
297
308
|
return toolInput;
|
|
298
309
|
}
|
|
310
|
+
if (toolInput && typeof toolInput.command === 'string') {
|
|
311
|
+
return toolInput.command;
|
|
312
|
+
}
|
|
299
313
|
if (toolInput && typeof toolInput.cmd === 'string') {
|
|
300
314
|
return toolInput.cmd;
|
|
301
315
|
}
|
|
@@ -593,6 +607,270 @@ function readTextSync(filePath, maxLength = 20000) {
|
|
|
593
607
|
}
|
|
594
608
|
}
|
|
595
609
|
|
|
610
|
+
function readJsonlTailSync(filePath, maxLines = 160, maxBytes = 262144) {
|
|
611
|
+
try {
|
|
612
|
+
const stat = fs.statSync(filePath);
|
|
613
|
+
const size = Number(stat.size || 0);
|
|
614
|
+
const start = Math.max(0, size - maxBytes);
|
|
615
|
+
const length = Math.max(0, size - start);
|
|
616
|
+
const fd = fs.openSync(filePath, 'r');
|
|
617
|
+
const buffer = Buffer.alloc(length);
|
|
618
|
+
fs.readSync(fd, buffer, 0, length, start);
|
|
619
|
+
fs.closeSync(fd);
|
|
620
|
+
let lines = buffer.toString('utf8').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
621
|
+
if (start > 0 && lines.length > 0) {
|
|
622
|
+
lines = lines.slice(1);
|
|
623
|
+
}
|
|
624
|
+
return lines.slice(-maxLines).map((line) => {
|
|
625
|
+
try {
|
|
626
|
+
return JSON.parse(line);
|
|
627
|
+
} catch {
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
}).filter(Boolean);
|
|
631
|
+
} catch {
|
|
632
|
+
return [];
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function stateEventsPath(root) {
|
|
637
|
+
return path.join(root, '.openprd', 'state', 'events.jsonl');
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function latestDesignStarterRecord(root) {
|
|
641
|
+
const entries = readJsonlTailSync(stateEventsPath(root), 80, 131072);
|
|
642
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
643
|
+
const entry = entries[index];
|
|
644
|
+
if (entry?.type !== 'design_starter_created' || typeof entry.output !== 'string') {
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
const output = normalizeProjectFile(root, entry.output) || entry.output.trim();
|
|
648
|
+
if (!output) {
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
return {
|
|
652
|
+
...entry,
|
|
653
|
+
output,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const PATCH_MODE_ACTIVE_CONTRACT_FILES = [
|
|
660
|
+
'.openprd/design/active/facts-sheet.md',
|
|
661
|
+
'.openprd/design/active/asset-spec.md',
|
|
662
|
+
'.openprd/design/active/image-preflight.md',
|
|
663
|
+
'.openprd/design/active/direction-plan.md',
|
|
664
|
+
'.openprd/design/active/selected-direction.md',
|
|
665
|
+
];
|
|
666
|
+
|
|
667
|
+
const PATCH_MODE_HANDOFF_FOCUS_ALLOWANCE = 2;
|
|
668
|
+
|
|
669
|
+
function patchModeDraftCandidates(relativeTarget) {
|
|
670
|
+
const normalized = String(relativeTarget || '').replace(/\\/g, '/').trim();
|
|
671
|
+
if (!normalized) {
|
|
672
|
+
return [];
|
|
673
|
+
}
|
|
674
|
+
const parsed = path.posix.parse(normalized);
|
|
675
|
+
const dirPrefix = parsed.dir ? `${parsed.dir}/` : '';
|
|
676
|
+
const ext = parsed.ext || '';
|
|
677
|
+
const name = parsed.name || normalized;
|
|
678
|
+
return [
|
|
679
|
+
`${dirPrefix}${name}.next${ext}`,
|
|
680
|
+
`${dirPrefix}${name}.rewrite${ext}`,
|
|
681
|
+
];
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function safeProjectMtimeMs(root, relativePath) {
|
|
685
|
+
const normalized = normalizeProjectFile(root, relativePath);
|
|
686
|
+
if (!normalized) {
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
try {
|
|
690
|
+
return fs.statSync(path.join(root, normalized)).mtimeMs;
|
|
691
|
+
} catch {
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function patchModeTargetFiles(gate) {
|
|
697
|
+
return [...new Set([
|
|
698
|
+
gate?.targetFile,
|
|
699
|
+
...(Array.isArray(gate?.draftFiles) ? gate.draftFiles : []),
|
|
700
|
+
].filter(Boolean).map((file) => String(file).replace(/\\/g, '/')))];
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function patchModeFocusFiles(root, gate) {
|
|
704
|
+
return [...new Set([
|
|
705
|
+
gate?.targetFile,
|
|
706
|
+
...PATCH_MODE_ACTIVE_CONTRACT_FILES.map((file) => normalizeProjectFile(root, file) || file),
|
|
707
|
+
].filter(Boolean).map((file) => String(file).replace(/\\/g, '/')))];
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function isPatchModeWriteObserved(root, gate) {
|
|
711
|
+
const threshold = Number(gate?.armedAtMs ?? 0);
|
|
712
|
+
if (!threshold) {
|
|
713
|
+
return false;
|
|
714
|
+
}
|
|
715
|
+
for (const file of patchModeTargetFiles(gate)) {
|
|
716
|
+
const mtimeMs = safeProjectMtimeMs(root, file);
|
|
717
|
+
if (mtimeMs && mtimeMs > threshold) {
|
|
718
|
+
return true;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function transcriptPathFor(root, payload) {
|
|
725
|
+
const raw = String(payload?.transcript_path || payload?.transcriptPath || '').trim();
|
|
726
|
+
if (!raw) {
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
const resolved = path.isAbsolute(raw) ? path.resolve(raw) : path.resolve(root, raw);
|
|
730
|
+
return fs.existsSync(resolved) ? resolved : null;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function assistantMessageTextFromTranscriptEntry(entry) {
|
|
734
|
+
if (!entry || typeof entry !== 'object') {
|
|
735
|
+
return '';
|
|
736
|
+
}
|
|
737
|
+
if (entry.type === 'event_msg' && entry.payload?.type === 'agent_message') {
|
|
738
|
+
return String(entry.payload.message || '');
|
|
739
|
+
}
|
|
740
|
+
if (entry.type !== 'response_item') {
|
|
741
|
+
return '';
|
|
742
|
+
}
|
|
743
|
+
const payload = entry.payload || {};
|
|
744
|
+
if (payload.type !== 'message' || payload.role !== 'assistant') {
|
|
745
|
+
return '';
|
|
746
|
+
}
|
|
747
|
+
const content = Array.isArray(payload.content) ? payload.content : [];
|
|
748
|
+
return content
|
|
749
|
+
.filter((item) => item?.type === 'output_text' && typeof item.text === 'string')
|
|
750
|
+
.map((item) => item.text)
|
|
751
|
+
.join('\n');
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function hashText(value) {
|
|
755
|
+
return crypto.createHash('sha256').update(String(value || '')).digest('hex').slice(0, 16);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function hashProjectFile(root, relativePath) {
|
|
759
|
+
const normalized = normalizeProjectFile(root, relativePath);
|
|
760
|
+
if (!normalized) {
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
try {
|
|
764
|
+
const buffer = fs.readFileSync(path.join(root, normalized));
|
|
765
|
+
return crypto.createHash('sha256').update(buffer).digest('hex').slice(0, 16);
|
|
766
|
+
} catch {
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function patchModeFileHashSnapshot(root, files) {
|
|
772
|
+
const snapshot = {};
|
|
773
|
+
for (const file of files) {
|
|
774
|
+
const normalized = normalizeProjectFile(root, file);
|
|
775
|
+
if (!normalized) {
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
snapshot[normalized] = hashProjectFile(root, normalized);
|
|
779
|
+
}
|
|
780
|
+
return snapshot;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function latestAssistantTranscriptMessage(root, payload) {
|
|
784
|
+
const transcriptPath = transcriptPathFor(root, payload);
|
|
785
|
+
if (!transcriptPath) {
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
const entries = readJsonlTailSync(transcriptPath, 240, 393216);
|
|
789
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
790
|
+
const text = assistantMessageTextFromTranscriptEntry(entries[index]);
|
|
791
|
+
if (!text) {
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
const normalized = stripMarkdown(text).replace(/\s+/g, ' ').trim();
|
|
795
|
+
if (!normalized) {
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
return {
|
|
799
|
+
text,
|
|
800
|
+
normalized,
|
|
801
|
+
preview: preview(normalized, 500),
|
|
802
|
+
hash: hashText(normalized),
|
|
803
|
+
transcriptPath,
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
return null;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function looksLikePatchModeDeclaration(text) {
|
|
810
|
+
const normalized = stripMarkdown(text).replace(/\s+/g, ' ').trim();
|
|
811
|
+
if (!normalized) {
|
|
812
|
+
return false;
|
|
813
|
+
}
|
|
814
|
+
if (/(不要|先别|暂时别).{0,10}(覆盖|重写|改写)/.test(normalized)) {
|
|
815
|
+
return false;
|
|
816
|
+
}
|
|
817
|
+
return [
|
|
818
|
+
/(?:开始|现在开始|准备开始|进入).{0,12}(?:整页)?(?:覆盖|重写|改写).{0,20}(?:入口文件|index\.html|首页|页面)/i,
|
|
819
|
+
/(?:开始|现在开始|准备开始).{0,12}(?:对|把)?\s*index\.html.{0,12}(?:覆盖|重写|改写)/i,
|
|
820
|
+
/(?:start|starting|begin|beginning|now).{0,24}(?:overwrite|rewrite|replace).{0,24}(?:index\.html|entry file|home page|landing page|page)/i,
|
|
821
|
+
/(?:overwrite|rewrite|replace).{0,24}(?:index\.html|entry file|home page|landing page).{0,24}(?:now|next)/i,
|
|
822
|
+
].some((pattern) => pattern.test(normalized));
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function commandLooksLikeFileWrite(command) {
|
|
826
|
+
const text = String(command || '');
|
|
827
|
+
if (!text) {
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
return /(?:^|[\s(;])(cat\s*<<|tee\b|cp\b|mv\b|install\b|perl\b|python\b|node\b|ruby\b|sed\s+-i\b|awk\b)|>>?/.test(text);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function isPatchModeWriteAttempt(root, payload, gate) {
|
|
834
|
+
const targetFiles = new Set(patchModeTargetFiles(gate));
|
|
835
|
+
if (targetFiles.size === 0) {
|
|
836
|
+
return false;
|
|
837
|
+
}
|
|
838
|
+
const name = toolName(payload) || (commandText(payload) ? 'Bash' : '');
|
|
839
|
+
const touchedFiles = extractTouchedFiles(root, payload);
|
|
840
|
+
if (['Write', 'Edit', 'MultiEdit', 'apply_patch'].includes(name) && touchedFiles.some((file) => targetFiles.has(file))) {
|
|
841
|
+
return true;
|
|
842
|
+
}
|
|
843
|
+
const command = commandText(payload).replace(/\\/g, '/');
|
|
844
|
+
if (!['Bash', 'LS', 'Glob', 'Grep'].includes(name) || !command || !commandLooksLikeFileWrite(command)) {
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
return [...targetFiles].some((file) => command.includes(file));
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function isPatchModeFocusAttempt(root, payload, gate) {
|
|
851
|
+
const remaining = Number(gate?.focusAllowanceRemaining ?? 0);
|
|
852
|
+
if (remaining <= 0) {
|
|
853
|
+
return false;
|
|
854
|
+
}
|
|
855
|
+
const focusFiles = new Set(patchModeFocusFiles(root, gate));
|
|
856
|
+
if (focusFiles.size === 0) {
|
|
857
|
+
return false;
|
|
858
|
+
}
|
|
859
|
+
const name = toolName(payload) || (commandText(payload) ? 'Bash' : '');
|
|
860
|
+
const touchedFiles = extractTouchedFiles(root, payload);
|
|
861
|
+
if (name === 'Read') {
|
|
862
|
+
return touchedFiles.length > 0 && touchedFiles.every((file) => focusFiles.has(file));
|
|
863
|
+
}
|
|
864
|
+
if (!['Bash', 'Glob', 'Grep', 'LS'].includes(name)) {
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
867
|
+
const command = commandText(payload).replace(/\\/g, '/');
|
|
868
|
+
if (!command || commandLooksLikeFileWrite(command)) {
|
|
869
|
+
return false;
|
|
870
|
+
}
|
|
871
|
+
return [...focusFiles].some((file) => command.includes(file));
|
|
872
|
+
}
|
|
873
|
+
|
|
596
874
|
function findChangeDir(root, changeId) {
|
|
597
875
|
const candidates = [
|
|
598
876
|
path.join(root, 'openprd', 'changes', changeId),
|
|
@@ -786,14 +1064,35 @@ function knowledgeSkillContextLines(knowledgeSkills) {
|
|
|
786
1064
|
if (matched.length === 0) {
|
|
787
1065
|
return [];
|
|
788
1066
|
}
|
|
1067
|
+
const mandatoryCheck = knowledgeSkills?.mandatoryCheck ?? null;
|
|
789
1068
|
const lines = [
|
|
790
|
-
|
|
1069
|
+
mandatoryCheck?.required
|
|
1070
|
+
? `项目级经验候选: 找到 ${matched.length} 条,并已加入当前上下文供判断`
|
|
1071
|
+
: `项目级 Skill: 自动命中 ${matched.length} 个,并已加入当前上下文`,
|
|
791
1072
|
];
|
|
1073
|
+
if (mandatoryCheck?.required) {
|
|
1074
|
+
lines.push(`${mandatoryCheck.title}: ${mandatoryCheck.summary}`);
|
|
1075
|
+
for (const instruction of (mandatoryCheck.instructions ?? []).slice(0, 4)) {
|
|
1076
|
+
lines.push(`- ${instruction}`);
|
|
1077
|
+
}
|
|
1078
|
+
if (Array.isArray(mandatoryCheck.focusSignals) && mandatoryCheck.focusSignals.length > 0) {
|
|
1079
|
+
lines.push(`当前判断线索: ${mandatoryCheck.focusSignals.join(';')}`);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
792
1082
|
for (const skill of matched.slice(0, 3)) {
|
|
793
1083
|
lines.push(`- ${skill.skillName}: ${skill.matchSummary || '命中当前上下文'}`);
|
|
794
|
-
|
|
1084
|
+
const useWhen = String(skill.useWhen || skill.description || '').trim().replace(/^use when\b[::]?\s*/i, '').trim();
|
|
1085
|
+
if (useWhen) {
|
|
1086
|
+
lines.push(` 适用时机: ${useWhen}`);
|
|
1087
|
+
} else if (skill.description) {
|
|
795
1088
|
lines.push(` 说明: ${skill.description}`);
|
|
796
1089
|
}
|
|
1090
|
+
if (Array.isArray(skill.reviewFirst) && skill.reviewFirst.length > 0) {
|
|
1091
|
+
lines.push(` 先看: ${skill.reviewFirst.slice(0, 3).join(';')}`);
|
|
1092
|
+
}
|
|
1093
|
+
if (Array.isArray(skill.antiPatterns) && skill.antiPatterns.length > 0) {
|
|
1094
|
+
lines.push(` 不要直接套用: ${skill.antiPatterns.slice(0, 2).join(';')}`);
|
|
1095
|
+
}
|
|
797
1096
|
if (Array.isArray(skill.touchedFiles) && skill.touchedFiles.length > 0) {
|
|
798
1097
|
lines.push(` 相关文件: ${skill.touchedFiles.slice(0, 4).join(';')}`);
|
|
799
1098
|
}
|
|
@@ -834,8 +1133,14 @@ function renderRunContextText(result) {
|
|
|
834
1133
|
lines.push(...knowledgeSkillContextLines(result.knowledgeSkills));
|
|
835
1134
|
lines.push('对外表达: 面向用户时,请优先说“本次调整”“后续任务”“继续落地”“完成后检查”这类人话,不要直接复述内部编号、命令、路径、版本号或流程术语。');
|
|
836
1135
|
const recommendation = result.recommendation || {};
|
|
1136
|
+
const clarifyReplyRequired = (recommendation.nextAction ?? '') === 'clarify-user'
|
|
1137
|
+
|| recommendation.title === 'clarify-user'
|
|
1138
|
+
|| recommendation.title === '继续本轮需求入口澄清';
|
|
837
1139
|
lines.push('建议下一步: ' + recommendation.title);
|
|
838
1140
|
lines.push('这样安排的原因: ' + recommendation.reason);
|
|
1141
|
+
if (clarifyReplyRequired) {
|
|
1142
|
+
lines.push('当前回复目标: 先在对话里输出 requirement 摘要或只追问 1 个最高价值澄清点;不要承诺“按默认方案直接实现”,也不要把“请帮我实现/继续实现”当成跳过 requirement 摘要确认的依据。');
|
|
1143
|
+
}
|
|
839
1144
|
if (recommendation.preparationCommand || recommendation.executionCommand || recommendation.commitCommand) {
|
|
840
1145
|
lines.push('开始动手前提: 只有在用户明确要求继续落地、实现、修复、深挖或提交时,才继续往下做;如果还缺这一步,就先用人话说明范围和影响。');
|
|
841
1146
|
}
|
|
@@ -873,7 +1178,7 @@ function renderRunContextText(result) {
|
|
|
873
1178
|
if (recommendation.commitCommand) {
|
|
874
1179
|
lines.push('内部提交参考: ' + recommendation.commitCommand);
|
|
875
1180
|
}
|
|
876
|
-
if (recommendation.loop?.worktreeRecommended) {
|
|
1181
|
+
if (recommendation.loop?.worktreeRecommended || recommendation.isolation?.worktreeRecommended) {
|
|
877
1182
|
lines.push('环境建议: 最好放到单独环境里继续,避免和别的事项串线。');
|
|
878
1183
|
}
|
|
879
1184
|
lines.push('内部检查参考: ' + recommendation.verifyCommand);
|
|
@@ -1119,9 +1424,7 @@ function analyzePromptIntent(prompt) {
|
|
|
1119
1424
|
const visualMockupRequest = imageGenerationTerms.test(text)
|
|
1120
1425
|
&& imageGenerationAction.test(text)
|
|
1121
1426
|
&& !codeVisualArtifactRequested;
|
|
1122
|
-
const
|
|
1123
|
-
&& /(大|较大|比较大|明显|重做|重构|改版|优化|重新设计|设计方向|三种|3种|方案|效果图|先看样子|确认方向|体验优化|产品内)/i.test(text);
|
|
1124
|
-
const visualReview = /效果图|实现截图|视觉对比|视觉评审|对标效果图|复刻/i.test(text);
|
|
1427
|
+
const visualReview = /效果图|实现截图|视觉对比|视觉评审|对标效果图|复刻|不一致|好丑|没对齐|对不上|不像/i.test(text);
|
|
1125
1428
|
const directBugfixExecution = explicitExecution && bugfixOrDiagnostic;
|
|
1126
1429
|
const newFeatureVerbMatched = /(新增|增加|新建)/.test(text);
|
|
1127
1430
|
const capabilityCreationMatched = capabilityCreationPatterns.some((pattern) => pattern.test(text));
|
|
@@ -1186,7 +1489,7 @@ function analyzePromptIntent(prompt) {
|
|
|
1186
1489
|
? 'l0'
|
|
1187
1490
|
: (!visualMockupRequest
|
|
1188
1491
|
&& !readOnly
|
|
1189
|
-
&& (
|
|
1492
|
+
&& (l1OptimizationMatched || requirementSignalMatched)
|
|
1190
1493
|
? 'l1'
|
|
1191
1494
|
: null)))
|
|
1192
1495
|
: null;
|
|
@@ -1206,7 +1509,6 @@ function analyzePromptIntent(prompt) {
|
|
|
1206
1509
|
readOnly,
|
|
1207
1510
|
simpleConcrete,
|
|
1208
1511
|
visualMockupRequest,
|
|
1209
|
-
largeUiChangeRequest,
|
|
1210
1512
|
continuationRequest,
|
|
1211
1513
|
continuationSessionId,
|
|
1212
1514
|
continuationTaskHandle,
|
|
@@ -1224,7 +1526,6 @@ function analyzePromptIntent(prompt) {
|
|
|
1224
1526
|
|| confirmation
|
|
1225
1527
|
|| readOnly
|
|
1226
1528
|
|| visualMockupRequest
|
|
1227
|
-
|| largeUiChangeRequest
|
|
1228
1529
|
|| continuationRequest
|
|
1229
1530
|
|| visualReview
|
|
1230
1531
|
|| publicRepoResearchRequest
|
|
@@ -1244,6 +1545,20 @@ function isShortAffirmativeConfirmation(prompt) {
|
|
|
1244
1545
|
return /^(可以|好|行|确认|没问题|OK|ok|yes|Yes|yep|Yep)[。!!,.,s]*$/.test(text);
|
|
1245
1546
|
}
|
|
1246
1547
|
|
|
1548
|
+
function looksLikeClarificationFollowupReply(prompt, intent = null) {
|
|
1549
|
+
const text = stripMarkdown(prompt).trim();
|
|
1550
|
+
if (!text || text.length > 48 || /[\n\r]/.test(text) || /[??]/.test(text)) {
|
|
1551
|
+
return false;
|
|
1552
|
+
}
|
|
1553
|
+
if (intent?.requiresIntake || intent?.readOnly || intent?.reviewDecision || intent?.publicRepoResearchRequest || intent?.externalTechResearchRequest || intent?.skillWorkflowEditRequest || intent?.continuationRequest) {
|
|
1554
|
+
return false;
|
|
1555
|
+
}
|
|
1556
|
+
if (/[::]/.test(text) && text.length > 24) {
|
|
1557
|
+
return false;
|
|
1558
|
+
}
|
|
1559
|
+
return true;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1247
1562
|
function detectRequirementIntakeMode(prompt) {
|
|
1248
1563
|
const text = String(prompt || '');
|
|
1249
1564
|
const deep = text.length >= 80
|
|
@@ -1459,6 +1774,93 @@ function closeWeappGate(root, sessionId = null, patch = {}) {
|
|
|
1459
1774
|
}, sessionId);
|
|
1460
1775
|
}
|
|
1461
1776
|
|
|
1777
|
+
function openPatchModeGate(root, patch, sessionId = null) {
|
|
1778
|
+
const current = readNamedGate(root, 'patch-mode', sessionId);
|
|
1779
|
+
return writeNamedGate(root, 'patch-mode', {
|
|
1780
|
+
version: 1,
|
|
1781
|
+
active: true,
|
|
1782
|
+
status: 'awaiting-entry-write',
|
|
1783
|
+
openedAt: current?.openedAt || now(),
|
|
1784
|
+
updatedAt: now(),
|
|
1785
|
+
...current,
|
|
1786
|
+
...patch,
|
|
1787
|
+
}, sessionId);
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
function closePatchModeGate(root, sessionId = null, patch = {}) {
|
|
1791
|
+
const current = readNamedGate(root, 'patch-mode', sessionId);
|
|
1792
|
+
if (!current) {
|
|
1793
|
+
return null;
|
|
1794
|
+
}
|
|
1795
|
+
return writeNamedGate(root, 'patch-mode', {
|
|
1796
|
+
...current,
|
|
1797
|
+
active: false,
|
|
1798
|
+
status: patch.status || 'closed',
|
|
1799
|
+
closedAt: patch.closedAt || now(),
|
|
1800
|
+
updatedAt: now(),
|
|
1801
|
+
...patch,
|
|
1802
|
+
}, sessionId);
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
function syncPatchModeGate(root, payload, sessionId = null, intent = null) {
|
|
1806
|
+
let gate = readNamedGate(root, 'patch-mode', sessionId);
|
|
1807
|
+
if (gate?.active && isPatchModeWriteObserved(root, gate)) {
|
|
1808
|
+
gate = closePatchModeGate(root, sessionId, {
|
|
1809
|
+
status: 'write-observed',
|
|
1810
|
+
writeObservedAt: now(),
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
const designStarter = latestDesignStarterRecord(root);
|
|
1814
|
+
if (!designStarter && !isFrontendTaskIntent(intent)) {
|
|
1815
|
+
return gate;
|
|
1816
|
+
}
|
|
1817
|
+
const targetFile = normalizeProjectFile(root, designStarter?.output || 'index.html') || 'index.html';
|
|
1818
|
+
const draftFiles = patchModeDraftCandidates(targetFile);
|
|
1819
|
+
const fileHashesAtArm = patchModeFileHashSnapshot(root, [targetFile, ...draftFiles]);
|
|
1820
|
+
const assistantMessage = latestAssistantTranscriptMessage(root, payload);
|
|
1821
|
+
if (assistantMessage && looksLikePatchModeDeclaration(assistantMessage.normalized)) {
|
|
1822
|
+
if (gate?.messageHash === assistantMessage.hash && gate?.targetFile === targetFile && gate?.phase === 'strict') {
|
|
1823
|
+
return gate;
|
|
1824
|
+
}
|
|
1825
|
+
return openPatchModeGate(root, {
|
|
1826
|
+
phase: 'strict',
|
|
1827
|
+
status: 'awaiting-entry-write',
|
|
1828
|
+
targetFile,
|
|
1829
|
+
targetHashAtArm: hashProjectFile(root, targetFile),
|
|
1830
|
+
fileHashesAtArm,
|
|
1831
|
+
draftFiles,
|
|
1832
|
+
focusAllowanceRemaining: 0,
|
|
1833
|
+
messageHash: assistantMessage.hash,
|
|
1834
|
+
messagePreview: assistantMessage.preview,
|
|
1835
|
+
transcriptPath: assistantMessage.transcriptPath,
|
|
1836
|
+
designStarterOutput: targetFile,
|
|
1837
|
+
armedAtMs: Date.now(),
|
|
1838
|
+
armedAt: now(),
|
|
1839
|
+
}, sessionId);
|
|
1840
|
+
}
|
|
1841
|
+
if (!designStarter) {
|
|
1842
|
+
return gate;
|
|
1843
|
+
}
|
|
1844
|
+
if (gate?.active && gate?.targetFile === targetFile) {
|
|
1845
|
+
return gate;
|
|
1846
|
+
}
|
|
1847
|
+
return openPatchModeGate(root, {
|
|
1848
|
+
phase: 'handoff',
|
|
1849
|
+
status: 'awaiting-patch-handoff',
|
|
1850
|
+
targetFile,
|
|
1851
|
+
targetHashAtArm: hashProjectFile(root, targetFile),
|
|
1852
|
+
fileHashesAtArm,
|
|
1853
|
+
draftFiles,
|
|
1854
|
+
focusAllowanceRemaining: PATCH_MODE_HANDOFF_FOCUS_ALLOWANCE,
|
|
1855
|
+
messageHash: assistantMessage?.hash || null,
|
|
1856
|
+
messagePreview: assistantMessage?.preview || null,
|
|
1857
|
+
transcriptPath: assistantMessage?.transcriptPath || null,
|
|
1858
|
+
designStarterOutput: targetFile,
|
|
1859
|
+
armedAtMs: Date.now(),
|
|
1860
|
+
armedAt: now(),
|
|
1861
|
+
}, sessionId);
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1462
1864
|
function evaluateRequirementGateProgress(root, sessionId = null) {
|
|
1463
1865
|
const gate = readRequirementGate(root, sessionId);
|
|
1464
1866
|
const binding = readSessionBinding(root, sessionId);
|
|
@@ -1477,17 +1879,45 @@ function evaluateRequirementGateProgress(root, sessionId = null) {
|
|
|
1477
1879
|
const hasTaskBreakdown = Boolean(activeChange && Number(taskSummary?.total ?? 0) > 0);
|
|
1478
1880
|
const approvalPolicy = requirementApprovalPolicy(gate);
|
|
1479
1881
|
const clarificationConfirmed = requirementWritePathExplicitlyAuthorized(gate);
|
|
1882
|
+
const recommendationNextAction = run.parsed?.recommendation?.nextAction
|
|
1883
|
+
?? run.parsed?.recommendation?.title
|
|
1884
|
+
?? '';
|
|
1885
|
+
const missingRequiredFields = Number(run.parsed?.analysis?.missingRequiredFields ?? 0);
|
|
1886
|
+
const lacksProductType = !run.parsed?.analysis?.productType;
|
|
1887
|
+
const clarificationStillNeedsWriteback = clarificationConfirmed
|
|
1888
|
+
&& recommendationNextAction === 'clarify-user'
|
|
1889
|
+
&& (lacksProductType || missingRequiredFields > 0);
|
|
1890
|
+
const classificationStillNeeded = clarificationConfirmed && recommendationNextAction === 'classify';
|
|
1891
|
+
const interviewStillNeeded = clarificationConfirmed && recommendationNextAction === 'interview';
|
|
1480
1892
|
let nextStep = 'implementation-ready';
|
|
1481
1893
|
let reason = '这版需求和后续任务都已经准备好了;如果用户原本就明确要继续做,就直接往下推进,否则再补一句清楚的人话授权。';
|
|
1482
1894
|
if (!review.versionId) {
|
|
1483
|
-
nextStep = clarificationConfirmed
|
|
1895
|
+
nextStep = clarificationConfirmed
|
|
1896
|
+
? (
|
|
1897
|
+
clarificationStillNeedsWriteback
|
|
1898
|
+
? 'clarification-writeback-required'
|
|
1899
|
+
: classificationStillNeeded
|
|
1900
|
+
? 'product-type-classification-required'
|
|
1901
|
+
: interviewStillNeeded
|
|
1902
|
+
? 'requirement-interview-required'
|
|
1903
|
+
: 'prd-synthesis-required'
|
|
1904
|
+
)
|
|
1905
|
+
: 'clarification-confirmation-required';
|
|
1484
1906
|
reason = clarificationConfirmed
|
|
1485
1907
|
? (
|
|
1486
|
-
|
|
1487
|
-
? '
|
|
1488
|
-
:
|
|
1908
|
+
clarificationStillNeedsWriteback
|
|
1909
|
+
? '用户已经确认当前需求摘要或默认方案,但 current.json 里还没有把这轮确认内容按 canonical 字段稳定写回。不要重新回到泛化 clarify,也不要继续写 agent-inferred / project-derived 事实;请直接用 openprd capture 按 canonical 字段路径并以 user-confirmed 写回这轮已确认内容。'
|
|
1910
|
+
: classificationStillNeeded
|
|
1911
|
+
? '用户已经确认当前需求摘要,但当前 lane 还没有锁定产品场景。先完成 openprd classify .,让产品场景和后续写回事实一致,再继续 synthesize。'
|
|
1912
|
+
: interviewStillNeeded
|
|
1913
|
+
? '用户已经确认当前需求摘要,但当前 lane 仍缺少必填 requirement 事实。先继续用 openprd capture 按 canonical 字段补齐当前范围、目标、约束和交接信息;如还缺问题,再用 openprd interview . 收敛剩余缺口,然后继续 synthesize。'
|
|
1914
|
+
: (
|
|
1915
|
+
reviewPolicyAllowsSilentRecord(approvalPolicy)
|
|
1916
|
+
? '用户已明确表示不需要再停下来确认,本轮可以直接整理需求事实、生成这版确认稿,并继续后面的整理步骤。'
|
|
1917
|
+
: '当前需求摘要已经确认,下一步把已确认内容整理成可确认的需求稿。'
|
|
1918
|
+
)
|
|
1489
1919
|
)
|
|
1490
|
-
: '当前还缺需求摘要确认。先在对话里按“需求判断 / 需求理解 / 功能范围 / 技术方案”整理结构化摘要,其中“功能范围”和“技术方案”优先用 Markdown
|
|
1920
|
+
: '当前还缺需求摘要确认。先在对话里按“需求判断 / 需求理解 / 功能范围 / 技术方案”整理结构化摘要,其中“功能范围”和“技术方案”优先用 Markdown 表格;`需求判断` 和 `需求理解` 先用 1 到 2 句轻量主句说清这次是什么、核心问题和第一版目标,再把边界、风险和技术细项下沉到后续分项或表格。若当前还在判断值不值得做,还要主动补上验证与创业闭环:第一批最容易触达的社区或种子用户、当前替代方案、先怎么手工交付、什么承诺才算真需求、最低成本先验证什么,以及验证阶段怎样先活下来。用户没确认前,不要直接把核心需求写成既定事实。';
|
|
1491
1921
|
} else if (review.status === 'needs-revision') {
|
|
1492
1922
|
nextStep = 'prd-review-required';
|
|
1493
1923
|
reason = '当前这版需求确认稿已经被标记为需要调整,先改完再继续后面的整理或实现。';
|
|
@@ -1603,7 +2033,10 @@ function gateHasConfirmedCurrentReview(gate, progress) {
|
|
|
1603
2033
|
function canAutoAuthorizeRequirementExecution(gate, progress) {
|
|
1604
2034
|
return Boolean(
|
|
1605
2035
|
isBlockingRequirementGate(gate)
|
|
1606
|
-
&&
|
|
2036
|
+
&& (
|
|
2037
|
+
gate?.intent?.explicitExecution
|
|
2038
|
+
|| gate?.reviewActionAuthorization?.continueAfterReview
|
|
2039
|
+
)
|
|
1607
2040
|
&& progress?.nextStep === 'implementation-ready'
|
|
1608
2041
|
&& gateHasConfirmedCurrentReview(gate, progress)
|
|
1609
2042
|
);
|
|
@@ -1655,15 +2088,129 @@ function isMutationPayload(payload, risk) {
|
|
|
1655
2088
|
|| /\*\*\* Begin Patch/.test(text);
|
|
1656
2089
|
}
|
|
1657
2090
|
|
|
2091
|
+
function isFrontendTaskIntent(intent = null) {
|
|
2092
|
+
const text = String(intent?.promptText || '').trim();
|
|
2093
|
+
if (!text || intent?.visualMockupRequest) {
|
|
2094
|
+
return false;
|
|
2095
|
+
}
|
|
2096
|
+
return /(界面|页面|前端|首页|落地页|原型|导览|展览|馆藏|网站|web\s*page|landing|dashboard|hero|layout|静态单页|静态页|样式|视觉|app\s*首页|官网)/i.test(text);
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
function promptHasExplicitVisualReference(intent = null) {
|
|
2100
|
+
const text = String(intent?.promptText || '');
|
|
2101
|
+
return /(参考图|效果图|设计稿|按这个做|照着这个做|复刻|跟这个|reference|mockup|figma)/i.test(text);
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
function shouldRequireFactsSheet(intent = null) {
|
|
2105
|
+
const text = String(intent?.promptText || '');
|
|
2106
|
+
return /(开放时间|营业时间|票价|价格|英镑|美元|会员|版本|发布|规格|排名|数据|hours?|price|ticket|version|release|spec)/i.test(text);
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
function shouldRequireImagePreflight(intent = null) {
|
|
2110
|
+
const text = String(intent?.promptText || '');
|
|
2111
|
+
return /(馆藏|展览|导览|展品|museum|gallery|travel|旅行|内容|专题|story|editorial|摄影|图片|photo|地图|route|路线规划)/i.test(text);
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
function shouldRequireAssetSpec(intent = null) {
|
|
2115
|
+
return isFrontendTaskIntent(intent);
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
function designActiveFilePath(root, filename) {
|
|
2119
|
+
return path.join(root, '.openprd', 'design', 'active', filename);
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
function designArtifactHasPlaceholder(root, filename) {
|
|
2123
|
+
const text = readTextSync(designActiveFilePath(root, filename), 12000);
|
|
2124
|
+
return !text || /待填写/.test(text);
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
function looksLikeFrontendImplementationFile(relativePath) {
|
|
2128
|
+
const file = String(relativePath || '');
|
|
2129
|
+
if (!file || file.startsWith('.openprd/')) {
|
|
2130
|
+
return false;
|
|
2131
|
+
}
|
|
2132
|
+
if (/^(miniprogram|wechat|weapp)\//i.test(file)) {
|
|
2133
|
+
return false;
|
|
2134
|
+
}
|
|
2135
|
+
if (/(^|\/)(index|home|landing)\.(html|jsx|tsx|vue|svelte|astro)$/i.test(file)) {
|
|
2136
|
+
return true;
|
|
2137
|
+
}
|
|
2138
|
+
if (/\.(html|css|scss|sass|less|jsx|tsx|vue|svelte|astro)$/i.test(file)) {
|
|
2139
|
+
return true;
|
|
2140
|
+
}
|
|
2141
|
+
if (!/\.(js|ts)$/i.test(file)) {
|
|
2142
|
+
return false;
|
|
2143
|
+
}
|
|
2144
|
+
return /(^|\/)(app|pages?|components?|ui|layouts?|views?|screens?|routes?|frontend|web)\//i.test(file);
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
function frontendImplementationTargets(root, payload) {
|
|
2148
|
+
return extractTouchedFiles(root, payload).filter((file) => looksLikeFrontendImplementationFile(file));
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
function frontendDesignPreflightIssues(root, intent, implementationFiles) {
|
|
2152
|
+
if (!isFrontendTaskIntent(intent) || implementationFiles.length === 0) {
|
|
2153
|
+
return [];
|
|
2154
|
+
}
|
|
2155
|
+
const issues = [];
|
|
2156
|
+
if (shouldRequireFactsSheet(intent) && designArtifactHasPlaceholder(root, 'facts-sheet.md')) {
|
|
2157
|
+
issues.push({
|
|
2158
|
+
file: 'facts-sheet.md',
|
|
2159
|
+
reason: '这次页面要写明确事实,先把已知事实和来源或用户给定值写进去。',
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
if (shouldRequireAssetSpec(intent) && designArtifactHasPlaceholder(root, 'asset-spec.md')) {
|
|
2163
|
+
issues.push({
|
|
2164
|
+
file: 'asset-spec.md',
|
|
2165
|
+
reason: '进入实现前先冻结资产口径;没有官方资产也要写“缺失 / 暂无 / 不适用”,不要留模板占位。',
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
if (shouldRequireImagePreflight(intent) && designArtifactHasPlaceholder(root, 'image-preflight.md')) {
|
|
2169
|
+
issues.push({
|
|
2170
|
+
file: 'image-preflight.md',
|
|
2171
|
+
reason: '这是内容型页面,先判断真实图片是不是成立前提,以及缺失时怎么降级。',
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
if (!promptHasExplicitVisualReference(intent) && designArtifactHasPlaceholder(root, 'direction-plan.md')) {
|
|
2175
|
+
issues.push({
|
|
2176
|
+
file: 'direction-plan.md',
|
|
2177
|
+
reason: '当前没有明确参考方向,先把 3 个异源方向写实,而不是直接落回同一种安全解。',
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
if (!promptHasExplicitVisualReference(intent) && designArtifactHasPlaceholder(root, 'selected-direction.md')) {
|
|
2181
|
+
issues.push({
|
|
2182
|
+
file: 'selected-direction.md',
|
|
2183
|
+
reason: '进入实现前先锁定选中的 lens、theme、layout 和组件。',
|
|
2184
|
+
});
|
|
2185
|
+
}
|
|
2186
|
+
return issues;
|
|
2187
|
+
}
|
|
2188
|
+
|
|
1658
2189
|
function captureSourceFromCommand(command) {
|
|
1659
2190
|
return readCliFlagValue(command, '--source');
|
|
1660
2191
|
}
|
|
1661
2192
|
|
|
2193
|
+
function captureFieldFromCommand(command) {
|
|
2194
|
+
return readCliFlagValue(command, '--field');
|
|
2195
|
+
}
|
|
2196
|
+
|
|
1662
2197
|
function isNonSemanticCaptureCommand(command) {
|
|
1663
2198
|
return /openprd\s+capture\b/i.test(command)
|
|
1664
2199
|
&& captureSourceFromCommand(command) === 'agent-normalized';
|
|
1665
2200
|
}
|
|
1666
2201
|
|
|
2202
|
+
function isDerivedClarificationCaptureCommand(command) {
|
|
2203
|
+
if (!/openprd\s+capture\b/i.test(command)) {
|
|
2204
|
+
return false;
|
|
2205
|
+
}
|
|
2206
|
+
const source = captureSourceFromCommand(command);
|
|
2207
|
+
return source === 'agent-inferred' || source === 'project-derived';
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
function isMetaStatusCaptureCommand(command) {
|
|
2211
|
+
return /openprd\s+capture\b/i.test(command) && captureFieldFromCommand(command) === 'meta.status';
|
|
2212
|
+
}
|
|
2213
|
+
|
|
1667
2214
|
function gateHasClarificationConfirmation(gate) {
|
|
1668
2215
|
return Boolean(gate?.clarificationConfirmedAt || gate?.status === 'clarification-confirmed');
|
|
1669
2216
|
}
|
|
@@ -1696,9 +2243,27 @@ function isAllowedDuringRequirementGate(root, payload, gate, sessionId = null) {
|
|
|
1696
2243
|
return true;
|
|
1697
2244
|
}
|
|
1698
2245
|
if (/openprd\s+capture\b/i.test(command)) {
|
|
1699
|
-
|
|
2246
|
+
if (isNonSemanticCaptureCommand(command)) {
|
|
2247
|
+
return true;
|
|
2248
|
+
}
|
|
2249
|
+
if (!requirementWritePathExplicitlyAuthorized(gate)) {
|
|
2250
|
+
return false;
|
|
2251
|
+
}
|
|
2252
|
+
if (progress.nextStep === 'clarification-writeback-required') {
|
|
2253
|
+
if (isMetaStatusCaptureCommand(command)) {
|
|
2254
|
+
return false;
|
|
2255
|
+
}
|
|
2256
|
+
if (isDerivedClarificationCaptureCommand(command)) {
|
|
2257
|
+
return false;
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
return true;
|
|
1700
2261
|
}
|
|
1701
|
-
if (/openprd\s+
|
|
2262
|
+
if (/openprd\s+synthesize\b/i.test(command)) {
|
|
2263
|
+
return requirementWritePathExplicitlyAuthorized(gate)
|
|
2264
|
+
&& !['clarification-writeback-required', 'product-type-classification-required', 'requirement-interview-required'].includes(progress.nextStep);
|
|
2265
|
+
}
|
|
2266
|
+
if (/openprd\s+(classify|diagram)\b/i.test(command)) {
|
|
1702
2267
|
return requirementWritePathExplicitlyAuthorized(gate);
|
|
1703
2268
|
}
|
|
1704
2269
|
if (/openprd\s+review-presentation\b/i.test(command)) {
|
|
@@ -1946,7 +2511,8 @@ function runOpenPrd(args, cwd) {
|
|
|
1946
2511
|
const result = spawnSync(command, commandArgs, {
|
|
1947
2512
|
cwd,
|
|
1948
2513
|
encoding: 'utf8',
|
|
1949
|
-
timeout:
|
|
2514
|
+
timeout: 30000,
|
|
2515
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
1950
2516
|
env: process.env,
|
|
1951
2517
|
});
|
|
1952
2518
|
return {
|
|
@@ -1969,6 +2535,17 @@ function parseJsonOutput(text) {
|
|
|
1969
2535
|
}
|
|
1970
2536
|
}
|
|
1971
2537
|
|
|
2538
|
+
function compactSingleLine(text, limit = 240) {
|
|
2539
|
+
const singleLine = String(text || '').replace(/\s+/g, ' ').trim();
|
|
2540
|
+
if (!singleLine) {
|
|
2541
|
+
return '';
|
|
2542
|
+
}
|
|
2543
|
+
if (singleLine.length <= limit) {
|
|
2544
|
+
return singleLine;
|
|
2545
|
+
}
|
|
2546
|
+
return `${singleLine.slice(0, Math.max(0, limit - 1))}…`;
|
|
2547
|
+
}
|
|
2548
|
+
|
|
1972
2549
|
function devCheckWrapUpMessage(root, turnState) {
|
|
1973
2550
|
const files = Array.isArray(turnState?.touchedFiles)
|
|
1974
2551
|
? [...new Set(turnState.touchedFiles)].filter(Boolean)
|
|
@@ -2023,7 +2600,8 @@ function summarizeRunVerifyCheck(parsed, fallbackText = '') {
|
|
|
2023
2600
|
|
|
2024
2601
|
function summarizeDoctorCheck(parsed, fallbackText = '') {
|
|
2025
2602
|
if (!parsed?.agentIntegration && !parsed?.standards && !parsed?.validation) {
|
|
2026
|
-
|
|
2603
|
+
const compact = compactSingleLine(fallbackText);
|
|
2604
|
+
return compact || 'doctor result unavailable';
|
|
2027
2605
|
}
|
|
2028
2606
|
const parts = [];
|
|
2029
2607
|
if (parsed.agentIntegration) {
|
|
@@ -2144,32 +2722,33 @@ function requirementGateMessage(intent, gate) {
|
|
|
2144
2722
|
'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.',
|
|
2145
2723
|
'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.',
|
|
2146
2724
|
'Do not create temporary HTML/SVG/CSS files for this image unless the user explicitly requested that format.',
|
|
2147
|
-
'Use `imagegen`, which is Codex native Image 2, to generate the image; keep implementation, PRD review, and visual-compare for later explicit confirmation.',
|
|
2725
|
+
'Use `imagegen`, which is Codex native Image 2, to generate the image; keep implementation, PRD review, and visual-compare for later explicit confirmation of whether this generated image should become a reference.',
|
|
2148
2726
|
].join('\n');
|
|
2149
2727
|
}
|
|
2150
2728
|
const status = gateBlocksImplementation ? 'active' : 'opened';
|
|
2151
2729
|
return [
|
|
2152
2730
|
'OpenPrd requirement intake gate: ' + status + '.',
|
|
2153
2731
|
'This prompt looks like a likely 新功能/新流程方案 (L2), so the heavy requirement-intake lane is active. Do not decide from fixed keywords; first use $openprd-requirement-intake to classify the user-visible requirement type by impact, unknowns, decision cost, and validation cost.',
|
|
2154
|
-
'Keep this mapping visible for internal review:
|
|
2732
|
+
'Keep this mapping visible for internal review: 直接处理=L0, 现有功能优化=L1, 新功能/新流程方案=L2.',
|
|
2155
2733
|
'L0 and L1 stay on lightweight paths and should not be forced through formal PRD/review/change/tasks unless the scope expands.',
|
|
2156
2734
|
'If the requirement type is 新功能/新流程方案 (L2), do not edit implementation files yet and proceed through PRD/review/change/tasks with the appropriate PRD scene lens: 通用场景、面向个人消费者场景、面向企业服务场景,或以 Agent 为主要使用场景。 Keep raw enum values such as base / consumer / b2b / agent for internal commands or records only; do not surface them to the user unless truly necessary.',
|
|
2157
2735
|
reviewPolicyAllowsSilentRecord(approvalPolicy)
|
|
2158
2736
|
? 'Decision-point policy: because the user explicitly said there is no need for any confirmation stop, you may skip the requirement-summary confirmation stop, write back the requirement facts, synthesize the PRD, record the exact current stable review artifact, generate the OpenPrd change, prepare the task breakdown, then implement within the confirmed scope.'
|
|
2159
|
-
: 'Decision-point policy: first output a short structured requirement summary in chat with 需求判断 / 需求理解 / 功能范围 / 技术方案, where 功能范围 and 技术方案 should prefer Markdown tables; wait for the user to confirm that summary, then write back confirmed facts, 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.',
|
|
2737
|
+
: 'Decision-point policy: first output a short structured requirement summary in chat with 需求判断 / 需求理解 / 功能范围 / 技术方案, where 功能范围 and 技术方案 should prefer Markdown tables; if this is still a 0-to-1 or worth-doing discussion, also surface the validation/startup loop with 第一批最容易触达的社区或种子用户、当前替代方案、先怎么手工交付、什么承诺才算真需求、最低成本先验证什么、以及验证阶段怎样先活下来; wait for the user to confirm that summary, then write back confirmed facts, 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.',
|
|
2160
2738
|
reviewPolicyAllowsSilentRecord(approvalPolicy)
|
|
2161
2739
|
? 'This lane is in silent-record mode only because the user explicitly said there is no need for any further review or confirmation stop. Plain "请帮我实现" is not enough; you may record only the exact current version, digest, and work unit.'
|
|
2162
2740
|
: 'Requirement-summary confirmation, review-artifact confirmation, and implementation authorization are different gates: do not treat "可以开做", "继续实现", plain "请帮我实现", or "不需要评审" as permission to skip them.',
|
|
2163
2741
|
'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.',
|
|
2164
|
-
'Recommended next action: write a short 需求类型判断 in chat, and by default merge the route into the label as 需求类型:新功能/新流程方案(L2); only add a separate 内部路由码 when internal debugging truly benefits. Then run openprd clarify ., summarize the requirement in chat using 需求判断 / 需求理解 / 功能范围 / 技术方案, prefer Markdown tables for 功能范围 and 技术方案, ask for confirmation, and only after that write back confirmed facts. Do not open a clarification HTML page; the formal HTML review happens after synthesize/review.',
|
|
2742
|
+
'Recommended next action: write a short 需求类型判断 in chat, and by default merge the route into the label as 需求类型:新功能/新流程方案(L2); only add a separate 内部路由码 when internal debugging truly benefits. Then run openprd clarify ., summarize the requirement in chat using 需求判断 / 需求理解 / 功能范围 / 技术方案, prefer Markdown tables for 功能范围 and 技术方案, and if the ask still looks like 0-to-1 validation also add 第一批最容易触达的社区或种子用户、当前替代方案、先怎么手工交付、什么承诺才算真需求、最低成本先验证什么、以及验证阶段怎样先活下来; ask for confirmation, and only after that write back confirmed facts. Do not open a clarification HTML page; the formal HTML review happens after synthesize/review.',
|
|
2165
2743
|
].join('\n');
|
|
2166
2744
|
}
|
|
2167
2745
|
|
|
2168
2746
|
function lightweightRequirementMessage(intent) {
|
|
2169
2747
|
if (intent?.requirementTier === 'l0') {
|
|
2170
2748
|
return [
|
|
2171
|
-
'OpenPrd 轻量需求路径:
|
|
2172
|
-
'先在 chat 用短格式写出“需求类型 / 理由 /
|
|
2749
|
+
'OpenPrd 轻量需求路径: 当前更接近直接处理 (L0)。',
|
|
2750
|
+
'先在 chat 用短格式写出“需求类型 / 理由 / 推荐下一步”,并默认写成“需求类型:直接处理(L0)”;只有内部排障确实需要时,才额外单列“内部路由码”。',
|
|
2751
|
+
'对用户先用一句轻量主句说清这次是什么和为什么先这样处理;边界和技术细项放到后一句,不要揉成一大段解释。',
|
|
2173
2752
|
'直接处理并事后说明即可,不打开正式 PRD/review/change/tasks。',
|
|
2174
2753
|
'优先做最小足够验证,并用 1-2 句说明本轮特别需要强化的测试点;默认不要求正式测试报告。',
|
|
2175
2754
|
'如果过程中暴露出跨系统依赖、支付/账号/权限/回调等高风险因素,再升级到 L2 重流程。',
|
|
@@ -2179,12 +2758,10 @@ function lightweightRequirementMessage(intent) {
|
|
|
2179
2758
|
return [
|
|
2180
2759
|
'OpenPrd 轻量需求路径: 当前更接近现有功能优化 (L1)。',
|
|
2181
2760
|
'先在 chat 用短格式写出“需求类型 / 理由 / 推荐下一步”,并默认写成“需求类型:现有功能优化(L1)”,再给 3-5 行 mini-plan;只有内部排障确实需要时,才额外单列“内部路由码”。',
|
|
2761
|
+
'主句先轻一点:先说这次调整是什么、要解决什么,再把范围边界和技术细项留给 mini-plan,不要一上来写成长段。',
|
|
2182
2762
|
'先在对话里给 3-5 行 mini-plan,至少写清目标、范围内、范围外和验证方式。',
|
|
2183
2763
|
'默认不要打开正式 PRD/review/change/tasks;只有在 mini-plan 暴露新决策缺口、跨系统风险或范围升级时,才提升到 L2 重流程。',
|
|
2184
2764
|
'验证采用最小足够组合即可,重点说明需要强化测试的地方;默认不要求正式测试报告。',
|
|
2185
|
-
intent?.largeUiChangeRequest
|
|
2186
|
-
? '如果这是大界面改动,mini-plan 之后先做 3 方向视觉方案评审,再进入实现。'
|
|
2187
|
-
: '',
|
|
2188
2765
|
].filter(Boolean).join('\n');
|
|
2189
2766
|
}
|
|
2190
2767
|
return null;
|
|
@@ -2200,20 +2777,38 @@ function visualMockupMessage(intent) {
|
|
|
2200
2777
|
'对 logo、icon、avatar、badge 等开发素材,如果用户没有明确要求 mockup、场景图、设备框、卡片承载、名片/包装展示或参考界面复刻,默认按独立素材输出(standalone asset)处理:使用全画布单主体,不额外添加 UI frame、卡片、设备壳、名片、桌面陈列、手持实拍或其他展示容器。',
|
|
2201
2778
|
'只有当用户明确要求 mockup、场景化效果图、容器化呈现,或参考图本身就包含这些承载结构时,才生成对应的容器或场景。',
|
|
2202
2779
|
'只有在实际发生 `imagegen` 调用后,才能汇报生图结果、失败或限流;未调用 `imagegen` 前,不要声称“生图限流”或“生图失败”。',
|
|
2203
|
-
'
|
|
2780
|
+
'把 `imagegen` 结果先当成候选效果图,不要默认登记到 `.openprd/harness/visual-reviews/`,也不要自动当成后续验收参考。',
|
|
2781
|
+
'如果用户看完图后还要继续做实现,主动连问三件事:是否符合预期、是否纳入后续效果图/实现截图对比、是否按此继续后续实现。只有用户确认纳入对比或继续实现后,才把这张图、这组图或其中选定子图整理成后续 reference-set;如果用户只是认可图片但暂不实现,就继续把它当候选效果图。',
|
|
2782
|
+
'如果一张图里有多个子图、网格、多对象或多个方向,不要把整板直接当成单一参考图;先运行 `openprd visual-prepare . --reference <效果图> --grid <列>x<行>` 或 `--boxes <plan.json>` 生成 reference-set、contact sheet 和 compare-plan,确认 contact sheet 后再逐项对比或统一验收。',
|
|
2783
|
+
'OpenPrd review.html 只用于需求评审,visual-compare 只用于实现阶段视觉证据:已有参考图时做效果图/实现截图对比;没有参考图时先判断新建界面还是修改既有界面,新建界面回到实现前 3 方向方案评审,修改既有界面做修改前/修改后自检;局部细节优先补局部焦点证据板,并行优化方向优先补并行实验证据板。',
|
|
2204
2784
|
].join('\n');
|
|
2205
2785
|
}
|
|
2206
2786
|
|
|
2207
|
-
function
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2787
|
+
function learningReviewMessage() {
|
|
2788
|
+
return [
|
|
2789
|
+
'如果当前交付目标是把一次工作转成可学习、可复用、可回看、可教学或可沉淀的材料,不要先按关键词判断,也不要只停留在普通说明文档。',
|
|
2790
|
+
'先判断期望产物形态:是否需要章节结构、证据锚点、图文讲解、检索练习、工作示例或阅读体验。需要这些形态时,优先使用 `openprd learn .` 生成学习包骨架、阅读器和证据清单,再补充正文内容;普通 Markdown 只能作为辅助讲义。',
|
|
2791
|
+
].join('\n');
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
function largeUiVisualDirectionMessage() {
|
|
2211
2795
|
return [
|
|
2212
2796
|
'OpenPrd 大界面改动视觉方案评审:',
|
|
2213
2797
|
'位置: 需求分流之后、PRD 定稿或实现开工之前;它不同于 review.html,也不同于实现后的 visual-compare。',
|
|
2214
|
-
'判断:
|
|
2215
|
-
'步骤:
|
|
2216
|
-
'交互:
|
|
2798
|
+
'判断: 先从用户目标、信息架构变化、视觉决策成本和验收风险判断是否需要方向评审,不用关键词触发。会决定首屏、核心布局、主视觉、关键路径、组件层级/密度,或用户需要先选设计方向时触发。',
|
|
2799
|
+
'步骤: 已有界面时用 Codex Computer Use 进入产品内对应功能并截当前真实界面;冷启动没有现有界面时,基于已确认 PRD、用户画像、第一版切片和视觉目标写设计 brief。然后调用 `imagegen`(Codex 原生 Image 2)生成至少 3 个不同设计思想方向;把效果图横向拼成一张带 1/2/3 序号的大图,先作为候选效果图给用户评审,不要默认写入 `.openprd/harness/visual-reviews/` 当验收参考。',
|
|
2800
|
+
'交互: 主动追问三件事:是否符合预期、是否纳入后续效果图/实现截图对比、是否按此继续后续实现。只有用户确认纳入后续对比或继续实现后,才把选定方向、整张图或其中子图整理成 reference-set 并写入 `.openprd/harness/visual-reviews/`;用户确认前,不进入大 UI 实现,也不要声称界面方案已定。',
|
|
2801
|
+
'多对象参考处理: 如果候选效果图是一张图里包含多个子图、网格、多对象或多方向,不要拿整板直接硬比;先运行 `openprd visual-prepare . --reference <效果图> --grid <列>x<行>` 或 `--boxes <plan.json>`,检查 contact sheet,确认 reference-set 后再逐项进入实现和验收。',
|
|
2802
|
+
].join('\n');
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
function visualLocalAdjustmentMessage() {
|
|
2806
|
+
return [
|
|
2807
|
+
'如果这轮验收重点在局部变化,不要只改代码后凭主观判断收尾。',
|
|
2808
|
+
'先判断这是新建界面还是修改既有界面:新建界面走实现前 3 方向方案评审;修改既有界面先保留修改前截图。完成后从同一入口、视口、账号和数据状态截修改后截图,再运行 `openprd visual-compare . --before <修改前截图> --after <修改后截图>`。',
|
|
2809
|
+
'如果局部细节需要放到同一张证据板里审阅,再补一份 `openprd visual-compare . --board <focus-board.json>` 的局部焦点证据板,把局部变化组合到同一张证据板里统一验收。',
|
|
2810
|
+
'如果参考图是一张整板、网格图或多对象组合图,先运行 `openprd visual-prepare . --reference <效果图> --grid <列>x<行>` 或 `--boxes <plan.json>`,确认 contact sheet 后,再决定是逐项 `--reference/--actual` 还是统一做 `focus-board` / `parallel-board`。',
|
|
2811
|
+
'当用户后续说“跟效果图”“不一致”“好丑”“复刻”“没对齐”这类反馈时,不能只口头说已经对比过了;至少先产出一份 `openprd visual-compare`、`focus-board` 或 `parallel-board` 证据图再下结论。'
|
|
2217
2812
|
].join('\n');
|
|
2218
2813
|
}
|
|
2219
2814
|
|
|
@@ -2231,7 +2826,8 @@ function confirmationGateMessage(gate) {
|
|
|
2231
2826
|
return [
|
|
2232
2827
|
intro,
|
|
2233
2828
|
'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.',
|
|
2234
|
-
'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>; if local detail matters more than the whole screen, add openprd visual-compare . --board <focus-board.json> so the agent can review numbered zoom regions. When there is no reference image, capture the before screenshot first, implement, capture the after screenshot from the same entry, viewport, account, and data state, then run openprd visual-compare . --before <before-screenshot> --after <after-screenshot>; if the agent explored multiple optimization directions, add openprd visual-compare . --board <parallel-board.json> and inspect expected changes plus unintended drift before claiming completion.',
|
|
2829
|
+
'For UI or visual work with an existing reference image, first confirm the user has accepted that image as a later comparison reference; if one image contains multiple sub-images, grid cells, or objects, run openprd visual-prepare . --reference <effect-image> --grid <columns>x<rows> or --boxes <plan.json>, inspect the contact sheet, and use the generated reference-set / compare-plan before comparing. Then capture the implemented UI and run openprd visual-compare . --reference <effect-image> --actual <implementation-screenshot>; if local detail matters more than the whole screen, add openprd visual-compare . --board <focus-board.json> so the agent can review numbered zoom regions. When there is no reference image, capture the before screenshot first, implement, capture the after screenshot from the same entry, viewport, account, and data state, then run openprd visual-compare . --before <before-screenshot> --after <after-screenshot>; if the agent explored multiple optimization directions, add openprd visual-compare . --board <parallel-board.json> and inspect expected changes plus unintended drift before claiming completion.',
|
|
2830
|
+
'If the user later says the implementation does not match the effect image, looks wrong, or asks for replication, do not rely on subjective narration alone; produce at least one visual evidence artifact before claiming completion.',
|
|
2235
2831
|
].join('\n');
|
|
2236
2832
|
}
|
|
2237
2833
|
|
|
@@ -2247,6 +2843,12 @@ function currentRequirementStatusLine(gate, progress) {
|
|
|
2247
2843
|
switch (progress?.nextStep) {
|
|
2248
2844
|
case 'clarification-confirmation-required':
|
|
2249
2845
|
return '当前卡点: 先让用户确认当前需求摘要;在此之前不要把核心需求写成既定事实,也不要直接往后推进。';
|
|
2846
|
+
case 'clarification-writeback-required':
|
|
2847
|
+
return '当前卡点: 用户已经确认这轮需求摘要或默认方案,但 current.json 里还没有把确认后的事实稳定写回。先按 canonical 字段路径用 user-confirmed 回写,再继续 synthesize;不要再回退成泛化 clarify,也不要继续写 agent-inferred。';
|
|
2848
|
+
case 'product-type-classification-required':
|
|
2849
|
+
return '当前卡点: 用户已经确认这轮需求摘要,但产品场景还没锁定。先完成 classify 并让当前 lane 的产品场景与写回事实一致,再继续 synthesize。';
|
|
2850
|
+
case 'requirement-interview-required':
|
|
2851
|
+
return '当前卡点: 用户已经确认这轮需求摘要,但 current.json 里还缺这轮 requirement 的关键事实。先补齐 canonical 字段,必要时再用 openprd interview 收敛剩余问题,然后继续 synthesize。';
|
|
2250
2852
|
case 'prd-synthesis-required':
|
|
2251
2853
|
return reviewPolicyAllowsSilentRecord(requirementApprovalPolicy(gate))
|
|
2252
2854
|
? '当前卡点: 用户已明确表示不需要再停下来确认,可以直接整理已确认内容,并生成这版需求确认稿。'
|
|
@@ -2279,7 +2881,7 @@ function currentRequirementMessage(intent, gate, progress) {
|
|
|
2279
2881
|
gateStatus,
|
|
2280
2882
|
'对外表达要求: 面向用户不要直接复述 PRD、review artifact、change、tasks、lane、approval policy、work unit、digest、worker shard、write-scope、worktree、内部版本号、命令或文件路径;改说“需求确认稿”“本次调整”“后续任务”“继续落地”“完成后检查”。',
|
|
2281
2883
|
'当前输入已被判定为可能的新功能/新流程方案(L2),因此进入重流程需求入口。不要按固定关键词判断;先用 $openprd-requirement-intake 按影响面、未知数、决策成本和验证成本判断用户可见需求类型。',
|
|
2282
|
-
'
|
|
2884
|
+
'内部审查保留固定对照:直接处理=L0,现有功能优化=L1,新功能/新流程方案=L2。',
|
|
2283
2885
|
'如果用户刚刚已经确认了现有功能优化(L1)的 mini-plan、范围边界或正式产品边界,下一句要明确写成“已确认,我按这个继续/收口/落地”;不要只写一个“确认”,更不要写成“确认,我们就按这个……”这种容易让用户误以为还要再表态的句子。',
|
|
2284
2886
|
'如果需求类型是新功能/新流程方案(L2),本轮只围绕这个新需求推进 PRD/review/change/tasks,并选择通用场景 / 面向个人消费者场景 / 面向企业服务场景 / 以 Agent 为主要使用场景的 PRD 视角,不自动继续历史 active change。对用户复述时不要直接把 consumer / b2b / agent 当展示词;这些枚举值只用于内部记录和命令。',
|
|
2285
2887
|
prompt ? '本轮需求: ' + prompt : '',
|
|
@@ -2295,11 +2897,8 @@ function currentRequirementMessage(intent, gate, progress) {
|
|
|
2295
2897
|
currentRequirementStatusLine(gate, progress),
|
|
2296
2898
|
reviewPolicyAllowsSilentRecord(approvalPolicy)
|
|
2297
2899
|
? 'Decision-point order: because the user explicitly waived any confirmation stop, you may skip requirement-summary confirmation, write back requirement facts, synthesize the PRD, record the exact stable review artifact, generate the OpenPrd change, prepare the task breakdown, then implement within the confirmed scope.'
|
|
2298
|
-
: 'Decision-point order: clarify the requirement, summarize it in chat using 需求判断 / 需求理解 / 功能范围 / 技术方案, prefer Markdown tables for 功能范围 and 技术方案, wait for the user to confirm that requirement summary, write back only confirmed facts, synthesize the PRD, wait for a human review decision on the stable artifact, generate the OpenPrd change, prepare the task breakdown, then implement within the confirmed scope.',
|
|
2299
|
-
|
|
2300
|
-
? 'Large UI direction gate: before PRD freeze or implementation, capture the current in-product screen with Codex Computer Use, generate at least three Image 2 directions, combine them into one horizontal numbered contact sheet, and wait for the user to choose a direction.'
|
|
2301
|
-
: '',
|
|
2302
|
-
'Recommended next action: 先在 chat 输出“需求类型判断”,默认把路由码并进“需求类型:新功能/新流程方案(L2)”这类标签里;只有内部排障确实需要时,才额外写“内部路由码”。若为新功能/新流程方案(L2),再运行 openprd clarify .,并按“需求判断 / 需求理解 / 功能范围 / 技术方案”给出十句话左右的结构化摘要,其中“功能范围”和“技术方案”优先用 Markdown 表格;请求确认后再写回 requirement 事实并继续 classify/synthesize,不要把这一步表述成“确认后直接开始实现”。Do not open clarification HTML; use review.html only after synthesize/review.',
|
|
2900
|
+
: 'Decision-point order: clarify the requirement, summarize it in chat using 需求判断 / 需求理解 / 功能范围 / 技术方案, prefer Markdown tables for 功能范围 and 技术方案, and if the ask still looks like 0-to-1 validation also surface 第一批最容易触达的社区或种子用户、当前替代方案、先怎么手工交付、什么承诺才算真需求、最低成本先验证什么、以及验证阶段怎样先活下来; wait for the user to confirm that requirement summary, write back only confirmed facts, synthesize the PRD, wait for a human review decision on the stable artifact, generate the OpenPrd change, prepare the task breakdown, then implement within the confirmed scope.',
|
|
2901
|
+
'Recommended next action: 先在 chat 输出“需求类型判断”,默认把路由码并进“需求类型:新功能/新流程方案(L2)”这类标签里;只有内部排障确实需要时,才额外写“内部路由码”。若为新功能/新流程方案(L2),再运行 openprd clarify .,并按“需求判断 / 需求理解 / 功能范围 / 技术方案”给出结构化摘要,其中“功能范围”和“技术方案”优先用 Markdown 表格;`需求判断` 和 `需求理解` 先用轻量主句说清这次是什么、核心问题和第一版目标,再把边界、风险、异常例子和技术细项下沉到后续分项或表格,不要把某条示例文案写成固定模板。如果这轮本质上还在判断值不值得做,还要主动补上:第一批最容易触达的社区或种子用户、当前替代方案、先怎么手工交付、什么承诺才算真需求、最低成本先验证什么,以及验证阶段怎样先活下来。请求确认后再写回 requirement 事实并继续 classify/synthesize,不要把这一步表述成“确认后直接开始实现”。Do not open clarification HTML; use review.html only after synthesize/review.',
|
|
2303
2902
|
];
|
|
2304
2903
|
if (isImplementationAdvanceIntent(intent)) {
|
|
2305
2904
|
lines.splice(2, 1, gate?.active
|
|
@@ -2311,7 +2910,7 @@ function currentRequirementMessage(intent, gate, progress) {
|
|
|
2311
2910
|
}
|
|
2312
2911
|
|
|
2313
2912
|
function requirementRoutingSummary() {
|
|
2314
|
-
return '需求类型由 $openprd-requirement-intake
|
|
2913
|
+
return '需求类型由 $openprd-requirement-intake 按影响面、未知数、决策成本和验证成本判断:直接处理(L0)可直接处理并事后说明,不打开正式 PRD/review/change/tasks;现有功能优化(L1)先给对话内 mini-plan,默认不生成正式 PRD/change/tasks;新功能/新流程方案(L2)才进入 requirement intake 与 PRD/review/change/tasks,并选择通用场景 / 面向个人消费者场景 / 面向企业服务场景 / 以 Agent 为主要使用场景的 PRD 视角。对用户复述时不要直接把 consumer / b2b / agent 当展示词;这些枚举值只用于内部记录和命令。单纯的“请帮我实现/继续实现”只表示有执行意图,不表示跳过 requirement 摘要确认或 review;只有用户明确表示不需要进行任何确认时,才允许静默走完整 requirement write path。';
|
|
2315
2914
|
}
|
|
2316
2915
|
|
|
2317
2916
|
function historicalRequirementReminder(root, runContext, intent, gate) {
|
|
@@ -2359,17 +2958,19 @@ function contextMessage(cwd, intent = null, gate = null, progress = null) {
|
|
|
2359
2958
|
'OpenPrd 上下文只是建议,不是自动执行指令。请先判断用户当前意图。',
|
|
2360
2959
|
lightweightRequirementMessage(intent),
|
|
2361
2960
|
visualMockupMessage(intent),
|
|
2362
|
-
|
|
2961
|
+
learningReviewMessage(),
|
|
2962
|
+
largeUiVisualDirectionMessage(),
|
|
2963
|
+
visualLocalAdjustmentMessage(),
|
|
2363
2964
|
requirementRoutingSummary(),
|
|
2364
2965
|
'如果用户只是要求看看、规划、分析、审查、解释影响或列出文件,请保持只读并基于证据回答;不要运行 OpenPrd loop、任务推进、discovery 推进、commit 或其他写入命令。',
|
|
2365
2966
|
'只有当用户当前明确要求开发、实现、修复、继续任务、深度调研、对标复刻或提交时,才运行 openprd loop --run、openprd tasks --advance、openprd discovery --advance、commit/push 等执行命令。',
|
|
2366
2967
|
'代码修改完成后、最终回复前,针对本轮实际 touched code files 运行 openprd dev-check . <file...>;若出现需要关注的文件,最终回复必须以 **后续建议** 为标题,直接复用 dev-check 生成的 Markdown 表格,列出影响对象、关注程度、规模信号、预警原因、本次处理结果和后续建议,并按 🔴 → 🟠 → 🟡 排序;不要把“关注程度”列改写成纯 emoji,必须保留例如“🟠 中风险|建议优先关注”这类完整标签;如果你改写了“预警原因 / 本次处理结果 / 后续建议”,先用 `node scripts/dev-check-wrapup-copy.mjs --validate` 校验每格不超过 20 字;若报错,按提示缩短后重试。',
|
|
2367
|
-
'
|
|
2368
|
-
'
|
|
2968
|
+
'大界面改动的效果图先当候选效果图;出图后主动确认是否符合预期、是否纳入后续效果图/实现截图对比、是否按此继续实现。只有确认后,才把选定方向、整张图或其中子图整理到 `.openprd/harness/visual-reviews/` 并进入大 UI 实现。',
|
|
2969
|
+
'涉及界面、页面、视觉、样式或前端体验时,已有确认参考图才运行 openprd visual-compare . --reference <效果图> --actual <实现截图>;如果一张参考图里有多个子图/对象/网格,先运行 openprd visual-prepare . --reference <效果图> --grid <列>x<行> 或 --boxes <plan.json>,确认 contact sheet 后再逐项对比。没有明确参考图时先判断新建还是修改:新建界面走实现前 3 方向方案评审;修改既有界面则动手前先截修改前截图,完成后运行 openprd visual-compare . --before <修改前截图> --after <修改后截图>;如果并行试了多个优化方向,再补一份 openprd visual-compare . --board <parallel-board.json>。用户后续如果说“跟效果图”“不一致”“好丑”“复刻”,不能只口头说对比过了,至少产出一份 visual-compare / focus-board / parallel-board 证据图。',
|
|
2369
2970
|
'发现可沉淀项时不要中途打断任务:代码扩展识别这类白名单工具补全会自动应用并记录;用户偏好、项目协作规矩和 OpenPrd 默认行为先记录为候选,收工时运行 openprd grow . --review 集中确认。',
|
|
2370
2971
|
'维护 OpenPrd 本身且涉及配置类能力时,先判断是否应纳入 openprd grow;高置信可成长默认纳入,不确定则主动询问用户。',
|
|
2371
2972
|
'涉及后端、脚本、Agent、工具链、服务或数据处理变更时,把 CLI 与 API 视为同级接入面:同步检查命令入口、参数、输出契约、help/doctor/dry-run/status 与接口协议、返回结构、身份边界是否受影响,并更新 docs/basic/backend-structure.md 或明确写不适用原因。',
|
|
2372
|
-
'声明实现就绪前,先运行 openprd standards . --verify 和 openprd run . --verify
|
|
2973
|
+
'声明实现就绪前,先运行 openprd standards . --verify、openprd quality . --verify 和 openprd run . --verify;L2 或跨页面实现的最终回复要列出最新 HTML 质量报告和 task-scoped Markdown/HTML 测试报告路径。',
|
|
2373
2974
|
].filter(Boolean).join('\n');
|
|
2374
2975
|
}
|
|
2375
2976
|
return [
|
|
@@ -2379,17 +2980,19 @@ function contextMessage(cwd, intent = null, gate = null, progress = null) {
|
|
|
2379
2980
|
'OpenPrd 上下文只是建议,不是自动执行指令。请先判断用户当前意图。',
|
|
2380
2981
|
lightweightRequirementMessage(intent),
|
|
2381
2982
|
visualMockupMessage(intent),
|
|
2382
|
-
|
|
2983
|
+
learningReviewMessage(),
|
|
2984
|
+
largeUiVisualDirectionMessage(),
|
|
2985
|
+
visualLocalAdjustmentMessage(),
|
|
2383
2986
|
requirementRoutingSummary(),
|
|
2384
2987
|
'如果用户只是要求看看、规划、分析、审查、解释影响或列出文件,请保持只读并基于证据回答;不要运行 OpenPrd loop、任务推进、discovery 推进、commit 或其他写入命令。',
|
|
2385
2988
|
'只有当用户当前明确要求开发、实现、修复、继续任务、深度调研、对标复刻或提交时,才运行 openprd loop --run、openprd tasks --advance、openprd discovery --advance、commit/push 等执行命令。',
|
|
2386
2989
|
'代码修改完成后、最终回复前,针对本轮实际 touched code files 运行 openprd dev-check . <file...>;若出现需要关注的文件,最终回复必须以 **后续建议** 为标题,直接复用 dev-check 生成的 Markdown 表格,列出影响对象、关注程度、规模信号、预警原因、本次处理结果和后续建议,并按 🔴 → 🟠 → 🟡 排序;不要把“关注程度”列改写成纯 emoji,必须保留例如“🟠 中风险|建议优先关注”这类完整标签;如果你改写了“预警原因 / 本次处理结果 / 后续建议”,先用 `node scripts/dev-check-wrapup-copy.mjs --validate` 校验每格不超过 20 字;若报错,按提示缩短后重试。',
|
|
2387
|
-
'
|
|
2388
|
-
'
|
|
2990
|
+
'大界面改动的效果图先当候选效果图;出图后主动确认是否符合预期、是否纳入后续效果图/实现截图对比、是否按此继续实现。只有确认后,才把选定方向、整张图或其中子图整理到 `.openprd/harness/visual-reviews/` 并进入大 UI 实现。',
|
|
2991
|
+
'涉及界面、页面、视觉、样式或前端体验时,已有确认参考图才运行 openprd visual-compare . --reference <效果图> --actual <实现截图>;如果一张参考图里有多个子图/对象/网格,先运行 openprd visual-prepare . --reference <效果图> --grid <列>x<行> 或 --boxes <plan.json>,确认 contact sheet 后再逐项对比。没有明确参考图时先判断新建还是修改:新建界面走实现前 3 方向方案评审;修改既有界面则动手前先截修改前截图,完成后运行 openprd visual-compare . --before <修改前截图> --after <修改后截图>;如果并行试了多个优化方向,再补一份 openprd visual-compare . --board <parallel-board.json>。用户后续如果说“跟效果图”“不一致”“好丑”“复刻”,不能只口头说对比过了,至少产出一份 visual-compare / focus-board / parallel-board 证据图。',
|
|
2389
2992
|
'发现可沉淀项时不要中途打断任务:代码扩展识别这类白名单工具补全会自动应用并记录;用户偏好、项目协作规矩和 OpenPrd 默认行为先记录为候选,收工时运行 openprd grow . --review 集中确认。',
|
|
2390
2993
|
'维护 OpenPrd 本身且涉及配置类能力时,先判断是否应纳入 openprd grow;高置信可成长默认纳入,不确定则主动询问用户。',
|
|
2391
2994
|
'涉及后端、脚本、Agent、工具链、服务或数据处理变更时,把 CLI 与 API 视为同级接入面:同步检查命令入口、参数、输出契约、help/doctor/dry-run/status 与接口协议、返回结构、身份边界是否受影响,并更新 docs/basic/backend-structure.md 或明确写不适用原因。',
|
|
2392
|
-
'声明实现就绪前,先运行 openprd standards . --verify 和 openprd run . --verify
|
|
2995
|
+
'声明实现就绪前,先运行 openprd standards . --verify、openprd quality . --verify 和 openprd run . --verify;L2 或跨页面实现的最终回复要列出最新 HTML 质量报告和 task-scoped Markdown/HTML 测试报告路径。',
|
|
2393
2996
|
].filter(Boolean).join('\n');
|
|
2394
2997
|
}
|
|
2395
2998
|
const status = runOpenPrd(['status', '.'], cwd);
|
|
@@ -2425,7 +3028,6 @@ function shouldInjectOpenPrdContext(payload) {
|
|
|
2425
3028
|
if (
|
|
2426
3029
|
intent.simpleConcrete
|
|
2427
3030
|
&& !intent.visualMockupRequest
|
|
2428
|
-
&& !intent.largeUiChangeRequest
|
|
2429
3031
|
&& !intent.continuationRequest
|
|
2430
3032
|
&& !hasProjectPathReference
|
|
2431
3033
|
&& !intent.productCopyRequest
|
|
@@ -2557,7 +3159,7 @@ function runGateChecks(cwd, payload, risk) {
|
|
|
2557
3159
|
output: change.stdout || change.stderr,
|
|
2558
3160
|
});
|
|
2559
3161
|
}
|
|
2560
|
-
if (risk.level === 'high' && shouldRunDoctorForHighRisk(payload)) {
|
|
3162
|
+
if (risk.level === 'high' && shouldRunDoctorForHighRisk(payload) && runWorkspaceReady) {
|
|
2561
3163
|
const doctor = runOpenPrd(['doctor', '.', '--tools', 'codex', '--json'], cwd);
|
|
2562
3164
|
const doctorParsed = parseJsonOutput(doctor.stdout);
|
|
2563
3165
|
checks.push({
|
|
@@ -2580,6 +3182,83 @@ function runGateChecks(cwd, payload, risk) {
|
|
|
2580
3182
|
};
|
|
2581
3183
|
}
|
|
2582
3184
|
|
|
3185
|
+
function hasTurnReviewSignal(turnState, kind) {
|
|
3186
|
+
return Array.isArray(turnState?.reviewSignals)
|
|
3187
|
+
&& turnState.reviewSignals.some((signal) => signal?.kind === kind);
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
function parseShanghaiTimestamp(value) {
|
|
3191
|
+
const text = String(value || '').trim();
|
|
3192
|
+
if (!text) return null;
|
|
3193
|
+
const normalized = text.includes('T') ? text : text.replace(' ', 'T');
|
|
3194
|
+
const withZone = /(?:Z|[+-]\d{2}:\d{2})$/.test(normalized) ? normalized : `${normalized}+08:00`;
|
|
3195
|
+
const parsed = Date.parse(withZone);
|
|
3196
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
function collectRecentArtifactPaths(dir, startedAtMs, matcher, output = []) {
|
|
3200
|
+
if (!fs.existsSync(dir)) {
|
|
3201
|
+
return output;
|
|
3202
|
+
}
|
|
3203
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
3204
|
+
const absolutePath = path.join(dir, entry.name);
|
|
3205
|
+
if (entry.isDirectory()) {
|
|
3206
|
+
collectRecentArtifactPaths(absolutePath, startedAtMs, matcher, output);
|
|
3207
|
+
continue;
|
|
3208
|
+
}
|
|
3209
|
+
if (typeof matcher === 'function' && !matcher(absolutePath)) {
|
|
3210
|
+
continue;
|
|
3211
|
+
}
|
|
3212
|
+
const stat = fs.statSync(absolutePath);
|
|
3213
|
+
if (startedAtMs && Number.isFinite(startedAtMs) && stat.mtimeMs < startedAtMs) {
|
|
3214
|
+
continue;
|
|
3215
|
+
}
|
|
3216
|
+
output.push(absolutePath);
|
|
3217
|
+
}
|
|
3218
|
+
return output;
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
function hasRecentArtifacts(root, relativeDir, turnState, matcher) {
|
|
3222
|
+
const dir = path.join(root, relativeDir);
|
|
3223
|
+
const startedAtMs = parseShanghaiTimestamp(turnState?.startedAt);
|
|
3224
|
+
return collectRecentArtifactPaths(dir, startedAtMs, matcher, []).length > 0;
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
function closeoutVerificationReminder(root, turnState, stopIntent) {
|
|
3228
|
+
const touchedFiles = Array.isArray(turnState?.touchedFiles) ? turnState.touchedFiles : [];
|
|
3229
|
+
if (touchedFiles.length === 0) {
|
|
3230
|
+
return null;
|
|
3231
|
+
}
|
|
3232
|
+
if (stopIntent?.requirementTier !== 'l2' && !hasTurnReviewSignal(turnState, 'loop-finish')) {
|
|
3233
|
+
return null;
|
|
3234
|
+
}
|
|
3235
|
+
const codeTouched = touchedFiles.some((file) => /^(app|src|lib|components|pages|server|backend|frontend|scripts|openprd\/src)\//.test(String(file))
|
|
3236
|
+
|| /\.(js|jsx|ts|tsx|mjs|cjs|py|rb|go|java|kt|swift|css|scss|less|vue|svelte|html)$/i.test(String(file)));
|
|
3237
|
+
if (!codeTouched) {
|
|
3238
|
+
return null;
|
|
3239
|
+
}
|
|
3240
|
+
const missing = [];
|
|
3241
|
+
if (!hasTurnReviewSignal(turnState, 'quality-verify')) {
|
|
3242
|
+
missing.push('最终 `openprd quality . --verify` 与 HTML 质量报告');
|
|
3243
|
+
}
|
|
3244
|
+
if (!hasTurnReviewSignal(turnState, 'run-verify')) {
|
|
3245
|
+
missing.push('最终 `openprd run . --verify`');
|
|
3246
|
+
}
|
|
3247
|
+
const hasMarkdownReport = hasRecentArtifacts(root, '.openprd/harness/test-reports', turnState, (artifactPath) => artifactPath.endsWith('.md'));
|
|
3248
|
+
const hasHtmlReport = hasRecentArtifacts(root, '.openprd/harness/test-reports', turnState, (artifactPath) => artifactPath.endsWith('.html'));
|
|
3249
|
+
if (!hasMarkdownReport || !hasHtmlReport) {
|
|
3250
|
+
missing.push('本轮 task-scoped Markdown / HTML 测试报告');
|
|
3251
|
+
}
|
|
3252
|
+
if (missing.length === 0) {
|
|
3253
|
+
return null;
|
|
3254
|
+
}
|
|
3255
|
+
return [
|
|
3256
|
+
'OpenPrd 在本轮收工回顾里发现项目级收口证据还没补齐。',
|
|
3257
|
+
`如果你准备宣称“本次调整已经整体完成”或“项目级完成”,请先补齐:${missing.join(';')}。`,
|
|
3258
|
+
'未补齐前,只能如实说“代码或局部实现已完成,但项目级收口还没做完”,不要把缺少最终验证的状态表述成已完全闭环。',
|
|
3259
|
+
].join('\n');
|
|
3260
|
+
}
|
|
3261
|
+
|
|
2583
3262
|
function hookSuppressed(root) {
|
|
2584
3263
|
const state = readJsonSync(path.join(harnessDir(root), 'hook-state.json'), {});
|
|
2585
3264
|
const lockPath = path.join(harnessDir(root), 'input-lock.json');
|
|
@@ -2639,6 +3318,9 @@ function handle(eventName, cwd, payload) {
|
|
|
2639
3318
|
let weappGate = readNamedGate(root, 'weapp', sessionId);
|
|
2640
3319
|
const shortAffirmative = isShortAffirmativeConfirmation(prompt);
|
|
2641
3320
|
let progress = isBlockingRequirementGate(gate) ? evaluateRequirementGateProgress(root, sessionId) : null;
|
|
3321
|
+
const clarificationFollowup = isBlockingRequirementGate(gate)
|
|
3322
|
+
? looksLikeClarificationFollowupReply(prompt, intent)
|
|
3323
|
+
: false;
|
|
2642
3324
|
if (isBlockingRequirementGate(gate) && shortAffirmative) {
|
|
2643
3325
|
intent = {
|
|
2644
3326
|
...intent,
|
|
@@ -2677,7 +3359,7 @@ function handle(eventName, cwd, payload) {
|
|
|
2677
3359
|
progress = null;
|
|
2678
3360
|
}
|
|
2679
3361
|
if (isBlockingRequirementGate(gate)) {
|
|
2680
|
-
if ((intent.confirmation || shortAffirmative) && progress?.nextStep === 'clarification-confirmation-required') {
|
|
3362
|
+
if ((intent.confirmation || shortAffirmative || clarificationFollowup) && progress?.nextStep === 'clarification-confirmation-required') {
|
|
2681
3363
|
gate = holdRequirementGate(root, prompt, progress, sessionId, {
|
|
2682
3364
|
status: 'clarification-confirmed',
|
|
2683
3365
|
clarificationConfirmedAt: now(),
|
|
@@ -2755,6 +3437,12 @@ function handle(eventName, cwd, payload) {
|
|
|
2755
3437
|
if (sessionId && isAllowedDuringRequirementGate(root, payload, gate, sessionId)) {
|
|
2756
3438
|
mirrorRequirementGate(root, sessionId);
|
|
2757
3439
|
}
|
|
3440
|
+
const authorizedReviewMutation = /openprd\s+review\b/i.test(commandText(payload))
|
|
3441
|
+
&& /--mark\b/i.test(commandText(payload))
|
|
3442
|
+
&& (
|
|
3443
|
+
isAuthorizedReviewMarkCommand(payload, gate)
|
|
3444
|
+
|| isSilentRecordReviewMarkCommand(payload, gate, progress)
|
|
3445
|
+
);
|
|
2758
3446
|
if (skillGate?.active && looksLikeSkillContractMutation(root, payload)) {
|
|
2759
3447
|
appendEvent(root, { ...baseEvent, outcome: 'blocked-skill-visualization-gate' });
|
|
2760
3448
|
recordRunHook(root, baseEvent, 'blocked-skill-visualization-gate');
|
|
@@ -2764,7 +3452,7 @@ function handle(eventName, cwd, payload) {
|
|
|
2764
3452
|
'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.',
|
|
2765
3453
|
].join('\n'));
|
|
2766
3454
|
}
|
|
2767
|
-
if (researchGate?.active && isMutationPayload(payload, risk)) {
|
|
3455
|
+
if (researchGate?.active && isMutationPayload(payload, risk) && !authorizedReviewMutation) {
|
|
2768
3456
|
appendEvent(root, { ...baseEvent, outcome: 'blocked-research-gate' });
|
|
2769
3457
|
recordRunHook(root, baseEvent, 'blocked-research-gate');
|
|
2770
3458
|
updateHookState(root, baseEvent);
|
|
@@ -2808,7 +3496,13 @@ function handle(eventName, cwd, payload) {
|
|
|
2808
3496
|
progress?.nextStep === 'implementation-ready'
|
|
2809
3497
|
? 'Do not edit implementation files until the user clearly asks to execute this reviewed requirement.'
|
|
2810
3498
|
: writePathMutation && progress?.nextStep === 'clarification-confirmation-required'
|
|
2811
|
-
? 'Do not write requirement facts, classify, or synthesize yet. First summarize the requirement in chat using 需求判断 / 需求理解 / 功能范围 / 技术方案, prefer Markdown tables for 功能范围 and 技术方案, wait for the user to confirm that summary, then continue the requirement write path.'
|
|
3499
|
+
? 'Do not write requirement facts, classify, or synthesize yet. First summarize the requirement in chat using 需求判断 / 需求理解 / 功能范围 / 技术方案, prefer Markdown tables for 功能范围 and 技术方案, use light lead sentences for 需求判断 and 需求理解 before sinking details into later bullets or tables, and if this is still a 0-to-1 or worth-doing discussion also surface 第一批最容易触达的社区或种子用户、当前替代方案、先怎么手工交付、什么承诺才算真需求、最低成本先验证什么、以及验证阶段怎样先活下来; wait for the user to confirm that summary, then continue the requirement write path.'
|
|
3500
|
+
: writePathMutation && progress?.nextStep === 'clarification-writeback-required'
|
|
3501
|
+
? 'Do not synthesize yet. The user has already confirmed the current summary or default choice, but current.json still lacks canonical write-back for this lane. Write the confirmed facts back with openprd capture using canonical field paths and source user-confirmed; do not fall back to generic clarify, and do not keep writing agent-inferred / project-derived clarification fields.'
|
|
3502
|
+
: writePathMutation && progress?.nextStep === 'product-type-classification-required'
|
|
3503
|
+
? 'Do not synthesize yet. The current lane still lacks a confirmed product-type classification. Run openprd classify . so the lane scenario is locked before you synthesize.'
|
|
3504
|
+
: writePathMutation && progress?.nextStep === 'requirement-interview-required'
|
|
3505
|
+
? 'Do not synthesize yet. The current lane still lacks required requirement facts. Fill the missing canonical fields with openprd capture, and only use openprd interview . to surface the remaining gaps before you synthesize.'
|
|
2812
3506
|
: writePathMutation && progress?.nextStep === 'prd-synthesis-required'
|
|
2813
3507
|
? 'You may continue the requirement write path only within the confirmed summary: write back confirmed facts, classify if needed, synthesize the PRD, then proceed to review/change/tasks.'
|
|
2814
3508
|
: progress?.nextStep === 'review-recording-required'
|
|
@@ -2816,13 +3510,72 @@ function handle(eventName, cwd, payload) {
|
|
|
2816
3510
|
: 'Do not edit implementation files until the active approval policy is satisfied and the OpenPrd change has generated tasks.',
|
|
2817
3511
|
silentRecord
|
|
2818
3512
|
? 'Decision-point order: because the user explicitly waived any confirmation stop, you may skip requirement-summary confirmation, write back requirement facts, synthesize the PRD, record the exact stable review artifact, generate the OpenPrd change, prepare the task breakdown, then implement within the confirmed scope.'
|
|
2819
|
-
: 'Decision-point order: clarify the requirement, summarize it in chat using 需求判断 / 需求理解 / 功能范围 / 技术方案, prefer Markdown tables for 功能范围 and 技术方案, wait for the user to confirm that requirement summary, write back only confirmed facts, synthesize the PRD, wait for a human review decision on the stable artifact, generate the OpenPrd change, prepare the task breakdown, then implement within the confirmed scope.',
|
|
3513
|
+
: 'Decision-point order: clarify the requirement, summarize it in chat using 需求判断 / 需求理解 / 功能范围 / 技术方案, prefer Markdown tables for 功能范围 and 技术方案, and if the ask still looks like 0-to-1 validation also surface 第一批最容易触达的社区或种子用户、当前替代方案、先怎么手工交付、什么承诺才算真需求、最低成本先验证什么、以及验证阶段怎样先活下来; wait for the user to confirm that requirement summary, write back only confirmed facts, synthesize the PRD, wait for a human review decision on the stable artifact, generate the OpenPrd change, prepare the task breakdown, then implement within the confirmed scope.',
|
|
2820
3514
|
].join('\n');
|
|
2821
3515
|
appendEvent(root, { ...baseEvent, outcome: 'blocked-requirement-intake' });
|
|
2822
3516
|
recordRunHook(root, baseEvent, 'blocked-requirement-intake');
|
|
2823
3517
|
updateHookState(root, baseEvent);
|
|
2824
3518
|
return blockHook(reason);
|
|
2825
3519
|
}
|
|
3520
|
+
let patchModeGate = syncPatchModeGate(root, payload, sessionId, turnIntent);
|
|
3521
|
+
if (patchModeGate?.active) {
|
|
3522
|
+
if (isPatchModeWriteAttempt(root, payload, patchModeGate)) {
|
|
3523
|
+
patchModeGate = updateNamedGate(root, 'patch-mode', {
|
|
3524
|
+
status: 'write-attempted',
|
|
3525
|
+
lastWriteAttemptAt: now(),
|
|
3526
|
+
lastWriteAttemptAtMs: Date.now(),
|
|
3527
|
+
lastWriteAttemptTool: toolName(payload) || null,
|
|
3528
|
+
lastWriteAttemptFiles: extractTouchedFiles(root, payload),
|
|
3529
|
+
}, sessionId) || patchModeGate;
|
|
3530
|
+
} else if (patchModeGate.phase === 'handoff' && isPatchModeFocusAttempt(root, payload, patchModeGate)) {
|
|
3531
|
+
const nextRemaining = Math.max(0, Number(patchModeGate.focusAllowanceRemaining ?? PATCH_MODE_HANDOFF_FOCUS_ALLOWANCE) - 1);
|
|
3532
|
+
patchModeGate = updateNamedGate(root, 'patch-mode', {
|
|
3533
|
+
status: nextRemaining > 0 ? 'handoff-focus-read' : 'handoff-awaiting-entry-write',
|
|
3534
|
+
focusAllowanceRemaining: nextRemaining,
|
|
3535
|
+
lastFocusAt: now(),
|
|
3536
|
+
lastFocusAtMs: Date.now(),
|
|
3537
|
+
lastFocusTool: toolName(payload) || null,
|
|
3538
|
+
lastFocusFiles: extractTouchedFiles(root, payload),
|
|
3539
|
+
}, sessionId) || patchModeGate;
|
|
3540
|
+
} else {
|
|
3541
|
+
appendEvent(root, {
|
|
3542
|
+
...baseEvent,
|
|
3543
|
+
outcome: 'blocked-patch-mode-hover',
|
|
3544
|
+
targetFile: patchModeGate.targetFile,
|
|
3545
|
+
});
|
|
3546
|
+
recordRunHook(root, baseEvent, 'blocked-patch-mode-hover');
|
|
3547
|
+
updateHookState(root, baseEvent);
|
|
3548
|
+
if (patchModeGate.phase === 'handoff') {
|
|
3549
|
+
return blockHook([
|
|
3550
|
+
'OpenPrd blocked a post-starter hover because Patch Mode handoff is already active.',
|
|
3551
|
+
`design-starter has already landed ${patchModeGate.targetFile} and the active design contracts. You may do at most one immediate orientation pass over the generated entry and the active contracts, then the next action must be a real write to that entry file or one of its sibling drafts.`,
|
|
3552
|
+
`Remaining orientation reads: ${Math.max(0, Number(patchModeGate.focusAllowanceRemaining ?? 0))}.`,
|
|
3553
|
+
`Allowed orientation files: ${patchModeFocusFiles(root, patchModeGate).join(', ')}.`,
|
|
3554
|
+
`Allowed write files: ${patchModeTargetFiles(patchModeGate).join(', ')}.`,
|
|
3555
|
+
'Do not go back to web search, docs/basic, template scans, or another shell-only loop after starter output has landed.',
|
|
3556
|
+
].join('\n'));
|
|
3557
|
+
}
|
|
3558
|
+
return blockHook([
|
|
3559
|
+
'OpenPrd blocked a non-writing tool call because Patch Mode is already in the entry-overwrite stage.',
|
|
3560
|
+
`You already announced an entry rewrite/overwrite step for ${patchModeGate.targetFile}. The very next action must be a real write to that entry file or one of its sibling drafts, not more reading, scanning, image handling, or shell exploration.`,
|
|
3561
|
+
`Allowed next files: ${patchModeTargetFiles(patchModeGate).join(', ')}.`,
|
|
3562
|
+
'If the user gave a mockup or reference image, keep it as the style judge, but apply that judgment inside the actual page write instead of pausing in another read-only loop.',
|
|
3563
|
+
].join('\n'));
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
const frontendTargets = frontendImplementationTargets(root, payload);
|
|
3567
|
+
const designIssues = frontendDesignPreflightIssues(root, turnIntent, frontendTargets);
|
|
3568
|
+
if (designIssues.length > 0) {
|
|
3569
|
+
appendEvent(root, { ...baseEvent, outcome: 'blocked-frontend-design-preflight' });
|
|
3570
|
+
recordRunHook(root, baseEvent, 'blocked-frontend-design-preflight');
|
|
3571
|
+
updateHookState(root, baseEvent);
|
|
3572
|
+
return blockHook([
|
|
3573
|
+
'OpenPrd blocked a frontend implementation write because the design-preflight contract is still incomplete.',
|
|
3574
|
+
frontendTargets.length > 0 ? `Trying to edit: ${frontendTargets.join(', ')}.` : null,
|
|
3575
|
+
'Before touching implementation files, update the required files under `.openprd/design/active/` and remove every `待填写` placeholder. If something truly does not apply, write `不适用` or a clear gap note instead of leaving the template unchanged.',
|
|
3576
|
+
...designIssues.map((issue) => `- ${issue.file}: ${issue.reason}`),
|
|
3577
|
+
].filter(Boolean).join('\n'));
|
|
3578
|
+
}
|
|
2826
3579
|
if (turnIntent.browserSafetyRequest && isHighRiskBrowserAction(payload)) {
|
|
2827
3580
|
appendEvent(root, { ...baseEvent, outcome: 'browser-safety-reminder' });
|
|
2828
3581
|
recordRunHook(root, baseEvent, 'browser-safety-reminder');
|
|
@@ -2845,7 +3598,7 @@ function handle(eventName, cwd, payload) {
|
|
|
2845
3598
|
recordRunHook(root, baseEvent, 'allowed-medium-risk');
|
|
2846
3599
|
updateHookState(root, baseEvent);
|
|
2847
3600
|
recordTouchedFiles(root, payload);
|
|
2848
|
-
return allowHook('OpenPrd 检测到写入动作。本轮写入完成后、最终回复前,请针对实际 touched code files 运行 openprd dev-check . <file...>;如出现需要关注的文件,最终回复必须以 **后续建议** 为标题,直接复用 dev-check 生成的 Markdown 表格,说明影响对象、关注程度、规模信号、预警原因、本次处理结果和后续建议,并按 🔴 → 🟠 → 🟡 排序;不要把“关注程度”列改写成纯 emoji,必须保留例如“🟠 中风险|建议优先关注”这类完整标签;如果你改写了“预警原因 / 本次处理结果 / 后续建议”,先用 `node scripts/dev-check-wrapup-copy.mjs --validate` 校验每格不超过 20 字;若报错,按提示缩短后重试;如涉及界面视觉且已有参考效果图并进入实现阶段,阶段性完成后运行 openprd visual-compare . --reference <效果图> --actual <实现截图> 并查看 JPG
|
|
3601
|
+
return allowHook('OpenPrd 检测到写入动作。本轮写入完成后、最终回复前,请针对实际 touched code files 运行 openprd dev-check . <file...>;如出现需要关注的文件,最终回复必须以 **后续建议** 为标题,直接复用 dev-check 生成的 Markdown 表格,说明影响对象、关注程度、规模信号、预警原因、本次处理结果和后续建议,并按 🔴 → 🟠 → 🟡 排序;不要把“关注程度”列改写成纯 emoji,必须保留例如“🟠 中风险|建议优先关注”这类完整标签;如果你改写了“预警原因 / 本次处理结果 / 后续建议”,先用 `node scripts/dev-check-wrapup-copy.mjs --validate` 校验每格不超过 20 字;若报错,按提示缩短后重试;如涉及界面视觉且已有参考效果图并进入实现阶段,阶段性完成后运行 openprd visual-compare . --reference <效果图> --actual <实现截图> 并查看 JPG 对比图;如果参考图来自本轮或前序 `imagegen`,先确认用户已接受它作为后续对比参考;若一张图里有多个子图、对象或网格,先运行 openprd visual-prepare . --reference <效果图> --grid <列>x<行> 或 --boxes <plan.json>,确认 contact sheet 后再逐项对比。若局部细节更重要,再补 openprd visual-compare . --board <focus-board.json>;如无参考图,先判断新建界面还是修改既有界面:新建界面应先完成 3 方向方案评审,修改既有界面确认已先截修改前截图,并在完成后运行 openprd visual-compare . --before <修改前截图> --after <修改后截图> 查看 JPG 自检图;若并行试了多个优化方向,再补 openprd visual-compare . --board <parallel-board.json>;当用户说“跟效果图”“不一致”“好丑”“复刻”时,至少给出一份视觉证据图,不要只口头判断。发现可沉淀项时不要中途打断任务,代码扩展识别这类白名单工具补全会自动应用并记录,用户偏好、项目协作规矩和 OpenPrd 默认行为留到收工时用 openprd grow . --review 集中确认;维护 OpenPrd 本身且涉及配置类能力时,先判断是否应纳入 openprd grow;声明就绪前,请同步维护 docs/basic、文件说明书、文件夹 README,以及相关 OpenPrd change/task 状态;如果涉及后端、脚本、Agent、工具链、服务或数据处理变更,还要把 CLI 与 API 视为同级接入面并更新 docs/basic/backend-structure.md。');
|
|
2849
3602
|
}
|
|
2850
3603
|
return allowHook();
|
|
2851
3604
|
}
|
|
@@ -2886,12 +3639,39 @@ function handle(eventName, cwd, payload) {
|
|
|
2886
3639
|
return allowHook([
|
|
2887
3640
|
'OpenPrd 生图事实对齐提醒:如果这轮要汇报图片结果、失败或限流,请确保它来自一次实际的 `imagegen` 调用。',
|
|
2888
3641
|
'未实际调用 `imagegen` 前,不要声称“生图限流”“生图失败”或“已经生成图片结果”;若本轮还没调用,请如实说明仍未开始或尚未完成生图。',
|
|
3642
|
+
'如果这轮确实已经生图,不要默认把结果登记到 `.openprd/harness/visual-reviews/` 或直接当成验收参考;先把它当候选效果图。',
|
|
3643
|
+
'若用户还要继续做实现,主动追问三件事:是否符合预期、是否纳入后续效果图/实现截图对比、是否按此继续后续实现;未确认前不要自动进入实现,也不要声称参考图已定。',
|
|
2889
3644
|
].join('\n'));
|
|
2890
3645
|
}
|
|
3646
|
+
const patchModeGate = readNamedGate(root, 'patch-mode', sessionId);
|
|
3647
|
+
if (patchModeGate?.active) {
|
|
3648
|
+
if (isPatchModeWriteObserved(root, patchModeGate)) {
|
|
3649
|
+
closePatchModeGate(root, sessionId, {
|
|
3650
|
+
status: 'write-observed',
|
|
3651
|
+
writeObservedAt: now(),
|
|
3652
|
+
});
|
|
3653
|
+
} else if (patchModeGate.phase === 'handoff') {
|
|
3654
|
+
return allowHook([
|
|
3655
|
+
'OpenPrd 在本轮收工回顾里发现 design-starter 已经落地,但 Patch Mode handoff 还没真正收口。',
|
|
3656
|
+
`当前应先把后续真实页面继续写到 ${patchModeGate.targetFile} 或它的 sibling draft(${(patchModeGate.draftFiles || []).join(', ')})之一,再继续汇报完成。`,
|
|
3657
|
+
`如果只是读了入口文件或 active design contracts,那还只算就地对焦,不算真正落盘;starter 之后不要再回头搜网页、翻 docs/basic 或继续模板漫游。`,
|
|
3658
|
+
].join('\n'));
|
|
3659
|
+
} else {
|
|
3660
|
+
return allowHook([
|
|
3661
|
+
'OpenPrd 在本轮收工回顾里发现 Patch Mode 还停在“已宣布开始覆盖,但入口文件还没真正落盘”的状态。',
|
|
3662
|
+
`当前应先把真实写入动作落到 ${patchModeGate.targetFile} 或它的 sibling draft(${(patchModeGate.draftFiles || []).join(', ')})之一,再继续汇报完成。`,
|
|
3663
|
+
'如果用户给了效果图或参考图,继续以它为准,但这个判断必须落实在实际页面写入里;只补说明、只补合同、只下载素材或只说“准备开始改”都不算完成。',
|
|
3664
|
+
].join('\n'));
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
2891
3667
|
const devCheckMessage = devCheckWrapUpMessage(root, turnState);
|
|
2892
3668
|
if (devCheckMessage) {
|
|
2893
3669
|
return allowHook(devCheckMessage);
|
|
2894
3670
|
}
|
|
3671
|
+
const closeoutReminder = closeoutVerificationReminder(root, turnState, stopIntent);
|
|
3672
|
+
if (closeoutReminder) {
|
|
3673
|
+
return allowHook(closeoutReminder);
|
|
3674
|
+
}
|
|
2895
3675
|
if (Array.isArray(turnState.touchedFiles) && turnState.touchedFiles.length > 0) {
|
|
2896
3676
|
const review = runOpenPrd(['quality', '.', '--learn', '--review', '--from', '.openprd/harness/turn-state.json', '--json'], root);
|
|
2897
3677
|
if (review.ok) {
|
|
@@ -2902,6 +3682,7 @@ function handle(eventName, cwd, payload) {
|
|
|
2902
3682
|
&& !parsedReview.skipped
|
|
2903
3683
|
&& parsedReview.ok !== false
|
|
2904
3684
|
&& parsedReview.candidateId
|
|
3685
|
+
&& parsedReview.status === 'pending-review'
|
|
2905
3686
|
&& parsedReview.candidateId !== turnState.lastKnowledgePromptCandidateId
|
|
2906
3687
|
) {
|
|
2907
3688
|
writeTurnState(root, {
|
|
@@ -2909,14 +3690,26 @@ function handle(eventName, cwd, payload) {
|
|
|
2909
3690
|
lastKnowledgePromptCandidateId: parsedReview.candidateId,
|
|
2910
3691
|
lastKnowledgePromptAt: now(),
|
|
2911
3692
|
});
|
|
3693
|
+
const userFacingMessage = String(parsedReview.userFacingExperience?.message || '').trim();
|
|
2912
3694
|
return allowHook([
|
|
2913
|
-
'OpenPrd
|
|
2914
|
-
|
|
2915
|
-
|
|
3695
|
+
'OpenPrd 在本轮收工回顾里发现了一条适合沉淀为当前项目经验的内容。',
|
|
3696
|
+
'请在最终回复结尾主动用下面这段结构化人话询问用户,不要改成内部术语,也不要提 Draft Skill、candidate、promote、路径或命令:',
|
|
3697
|
+
userFacingMessage || [
|
|
3698
|
+
'这次我观察到一个以后可能重复出现的情况:',
|
|
3699
|
+
'这次任务里已经形成了一种以后可能重复出现的处理方式。',
|
|
3700
|
+
'',
|
|
3701
|
+
'我计划保留一条项目经验:',
|
|
3702
|
+
'以后再遇到类似任务时,我会优先复用这次已经验证过的处理顺序和注意事项。',
|
|
3703
|
+
'',
|
|
3704
|
+
'以后如果再遇到类似任务,我会优先按这套经验来处理,减少重复解释和重复判断。',
|
|
3705
|
+
'这条经验只会保留在当前项目里。',
|
|
3706
|
+
'要我把它一起保留下来吗?',
|
|
3707
|
+
].join('\n'),
|
|
2916
3708
|
parsedReview.suggestedLearnCommand
|
|
2917
|
-
?
|
|
3709
|
+
? `如果用户明确同意,再执行:${parsedReview.suggestedLearnCommand}`
|
|
2918
3710
|
: null,
|
|
2919
|
-
|
|
3711
|
+
`如果用户明确表示暂不保留,执行:openprd knowledge reject --path . --id ${parsedReview.candidateId} --reason "<用户原话>"`,
|
|
3712
|
+
`如果用户表示先不处理、以后再说,执行:openprd knowledge archive --path . --id ${parsedReview.candidateId} --reason "<用户原话>"`,
|
|
2920
3713
|
].filter(Boolean).join('\n'));
|
|
2921
3714
|
}
|
|
2922
3715
|
} catch {}
|