@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,1470 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { appendJsonl, appendText, cjoin, exists, readJson, readJsonl, writeJson, writeText } from './fs-utils.js';
|
|
3
|
+
import { OPENPRD_HARNESS_TURN_STATE, recordKnowledgeReviewSignal, reviewKnowledgeWorkspace } from './knowledge.js';
|
|
4
|
+
import { readSessionBinding } from './session-binding.js';
|
|
5
|
+
import { timestamp } from './time.js';
|
|
6
|
+
|
|
7
|
+
const OPENPRD_HARNESS_DIR = cjoin('.openprd', 'harness');
|
|
8
|
+
const OPENPRD_HARNESS_RUN_STATE = cjoin(OPENPRD_HARNESS_DIR, 'run-state.json');
|
|
9
|
+
const OPENPRD_HARNESS_ITERATIONS = cjoin(OPENPRD_HARNESS_DIR, 'iterations.jsonl');
|
|
10
|
+
const OPENPRD_HARNESS_LEARNINGS = cjoin(OPENPRD_HARNESS_DIR, 'learnings.md');
|
|
11
|
+
const OPENPRD_HARNESS_LOOP_FEATURE_LIST = cjoin(OPENPRD_HARNESS_DIR, 'feature-list.json');
|
|
12
|
+
const OPENPRD_HARNESS_REQUIREMENT_GATE = cjoin(OPENPRD_HARNESS_DIR, 'requirement-gate.json');
|
|
13
|
+
const OPENPRD_HARNESS_REQUIREMENT_GATES_DIR = cjoin(OPENPRD_HARNESS_DIR, 'requirement-gates');
|
|
14
|
+
const OPENPRD_HARNESS_EVENTS = cjoin(OPENPRD_HARNESS_DIR, 'events.jsonl');
|
|
15
|
+
const OPENPRD_WORK_UNITS_DIR = cjoin('.openprd', 'engagements', 'work-units');
|
|
16
|
+
const OPENPRD_LOOP_REQUIRED_IMPLEMENTATION_TASK_THRESHOLD = 10;
|
|
17
|
+
const CONTINUATION_SESSION_PATTERN = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i;
|
|
18
|
+
const CONTINUATION_TASK_HANDLE_PATTERN = /\b[a-z0-9._-]+:T\d{3}\.\d{2}:[a-z0-9._-]+\b/i;
|
|
19
|
+
const CONTINUATION_WORK_UNIT_PATTERN = /\bwu-[a-z0-9._-]+\b/i;
|
|
20
|
+
const CONTINUATION_EXPLICIT_PATTERN = /(继续(这个|这条|当前)?(对话|任务|会话|记录|历史)?|续做|接着做|继续执行|继续推进)/i;
|
|
21
|
+
const CONTINUATION_CURRENT_PATTERN = /(继续当前|当前(这个|这条)?(任务|会话|记录|需求|变更)|current\s+(task|change|session)|resume current)/i;
|
|
22
|
+
function harnessFile(projectRoot, relativePath) {
|
|
23
|
+
return cjoin(projectRoot, relativePath);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function ensureRunHarness(projectRoot) {
|
|
27
|
+
await fs.mkdir(harnessFile(projectRoot, OPENPRD_HARNESS_DIR), { recursive: true });
|
|
28
|
+
const statePath = harnessFile(projectRoot, OPENPRD_HARNESS_RUN_STATE);
|
|
29
|
+
if (!(await exists(statePath))) {
|
|
30
|
+
await writeJson(statePath, {
|
|
31
|
+
version: 1,
|
|
32
|
+
active: true,
|
|
33
|
+
currentIteration: 0,
|
|
34
|
+
lastContextAt: null,
|
|
35
|
+
lastHookAt: null,
|
|
36
|
+
lastOutcome: null,
|
|
37
|
+
lastRecommendation: null,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const iterationsPath = harnessFile(projectRoot, OPENPRD_HARNESS_ITERATIONS);
|
|
41
|
+
if (!(await exists(iterationsPath))) {
|
|
42
|
+
await writeText(iterationsPath, '');
|
|
43
|
+
}
|
|
44
|
+
const learningsPath = harnessFile(projectRoot, OPENPRD_HARNESS_LEARNINGS);
|
|
45
|
+
if (!(await exists(learningsPath))) {
|
|
46
|
+
await writeText(learningsPath, '# OpenPrd Harness Learnings\n\nReusable patterns discovered during hook-driven runs belong here.\n');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function readRunState(projectRoot) {
|
|
51
|
+
await ensureRunHarness(projectRoot);
|
|
52
|
+
return readJson(harnessFile(projectRoot, OPENPRD_HARNESS_RUN_STATE)).catch(() => ({
|
|
53
|
+
version: 1,
|
|
54
|
+
active: true,
|
|
55
|
+
currentIteration: 0,
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function readActiveRequirementGate(projectRoot) {
|
|
60
|
+
const gate = await readJson(harnessFile(projectRoot, OPENPRD_HARNESS_REQUIREMENT_GATE)).catch(() => null);
|
|
61
|
+
return gate?.active ? gate : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function writeRunState(projectRoot, state) {
|
|
65
|
+
await writeJson(harnessFile(projectRoot, OPENPRD_HARNESS_RUN_STATE), {
|
|
66
|
+
version: 1,
|
|
67
|
+
active: true,
|
|
68
|
+
...state,
|
|
69
|
+
updatedAt: timestamp(),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function compactTask(task) {
|
|
74
|
+
if (!task) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
id: task.id,
|
|
79
|
+
taskHandle: task.taskHandle ?? null,
|
|
80
|
+
title: task.title,
|
|
81
|
+
relativePath: task.relativePath,
|
|
82
|
+
lineNumber: task.lineNumber,
|
|
83
|
+
verify: task.metadata?.verify ?? null,
|
|
84
|
+
done: task.metadata?.done ?? null,
|
|
85
|
+
oracle: task.metadata?.oracle ?? null,
|
|
86
|
+
deps: task.metadata?.deps ?? null,
|
|
87
|
+
type: task.metadata?.type ?? task.metadata?.category ?? task.metadata?.kind ?? null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function compactCoverageItem(item) {
|
|
92
|
+
if (!item) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
id: item.id,
|
|
97
|
+
title: item.title,
|
|
98
|
+
status: item.status,
|
|
99
|
+
source: item.source ?? null,
|
|
100
|
+
evidence: item.evidence ?? [],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function shellQuote(value) {
|
|
105
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function slugify(value, fallback = 'openprd-generated-change') {
|
|
109
|
+
const slug = String(value ?? '')
|
|
110
|
+
.toLowerCase()
|
|
111
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
112
|
+
.replace(/^-+|-+$/g, '')
|
|
113
|
+
.slice(0, 80);
|
|
114
|
+
return slug || fallback;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function reviewMarkCommand(snapshot) {
|
|
118
|
+
if (!snapshot?.versionId || !snapshot?.digest) {
|
|
119
|
+
return 'openprd review . --mark confirmed';
|
|
120
|
+
}
|
|
121
|
+
const parts = [
|
|
122
|
+
'openprd review . --mark confirmed',
|
|
123
|
+
`--version ${shellQuote(snapshot.versionId)}`,
|
|
124
|
+
`--digest ${shellQuote(snapshot.digest)}`,
|
|
125
|
+
];
|
|
126
|
+
if (snapshot.workUnitId) {
|
|
127
|
+
parts.push(`--work-unit ${shellQuote(snapshot.workUnitId)}`);
|
|
128
|
+
}
|
|
129
|
+
return parts.join(' ');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function executionGate() {
|
|
133
|
+
return {
|
|
134
|
+
requiresExplicitIntent: true,
|
|
135
|
+
allowedIntents: ['开发', '实现', '修复', '继续任务', '落地执行', '深度调研', '深度对标', '复刻落地', '提交'],
|
|
136
|
+
readOnlyIntents: ['看看', '规划', '梳理', '分析', '评估', '预计动哪些文件', '怎么改', '代码审查'],
|
|
137
|
+
rule: '只有当用户当前明确要求实现、继续、深度调研、对标或提交时,才运行 executionCommand。规划、分析、文件影响范围和审查类请求保持只读,并基于证据回答。',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function analyzeRunMessage(message = null) {
|
|
142
|
+
const text = String(message ?? '').trim();
|
|
143
|
+
if (!text) {
|
|
144
|
+
return {
|
|
145
|
+
kind: 'default',
|
|
146
|
+
requested: false,
|
|
147
|
+
explicit: false,
|
|
148
|
+
selectorType: null,
|
|
149
|
+
selector: null,
|
|
150
|
+
sessionId: null,
|
|
151
|
+
taskHandle: null,
|
|
152
|
+
workUnitId: null,
|
|
153
|
+
explicitCurrent: false,
|
|
154
|
+
text: '',
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const sessionId = text.match(CONTINUATION_SESSION_PATTERN)?.[0] ?? null;
|
|
159
|
+
const taskHandle = text.match(CONTINUATION_TASK_HANDLE_PATTERN)?.[0] ?? null;
|
|
160
|
+
const workUnitId = text.match(CONTINUATION_WORK_UNIT_PATTERN)?.[0] ?? null;
|
|
161
|
+
const explicit = CONTINUATION_EXPLICIT_PATTERN.test(text);
|
|
162
|
+
const explicitCurrent = CONTINUATION_CURRENT_PATTERN.test(text);
|
|
163
|
+
const requested = explicit || Boolean(sessionId || taskHandle || workUnitId);
|
|
164
|
+
if (!requested) {
|
|
165
|
+
return {
|
|
166
|
+
kind: 'default',
|
|
167
|
+
requested: false,
|
|
168
|
+
explicit: false,
|
|
169
|
+
selectorType: null,
|
|
170
|
+
selector: null,
|
|
171
|
+
sessionId: null,
|
|
172
|
+
taskHandle: null,
|
|
173
|
+
workUnitId: null,
|
|
174
|
+
explicitCurrent,
|
|
175
|
+
text,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
kind: 'continuation',
|
|
181
|
+
requested: true,
|
|
182
|
+
explicit,
|
|
183
|
+
selectorType: taskHandle ? 'task-handle' : workUnitId ? 'work-unit' : sessionId ? 'session' : 'implicit',
|
|
184
|
+
selector: taskHandle ?? workUnitId ?? sessionId ?? null,
|
|
185
|
+
sessionId,
|
|
186
|
+
taskHandle,
|
|
187
|
+
workUnitId,
|
|
188
|
+
explicitCurrent,
|
|
189
|
+
text,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function normalizeSearchText(value) {
|
|
194
|
+
return String(value ?? '')
|
|
195
|
+
.normalize('NFKC')
|
|
196
|
+
.toLowerCase()
|
|
197
|
+
.replace(/[_:/.-]+/g, ' ')
|
|
198
|
+
.replace(/[^\p{Letter}\p{Number}\u4e00-\u9fff]+/gu, ' ')
|
|
199
|
+
.replace(/\s+/g, ' ')
|
|
200
|
+
.trim();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function uniqueItems(items) {
|
|
204
|
+
return [...new Set(items.filter(Boolean))];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function searchTokens(value) {
|
|
208
|
+
const normalized = normalizeSearchText(value);
|
|
209
|
+
if (!normalized) {
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
return uniqueItems(
|
|
213
|
+
normalized
|
|
214
|
+
.split(' ')
|
|
215
|
+
.map((item) => item.trim())
|
|
216
|
+
.filter((item) => item.length >= 2),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function scoreSearchCandidate(query, fields = []) {
|
|
221
|
+
const normalizedQuery = normalizeSearchText(query);
|
|
222
|
+
if (!normalizedQuery) {
|
|
223
|
+
return 0;
|
|
224
|
+
}
|
|
225
|
+
const queryTokens = searchTokens(query);
|
|
226
|
+
let bestScore = 0;
|
|
227
|
+
for (const field of fields) {
|
|
228
|
+
const normalizedField = normalizeSearchText(field);
|
|
229
|
+
if (!normalizedField) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (normalizedField === normalizedQuery) {
|
|
233
|
+
bestScore = Math.max(bestScore, 220);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (normalizedField.includes(normalizedQuery)) {
|
|
237
|
+
bestScore = Math.max(bestScore, 180);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (normalizedQuery.includes(normalizedField) && normalizedField.length >= 6) {
|
|
241
|
+
bestScore = Math.max(bestScore, 160);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
let score = 0;
|
|
245
|
+
let hits = 0;
|
|
246
|
+
for (const token of queryTokens) {
|
|
247
|
+
if (!normalizedField.includes(token)) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
hits += 1;
|
|
251
|
+
score += token.length >= 4 ? 24 : 14;
|
|
252
|
+
}
|
|
253
|
+
if (hits > 0) {
|
|
254
|
+
if (hits === queryTokens.length && queryTokens.length > 1) {
|
|
255
|
+
score += 24;
|
|
256
|
+
}
|
|
257
|
+
if (normalizedField.includes(normalizedQuery.slice(0, Math.min(normalizedQuery.length, 12)))) {
|
|
258
|
+
score += 12;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
bestScore = Math.max(bestScore, score);
|
|
262
|
+
}
|
|
263
|
+
return bestScore;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function readFirstHeading(filePath, fallback = null) {
|
|
267
|
+
const text = await fs.readFile(filePath, 'utf8').catch(() => '');
|
|
268
|
+
const heading = text.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? null;
|
|
269
|
+
return heading || fallback;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function workUnitBindingPath(projectRoot, workUnitId) {
|
|
273
|
+
if (!workUnitId) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
return cjoin(
|
|
277
|
+
projectRoot,
|
|
278
|
+
OPENPRD_WORK_UNITS_DIR,
|
|
279
|
+
`${String(workUnitId).replace(/[^A-Za-z0-9._-]/g, '_')}.json`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function readWorkUnitBindingRecord(projectRoot, workUnitId) {
|
|
284
|
+
const filePath = workUnitBindingPath(projectRoot, workUnitId);
|
|
285
|
+
if (!filePath) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
const binding = await readJson(filePath).catch(() => null);
|
|
289
|
+
return binding ? { ...binding, path: filePath } : null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function extractFirstSelectorMatch(texts, pattern) {
|
|
293
|
+
for (const text of texts) {
|
|
294
|
+
const match = String(text ?? '').match(pattern)?.[0] ?? null;
|
|
295
|
+
if (match) {
|
|
296
|
+
return match;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function buildRunResolutionIndex(projectRoot, changes, listOpenSpecTaskWorkspace) {
|
|
303
|
+
const changeRows = Array.isArray(changes?.changes) ? changes.changes : [];
|
|
304
|
+
const index = {
|
|
305
|
+
changes: [],
|
|
306
|
+
tasks: [],
|
|
307
|
+
};
|
|
308
|
+
for (const change of changeRows) {
|
|
309
|
+
const title = await readFirstHeading(cjoin(change.changeDir, 'proposal.md'), change.id);
|
|
310
|
+
const taskState = await listOpenSpecTaskWorkspace(projectRoot, { change: change.id }).catch(() => null);
|
|
311
|
+
const pendingTaskTitles = Array.isArray(taskState?.tasks)
|
|
312
|
+
? taskState.tasks
|
|
313
|
+
.filter((task) => !task.checked)
|
|
314
|
+
.map((task) => String(task.title ?? '').trim())
|
|
315
|
+
.filter(Boolean)
|
|
316
|
+
.slice(0, 3)
|
|
317
|
+
: [];
|
|
318
|
+
index.changes.push({
|
|
319
|
+
changeId: change.id,
|
|
320
|
+
title,
|
|
321
|
+
active: Boolean(change.active),
|
|
322
|
+
pendingTaskTitles,
|
|
323
|
+
});
|
|
324
|
+
for (const task of taskState?.tasks ?? []) {
|
|
325
|
+
index.tasks.push({
|
|
326
|
+
changeId: change.id,
|
|
327
|
+
changeTitle: title,
|
|
328
|
+
checked: Boolean(task.checked),
|
|
329
|
+
task: compactTask(task),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return index;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function findLoopTaskByHandle(loopFeatureList, taskHandle) {
|
|
337
|
+
if (!taskHandle || !Array.isArray(loopFeatureList?.tasks)) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
return loopFeatureList.tasks.find((task) => task.taskHandle === taskHandle) ?? null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function buildTaskTarget(match, source, extra = {}) {
|
|
344
|
+
if (!match) {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
matched: true,
|
|
349
|
+
source,
|
|
350
|
+
sessionId: extra.sessionId ?? null,
|
|
351
|
+
taskId: match.task.id ?? match.taskId ?? null,
|
|
352
|
+
taskHandle: match.task.taskHandle ?? match.taskHandle ?? null,
|
|
353
|
+
changeId: match.changeId ?? null,
|
|
354
|
+
workUnitId: extra.workUnitId ?? null,
|
|
355
|
+
title: match.task.title ?? match.title ?? null,
|
|
356
|
+
promptPreview: extra.promptPreview ?? null,
|
|
357
|
+
reason: extra.reason ?? null,
|
|
358
|
+
artifacts: extra.artifacts ?? null,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function resolveTaskHandleTarget(taskHandle, index, loopFeatureList, extra = {}) {
|
|
363
|
+
const loopTask = findLoopTaskByHandle(loopFeatureList, taskHandle);
|
|
364
|
+
if (loopTask) {
|
|
365
|
+
return buildTaskTarget({
|
|
366
|
+
task: {
|
|
367
|
+
id: loopTask.id,
|
|
368
|
+
taskHandle: loopTask.taskHandle,
|
|
369
|
+
title: loopTask.title,
|
|
370
|
+
},
|
|
371
|
+
changeId: loopTask.changeId,
|
|
372
|
+
}, extra.source ?? 'task-handle', {
|
|
373
|
+
...extra,
|
|
374
|
+
reason: extra.reason ?? `任务句柄 ${taskHandle} 命中 Loop 任务索引。`,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
const indexedTask = index?.tasks?.find((item) => item.task.taskHandle === taskHandle) ?? null;
|
|
378
|
+
if (!indexedTask) {
|
|
379
|
+
return {
|
|
380
|
+
matched: false,
|
|
381
|
+
source: extra.source ?? 'task-handle',
|
|
382
|
+
sessionId: extra.sessionId ?? null,
|
|
383
|
+
taskId: null,
|
|
384
|
+
taskHandle,
|
|
385
|
+
changeId: null,
|
|
386
|
+
workUnitId: extra.workUnitId ?? null,
|
|
387
|
+
title: null,
|
|
388
|
+
promptPreview: extra.promptPreview ?? null,
|
|
389
|
+
reason: extra.reason ?? `未在本地任务索引中找到任务句柄 ${taskHandle}。`,
|
|
390
|
+
artifacts: extra.artifacts ?? null,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
return buildTaskTarget(indexedTask, extra.source ?? 'task-handle', {
|
|
394
|
+
...extra,
|
|
395
|
+
reason: extra.reason ?? `任务句柄 ${taskHandle} 命中 OpenPrd 任务索引。`,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function resolveSemanticTarget(query, index, extra = {}) {
|
|
400
|
+
if (!String(query ?? '').trim()) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
const candidates = [];
|
|
404
|
+
for (const taskEntry of index?.tasks ?? []) {
|
|
405
|
+
const score = scoreSearchCandidate(query, [
|
|
406
|
+
taskEntry.task.taskHandle,
|
|
407
|
+
taskEntry.task.id,
|
|
408
|
+
taskEntry.task.title,
|
|
409
|
+
taskEntry.changeId,
|
|
410
|
+
taskEntry.changeTitle,
|
|
411
|
+
]);
|
|
412
|
+
if (score > 0) {
|
|
413
|
+
candidates.push({
|
|
414
|
+
kind: 'task',
|
|
415
|
+
score,
|
|
416
|
+
source: extra.source ?? 'semantic',
|
|
417
|
+
changeId: taskEntry.changeId,
|
|
418
|
+
taskId: taskEntry.task.id,
|
|
419
|
+
taskHandle: taskEntry.task.taskHandle,
|
|
420
|
+
title: taskEntry.task.title,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
for (const changeEntry of index?.changes ?? []) {
|
|
425
|
+
const score = scoreSearchCandidate(query, [
|
|
426
|
+
changeEntry.changeId,
|
|
427
|
+
changeEntry.title,
|
|
428
|
+
...(changeEntry.pendingTaskTitles ?? []),
|
|
429
|
+
]);
|
|
430
|
+
if (score > 0) {
|
|
431
|
+
candidates.push({
|
|
432
|
+
kind: 'change',
|
|
433
|
+
score,
|
|
434
|
+
source: extra.source ?? 'semantic',
|
|
435
|
+
changeId: changeEntry.changeId,
|
|
436
|
+
taskId: null,
|
|
437
|
+
taskHandle: null,
|
|
438
|
+
title: changeEntry.title,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (candidates.length === 0) {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
candidates.sort((left, right) => right.score - left.score);
|
|
446
|
+
const best = candidates[0];
|
|
447
|
+
const second = candidates[1] ?? null;
|
|
448
|
+
const ambiguous = second
|
|
449
|
+
&& second.changeId !== best.changeId
|
|
450
|
+
&& best.score < (second.score + 18);
|
|
451
|
+
if (best.score < 70 || ambiguous) {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
return {
|
|
455
|
+
matched: true,
|
|
456
|
+
source: best.source,
|
|
457
|
+
sessionId: extra.sessionId ?? null,
|
|
458
|
+
changeId: best.changeId,
|
|
459
|
+
taskId: best.taskId,
|
|
460
|
+
taskHandle: best.taskHandle,
|
|
461
|
+
workUnitId: extra.workUnitId ?? null,
|
|
462
|
+
title: best.title,
|
|
463
|
+
promptPreview: extra.promptPreview ?? null,
|
|
464
|
+
reason: extra.reason ?? `用户描述命中已有${best.kind === 'task' ? '任务' : '变更'} ${best.taskId ?? best.changeId}。`,
|
|
465
|
+
score: best.score,
|
|
466
|
+
artifacts: extra.artifacts ?? null,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function resolveWorkUnitTarget(projectRoot, workUnitId, index, extra = {}) {
|
|
471
|
+
const binding = await readWorkUnitBindingRecord(projectRoot, workUnitId);
|
|
472
|
+
if (!binding) {
|
|
473
|
+
return {
|
|
474
|
+
matched: false,
|
|
475
|
+
source: extra.source ?? 'work-unit',
|
|
476
|
+
sessionId: extra.sessionId ?? null,
|
|
477
|
+
taskId: null,
|
|
478
|
+
taskHandle: null,
|
|
479
|
+
changeId: null,
|
|
480
|
+
workUnitId,
|
|
481
|
+
title: null,
|
|
482
|
+
promptPreview: extra.promptPreview ?? null,
|
|
483
|
+
reason: extra.reason ?? `未在本地 work unit 绑定中找到 ${workUnitId}。`,
|
|
484
|
+
artifacts: extra.artifacts ?? null,
|
|
485
|
+
binding: null,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
const semanticMatch = resolveSemanticTarget(
|
|
489
|
+
[
|
|
490
|
+
binding.title,
|
|
491
|
+
binding.latestVersionId,
|
|
492
|
+
extra.promptPreview,
|
|
493
|
+
extra.query,
|
|
494
|
+
].filter(Boolean).join(' '),
|
|
495
|
+
index,
|
|
496
|
+
{
|
|
497
|
+
source: extra.source ?? 'work-unit',
|
|
498
|
+
sessionId: extra.sessionId ?? null,
|
|
499
|
+
workUnitId,
|
|
500
|
+
promptPreview: extra.promptPreview ?? null,
|
|
501
|
+
artifacts: extra.artifacts ?? null,
|
|
502
|
+
reason: `工作单元 ${workUnitId} 命中 ${binding.title ?? binding.latestVersionId ?? '本地绑定'}。`,
|
|
503
|
+
},
|
|
504
|
+
);
|
|
505
|
+
if (semanticMatch) {
|
|
506
|
+
return {
|
|
507
|
+
...semanticMatch,
|
|
508
|
+
workUnitId,
|
|
509
|
+
binding,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
matched: true,
|
|
514
|
+
source: extra.source ?? 'work-unit',
|
|
515
|
+
sessionId: extra.sessionId ?? null,
|
|
516
|
+
taskId: null,
|
|
517
|
+
taskHandle: null,
|
|
518
|
+
changeId: null,
|
|
519
|
+
workUnitId,
|
|
520
|
+
title: binding.title ?? binding.latestVersionId ?? null,
|
|
521
|
+
promptPreview: extra.promptPreview ?? null,
|
|
522
|
+
reason: extra.reason ?? `定位到工作单元 ${workUnitId},但还没有足够证据绑定到具体 change/task。`,
|
|
523
|
+
artifacts: extra.artifacts ?? null,
|
|
524
|
+
binding,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function readSessionRequirementGate(projectRoot, sessionId) {
|
|
529
|
+
if (!sessionId) {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
return readJson(cjoin(projectRoot, OPENPRD_HARNESS_REQUIREMENT_GATES_DIR, `${sessionId}.json`)).catch(() => null);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async function readSessionEvents(projectRoot, sessionId) {
|
|
536
|
+
if (!sessionId) {
|
|
537
|
+
return [];
|
|
538
|
+
}
|
|
539
|
+
const events = await readJsonl(cjoin(projectRoot, OPENPRD_HARNESS_EVENTS)).catch(() => []);
|
|
540
|
+
return events.filter((event) => event?.sessionId === sessionId);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async function resolveSessionTarget(projectRoot, sessionId, index, loopFeatureList) {
|
|
544
|
+
const binding = await readSessionBinding(projectRoot, sessionId);
|
|
545
|
+
const gate = await readSessionRequirementGate(projectRoot, sessionId);
|
|
546
|
+
const events = await readSessionEvents(projectRoot, sessionId);
|
|
547
|
+
const directBindingArtifacts = binding ? { sessionBinding: true } : {};
|
|
548
|
+
if (binding?.taskHandle) {
|
|
549
|
+
return resolveTaskHandleTarget(binding.taskHandle, index, loopFeatureList, {
|
|
550
|
+
source: 'session-binding',
|
|
551
|
+
sessionId,
|
|
552
|
+
workUnitId: binding.workUnitId ?? null,
|
|
553
|
+
promptPreview: binding.promptPreview ?? null,
|
|
554
|
+
artifacts: {
|
|
555
|
+
...directBindingArtifacts,
|
|
556
|
+
requirementGate: Boolean(gate),
|
|
557
|
+
events: events.length,
|
|
558
|
+
},
|
|
559
|
+
reason: `会话 ${sessionId} 的 lane 绑定命中任务句柄 ${binding.taskHandle}。`,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
if (binding?.changeId) {
|
|
563
|
+
return {
|
|
564
|
+
matched: true,
|
|
565
|
+
source: 'session-binding',
|
|
566
|
+
sessionId,
|
|
567
|
+
taskId: null,
|
|
568
|
+
taskHandle: binding.taskHandle ?? null,
|
|
569
|
+
changeId: binding.changeId,
|
|
570
|
+
workUnitId: binding.workUnitId ?? null,
|
|
571
|
+
title: binding.title ?? null,
|
|
572
|
+
promptPreview: binding.promptPreview ?? null,
|
|
573
|
+
reason: `会话 ${sessionId} 的 lane 绑定指向变更 ${binding.changeId}。`,
|
|
574
|
+
artifacts: {
|
|
575
|
+
...directBindingArtifacts,
|
|
576
|
+
requirementGate: Boolean(gate),
|
|
577
|
+
events: events.length,
|
|
578
|
+
},
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
if (binding?.workUnitId) {
|
|
582
|
+
const boundWorkUnitTarget = await resolveWorkUnitTarget(projectRoot, binding.workUnitId, index, {
|
|
583
|
+
source: 'session-binding',
|
|
584
|
+
sessionId,
|
|
585
|
+
promptPreview: binding.promptPreview ?? null,
|
|
586
|
+
artifacts: {
|
|
587
|
+
...directBindingArtifacts,
|
|
588
|
+
requirementGate: Boolean(gate),
|
|
589
|
+
events: events.length,
|
|
590
|
+
},
|
|
591
|
+
query: [binding.title, binding.promptPreview, gate?.promptPreview].filter(Boolean).join(' '),
|
|
592
|
+
reason: `会话 ${sessionId} 的 lane 绑定命中工作单元 ${binding.workUnitId}。`,
|
|
593
|
+
});
|
|
594
|
+
if (boundWorkUnitTarget?.matched) {
|
|
595
|
+
return {
|
|
596
|
+
...boundWorkUnitTarget,
|
|
597
|
+
changeId: boundWorkUnitTarget.changeId ?? binding.changeId ?? null,
|
|
598
|
+
title: boundWorkUnitTarget.title ?? binding.title ?? null,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
const promptPreview = gate?.promptPreview
|
|
603
|
+
?? binding?.promptPreview
|
|
604
|
+
?? gate?.reviewActionAuthorization?.promptPreview
|
|
605
|
+
?? events.find((event) => typeof event?.preview === 'string')?.preview
|
|
606
|
+
?? null;
|
|
607
|
+
const texts = [
|
|
608
|
+
binding?.title,
|
|
609
|
+
binding?.promptPreview,
|
|
610
|
+
gate?.promptPreview,
|
|
611
|
+
gate?.confirmationPreview,
|
|
612
|
+
gate?.reviewActionAuthorization?.promptPreview,
|
|
613
|
+
...events.map((event) => event?.preview ?? null),
|
|
614
|
+
].filter(Boolean);
|
|
615
|
+
const artifacts = {
|
|
616
|
+
sessionBinding: Boolean(binding),
|
|
617
|
+
requirementGate: Boolean(gate),
|
|
618
|
+
events: events.length,
|
|
619
|
+
};
|
|
620
|
+
const taskHandle = extractFirstSelectorMatch(texts, CONTINUATION_TASK_HANDLE_PATTERN);
|
|
621
|
+
if (taskHandle) {
|
|
622
|
+
return resolveTaskHandleTarget(taskHandle, index, loopFeatureList, {
|
|
623
|
+
source: 'session',
|
|
624
|
+
sessionId,
|
|
625
|
+
promptPreview,
|
|
626
|
+
artifacts,
|
|
627
|
+
reason: `会话 ${sessionId} 的本地记录命中任务句柄 ${taskHandle}。`,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
const workUnitId = gate?.reviewActionAuthorization?.workUnitId
|
|
631
|
+
?? extractFirstSelectorMatch(texts, CONTINUATION_WORK_UNIT_PATTERN);
|
|
632
|
+
if (workUnitId) {
|
|
633
|
+
return resolveWorkUnitTarget(projectRoot, workUnitId, index, {
|
|
634
|
+
source: 'session',
|
|
635
|
+
sessionId,
|
|
636
|
+
promptPreview,
|
|
637
|
+
artifacts,
|
|
638
|
+
query: texts.join(' '),
|
|
639
|
+
reason: `会话 ${sessionId} 的本地记录命中工作单元 ${workUnitId}。`,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
const semanticMatch = resolveSemanticTarget(texts.join(' '), index, {
|
|
643
|
+
source: 'session',
|
|
644
|
+
sessionId,
|
|
645
|
+
promptPreview,
|
|
646
|
+
artifacts,
|
|
647
|
+
reason: `会话 ${sessionId} 的本地 requirement / hook 历史命中已有任务对象。`,
|
|
648
|
+
});
|
|
649
|
+
if (semanticMatch) {
|
|
650
|
+
return semanticMatch;
|
|
651
|
+
}
|
|
652
|
+
return {
|
|
653
|
+
matched: false,
|
|
654
|
+
source: 'session',
|
|
655
|
+
sessionId,
|
|
656
|
+
taskId: null,
|
|
657
|
+
taskHandle: null,
|
|
658
|
+
changeId: null,
|
|
659
|
+
workUnitId: null,
|
|
660
|
+
title: null,
|
|
661
|
+
promptPreview,
|
|
662
|
+
reason: gate || events.length > 0
|
|
663
|
+
? `本地找到了会话 ${sessionId} 的 requirement gate / hook 事件,但还没有足够证据绑定到具体 change/task/work unit。`
|
|
664
|
+
: `本地没有会话 ${sessionId} 的 requirement gate、hook 事件或 work unit 绑定。`,
|
|
665
|
+
artifacts,
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
async function resolveRunTarget({
|
|
670
|
+
projectRoot,
|
|
671
|
+
message,
|
|
672
|
+
request,
|
|
673
|
+
index,
|
|
674
|
+
loopFeatureList,
|
|
675
|
+
}) {
|
|
676
|
+
const text = String(message ?? '').trim();
|
|
677
|
+
if (!text) {
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
if (request.sessionId) {
|
|
681
|
+
return resolveSessionTarget(projectRoot, request.sessionId, index, loopFeatureList);
|
|
682
|
+
}
|
|
683
|
+
if (request.taskHandle) {
|
|
684
|
+
return resolveTaskHandleTarget(request.taskHandle, index, loopFeatureList);
|
|
685
|
+
}
|
|
686
|
+
if (request.workUnitId) {
|
|
687
|
+
return resolveWorkUnitTarget(projectRoot, request.workUnitId, index, {
|
|
688
|
+
query: text,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
if (request.explicitCurrent) {
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
return resolveSemanticTarget(text, index);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function selectFocusedChangeId(request, resolvedTarget, activeChange) {
|
|
698
|
+
if (resolvedTarget?.changeId) {
|
|
699
|
+
return resolvedTarget.changeId;
|
|
700
|
+
}
|
|
701
|
+
if (request.sessionId || request.taskHandle || request.workUnitId) {
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
return activeChange ?? null;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function describeRunLane(lane) {
|
|
708
|
+
if (lane?.kind === 'targeted') {
|
|
709
|
+
const target = lane.target?.taskHandle
|
|
710
|
+
?? lane.target?.taskId
|
|
711
|
+
?? lane.target?.changeId
|
|
712
|
+
?? lane.target?.workUnitId
|
|
713
|
+
?? '已有对象';
|
|
714
|
+
return `按用户描述定位已有对象 (${target})`;
|
|
715
|
+
}
|
|
716
|
+
if (lane?.kind !== 'continuation') {
|
|
717
|
+
return '默认执行流';
|
|
718
|
+
}
|
|
719
|
+
const selectorLabel = lane.selectorType === 'task-handle'
|
|
720
|
+
? '任务句柄'
|
|
721
|
+
: lane.selectorType === 'work-unit'
|
|
722
|
+
? '工作单元'
|
|
723
|
+
: lane.selectorType === 'session'
|
|
724
|
+
? '历史会话'
|
|
725
|
+
: '继续提示';
|
|
726
|
+
const target = lane.target?.sessionId
|
|
727
|
+
?? lane.target?.taskHandle
|
|
728
|
+
?? lane.target?.taskId
|
|
729
|
+
?? lane.target?.changeId
|
|
730
|
+
?? lane.target?.workUnitId
|
|
731
|
+
?? lane.selector
|
|
732
|
+
?? '当前活动上下文';
|
|
733
|
+
return `继续已有任务 (${selectorLabel}: ${target})`;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function buildRunLane({ message, recommendation, activeChange, latestPrd, loopFeatureList, resolvedTarget }) {
|
|
737
|
+
const request = analyzeRunMessage(message);
|
|
738
|
+
if (!request.requested) {
|
|
739
|
+
if (resolvedTarget?.matched) {
|
|
740
|
+
const target = {
|
|
741
|
+
sessionId: resolvedTarget.sessionId ?? null,
|
|
742
|
+
taskHandle: resolvedTarget.taskHandle ?? recommendation?.task?.taskHandle ?? null,
|
|
743
|
+
taskId: resolvedTarget.taskId ?? recommendation?.task?.id ?? null,
|
|
744
|
+
changeId: resolvedTarget.changeId ?? recommendation?.changeId ?? null,
|
|
745
|
+
workUnitId: resolvedTarget.workUnitId ?? latestPrd?.workUnitId ?? null,
|
|
746
|
+
};
|
|
747
|
+
const lane = {
|
|
748
|
+
kind: 'targeted',
|
|
749
|
+
requested: false,
|
|
750
|
+
selectorType: resolvedTarget.source ?? 'semantic',
|
|
751
|
+
selector: null,
|
|
752
|
+
target,
|
|
753
|
+
matched: Boolean(target.sessionId || target.taskHandle || target.taskId || target.changeId || target.workUnitId),
|
|
754
|
+
resolution: resolvedTarget,
|
|
755
|
+
activeChange,
|
|
756
|
+
};
|
|
757
|
+
return {
|
|
758
|
+
...lane,
|
|
759
|
+
summary: describeRunLane(lane),
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
return {
|
|
763
|
+
kind: 'default',
|
|
764
|
+
requested: false,
|
|
765
|
+
summary: '默认执行流',
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const matchedLoopTask = request.taskHandle ? findLoopTaskByHandle(loopFeatureList, request.taskHandle) : null;
|
|
770
|
+
let target;
|
|
771
|
+
let matched;
|
|
772
|
+
if (request.selectorType === 'session') {
|
|
773
|
+
target = {
|
|
774
|
+
sessionId: request.sessionId,
|
|
775
|
+
taskHandle: resolvedTarget?.taskHandle ?? null,
|
|
776
|
+
taskId: resolvedTarget?.taskId ?? null,
|
|
777
|
+
changeId: resolvedTarget?.changeId ?? null,
|
|
778
|
+
workUnitId: resolvedTarget?.workUnitId ?? null,
|
|
779
|
+
};
|
|
780
|
+
matched = Boolean(resolvedTarget?.matched);
|
|
781
|
+
} else if (request.selectorType === 'task-handle') {
|
|
782
|
+
target = {
|
|
783
|
+
sessionId: null,
|
|
784
|
+
taskHandle: resolvedTarget?.taskHandle ?? matchedLoopTask?.taskHandle ?? request.taskHandle ?? null,
|
|
785
|
+
taskId: resolvedTarget?.taskId ?? matchedLoopTask?.id ?? null,
|
|
786
|
+
changeId: resolvedTarget?.changeId ?? matchedLoopTask?.changeId ?? null,
|
|
787
|
+
workUnitId: resolvedTarget?.workUnitId ?? null,
|
|
788
|
+
};
|
|
789
|
+
matched = Boolean(resolvedTarget?.matched || matchedLoopTask);
|
|
790
|
+
} else if (request.selectorType === 'work-unit') {
|
|
791
|
+
target = {
|
|
792
|
+
sessionId: null,
|
|
793
|
+
taskHandle: resolvedTarget?.taskHandle ?? null,
|
|
794
|
+
taskId: resolvedTarget?.taskId ?? null,
|
|
795
|
+
changeId: resolvedTarget?.changeId ?? null,
|
|
796
|
+
workUnitId: resolvedTarget?.workUnitId ?? request.workUnitId ?? null,
|
|
797
|
+
};
|
|
798
|
+
matched = Boolean(resolvedTarget?.matched);
|
|
799
|
+
} else {
|
|
800
|
+
target = {
|
|
801
|
+
sessionId: null,
|
|
802
|
+
taskHandle: recommendation?.task?.taskHandle ?? null,
|
|
803
|
+
taskId: recommendation?.task?.id ?? null,
|
|
804
|
+
changeId: recommendation?.changeId ?? activeChange ?? null,
|
|
805
|
+
workUnitId: latestPrd?.workUnitId ?? null,
|
|
806
|
+
};
|
|
807
|
+
matched = Boolean(target.taskHandle || target.taskId || target.changeId || target.workUnitId);
|
|
808
|
+
}
|
|
809
|
+
const lane = {
|
|
810
|
+
...request,
|
|
811
|
+
target,
|
|
812
|
+
matched,
|
|
813
|
+
resolution: resolvedTarget ?? null,
|
|
814
|
+
activeChange,
|
|
815
|
+
};
|
|
816
|
+
return {
|
|
817
|
+
...lane,
|
|
818
|
+
summary: describeRunLane(lane),
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function buildSessionContinuationRecommendation(recommendation, lane) {
|
|
823
|
+
const sessionId = lane?.target?.sessionId ?? lane?.sessionId ?? lane?.selector ?? null;
|
|
824
|
+
const recoveredTarget = [
|
|
825
|
+
lane?.target?.changeId ? `变更 ${lane.target.changeId}` : null,
|
|
826
|
+
lane?.target?.taskHandle ? `任务句柄 ${lane.target.taskHandle}` : null,
|
|
827
|
+
lane?.target?.workUnitId ? `工作单元 ${lane.target.workUnitId}` : null,
|
|
828
|
+
].filter(Boolean).join('、');
|
|
829
|
+
return {
|
|
830
|
+
type: 'session-continuation',
|
|
831
|
+
title: sessionId ? `恢复历史会话 ${sessionId}` : '恢复历史会话',
|
|
832
|
+
command: sessionId
|
|
833
|
+
? `openprd run . --context --message ${shellQuote(sessionId)}`
|
|
834
|
+
: 'openprd run . --context',
|
|
835
|
+
verifyCommand: 'openprd run . --verify',
|
|
836
|
+
reason: [
|
|
837
|
+
'当前请求给出的是工具无关的会话 ID;先按本地会话索引恢复该会话历史,再决定后续任务对象。',
|
|
838
|
+
recoveredTarget ? `本地已恢复到 ${recoveredTarget}。` : (lane?.resolution?.reason ?? '本地还没有足够证据把这个会话绑定到具体 change/task/work unit。'),
|
|
839
|
+
lane?.resolution?.promptPreview ? `会话摘要: ${lane.resolution.promptPreview}` : null,
|
|
840
|
+
'不能用相似历史、当前 active change 或当前 requirement gate 替代这个会话 ID。',
|
|
841
|
+
lane?.activeChange && lane.activeChange !== lane?.target?.changeId
|
|
842
|
+
? `当前工作区 active change ${lane.activeChange} 只作为背景提醒。`
|
|
843
|
+
: null,
|
|
844
|
+
].filter(Boolean).join(' '),
|
|
845
|
+
changeId: lane?.target?.changeId ?? null,
|
|
846
|
+
task: lane?.target?.taskId || lane?.target?.taskHandle
|
|
847
|
+
? {
|
|
848
|
+
id: lane.target.taskId ?? null,
|
|
849
|
+
taskHandle: lane.target.taskHandle ?? null,
|
|
850
|
+
title: lane?.resolution?.title ?? null,
|
|
851
|
+
}
|
|
852
|
+
: null,
|
|
853
|
+
coverageItem: null,
|
|
854
|
+
continuationTarget: lane.target ?? null,
|
|
855
|
+
previousRecommendation: recommendation
|
|
856
|
+
? {
|
|
857
|
+
type: recommendation.type ?? null,
|
|
858
|
+
title: recommendation.title ?? null,
|
|
859
|
+
changeId: recommendation.changeId ?? null,
|
|
860
|
+
task: recommendation.task ?? null,
|
|
861
|
+
}
|
|
862
|
+
: null,
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function buildUnresolvedContinuationRecommendation({ message, request, resolution, activeChange }) {
|
|
867
|
+
const selectorLabel = request.selectorType === 'task-handle'
|
|
868
|
+
? '任务句柄'
|
|
869
|
+
: request.selectorType === 'work-unit'
|
|
870
|
+
? '工作单元'
|
|
871
|
+
: '继续目标';
|
|
872
|
+
const selectorValue = request.selector ?? String(message ?? '').trim() ?? '';
|
|
873
|
+
return {
|
|
874
|
+
type: 'continuation-unresolved',
|
|
875
|
+
title: `未能解析${selectorLabel} ${selectorValue}`,
|
|
876
|
+
command: String(message ?? '').trim()
|
|
877
|
+
? `openprd run . --context --message ${shellQuote(String(message).trim())}`
|
|
878
|
+
: 'openprd run . --context',
|
|
879
|
+
verifyCommand: 'openprd run . --verify',
|
|
880
|
+
reason: [
|
|
881
|
+
`当前请求显式给出了${selectorLabel},但本地 OpenPrd 索引还不能把它精确绑定到 change/task/work unit。`,
|
|
882
|
+
resolution?.reason ?? null,
|
|
883
|
+
activeChange ? `当前工作区 active change ${activeChange} 只作为背景提醒,不会自动顶替这个显式目标。` : null,
|
|
884
|
+
].filter(Boolean).join(' '),
|
|
885
|
+
changeId: null,
|
|
886
|
+
task: null,
|
|
887
|
+
coverageItem: null,
|
|
888
|
+
continuationTarget: {
|
|
889
|
+
sessionId: request.sessionId ?? null,
|
|
890
|
+
taskHandle: request.taskHandle ?? null,
|
|
891
|
+
taskId: null,
|
|
892
|
+
changeId: null,
|
|
893
|
+
workUnitId: request.workUnitId ?? null,
|
|
894
|
+
},
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function applyLaneToRecommendation(recommendation, lane) {
|
|
899
|
+
if (!recommendation || !['continuation', 'targeted'].includes(lane?.kind)) {
|
|
900
|
+
return recommendation;
|
|
901
|
+
}
|
|
902
|
+
if (lane.selectorType === 'session') {
|
|
903
|
+
return buildSessionContinuationRecommendation(recommendation, lane);
|
|
904
|
+
}
|
|
905
|
+
if (
|
|
906
|
+
lane.kind === 'continuation'
|
|
907
|
+
&& ['task-handle', 'work-unit'].includes(lane.selectorType)
|
|
908
|
+
&& !lane.matched
|
|
909
|
+
) {
|
|
910
|
+
return buildUnresolvedContinuationRecommendation({
|
|
911
|
+
message: lane.text,
|
|
912
|
+
request: lane,
|
|
913
|
+
resolution: lane.resolution,
|
|
914
|
+
activeChange: recommendation?.changeId ?? null,
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
const targetParts = [
|
|
918
|
+
lane.target?.sessionId ? `会话 ${lane.target.sessionId}` : null,
|
|
919
|
+
lane.target?.taskHandle ? `任务句柄 ${lane.target.taskHandle}` : null,
|
|
920
|
+
lane.target?.changeId ? `变更 ${lane.target.changeId}` : null,
|
|
921
|
+
lane.target?.workUnitId ? `工作单元 ${lane.target.workUnitId}` : null,
|
|
922
|
+
].filter(Boolean);
|
|
923
|
+
const prefix = lane.kind === 'targeted'
|
|
924
|
+
? `当前用户消息已经命中${targetParts.join('、') || '已有对象'};优先围绕这个目标给出结论,再把工作区历史 debt 单列。`
|
|
925
|
+
: lane.matched
|
|
926
|
+
? `当前请求是在继续已有任务;优先围绕${targetParts.join('、') || '当前活动上下文'}给出任务级结论,再把工作区历史 debt 单列。`
|
|
927
|
+
: '当前请求是在继续已有任务;先恢复最接近的任务上下文,再把工作区历史 debt 单列。';
|
|
928
|
+
return {
|
|
929
|
+
...recommendation,
|
|
930
|
+
reason: `${prefix} ${recommendation.reason}`.trim(),
|
|
931
|
+
continuationTarget: lane.target ?? null,
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function shouldSurfaceDiscoveryInRunContext(discovery) {
|
|
936
|
+
const mode = String(discovery?.control?.mode ?? '').trim().toLowerCase();
|
|
937
|
+
if (!mode) {
|
|
938
|
+
return Boolean(discovery);
|
|
939
|
+
}
|
|
940
|
+
return mode !== 'reference';
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function buildPrdPromotionRecommendation({ changes, next }) {
|
|
944
|
+
if (changes?.activeChange) {
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const snapshot = next?.analysisSnapshot ?? null;
|
|
949
|
+
if (!snapshot?.digest) {
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const reviewState = next?.prdReviewState ?? null;
|
|
954
|
+
const suggestedChangeId = slugify(snapshot.title ?? snapshot.versionId);
|
|
955
|
+
if (reviewState?.status !== 'confirmed') {
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
return {
|
|
960
|
+
type: 'prd-change',
|
|
961
|
+
title: `生成 ${suggestedChangeId} 的 change 和任务拆解`,
|
|
962
|
+
command: 'openprd review . --open',
|
|
963
|
+
executionCommand: `openprd change . --generate --change ${shellQuote(suggestedChangeId)}`,
|
|
964
|
+
verifyCommand: `openprd change . --validate --change ${shellQuote(suggestedChangeId)}`,
|
|
965
|
+
reason: '最新 PRD review.html 已确认,但还没有 active change;进入实现前需要先生成 change、spec 和结构化任务。',
|
|
966
|
+
changeId: suggestedChangeId,
|
|
967
|
+
task: null,
|
|
968
|
+
coverageItem: null,
|
|
969
|
+
prd: {
|
|
970
|
+
versionId: snapshot.versionId,
|
|
971
|
+
digest: snapshot.digest,
|
|
972
|
+
workUnitId: snapshot.workUnitId ?? null,
|
|
973
|
+
reviewStatus: reviewState.status,
|
|
974
|
+
reviewCommand: reviewMarkCommand(snapshot),
|
|
975
|
+
},
|
|
976
|
+
intentGate: executionGate(),
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function buildRequirementIntakeRecommendation({ gate, next, activeChange }) {
|
|
981
|
+
const nextAction = next?.recommendation?.nextAction ?? 'clarify-user';
|
|
982
|
+
const titleByAction = {
|
|
983
|
+
'clarify-user': '继续本轮需求入口澄清',
|
|
984
|
+
classify: '补齐本轮需求的产品类型',
|
|
985
|
+
interview: '补齐本轮需求的关键事实',
|
|
986
|
+
synthesize: '生成本轮需求的 PRD 评审稿',
|
|
987
|
+
diagram: '生成本轮需求的可视化评审',
|
|
988
|
+
review: '确认本轮需求的 review.html',
|
|
989
|
+
freeze: '进入本轮需求定稿前检查',
|
|
990
|
+
handoff: '导出本轮需求交接包',
|
|
991
|
+
};
|
|
992
|
+
return {
|
|
993
|
+
type: 'requirement-intake',
|
|
994
|
+
title: titleByAction[nextAction] ?? '继续本轮需求入口',
|
|
995
|
+
command: next?.recommendation?.suggestedCommand ?? 'openprd clarify .',
|
|
996
|
+
verifyCommand: 'openprd run . --verify',
|
|
997
|
+
reason: [
|
|
998
|
+
'当前有 active requirement intake;先围绕本轮需求完成澄清、评审、change 和任务拆解。',
|
|
999
|
+
activeChange ? `历史 active change ${activeChange} 仅作为提醒,不抢本轮默认执行路线。` : null,
|
|
1000
|
+
next?.recommendation?.reason ?? null,
|
|
1001
|
+
].filter(Boolean).join(' '),
|
|
1002
|
+
changeId: null,
|
|
1003
|
+
task: null,
|
|
1004
|
+
coverageItem: null,
|
|
1005
|
+
requirementGate: {
|
|
1006
|
+
status: gate?.status ?? null,
|
|
1007
|
+
promptPreview: gate?.promptPreview ?? null,
|
|
1008
|
+
intakeMode: gate?.intakeMode ?? null,
|
|
1009
|
+
sessionId: gate?.sessionId ?? null,
|
|
1010
|
+
},
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function buildRunRecommendation({
|
|
1015
|
+
message,
|
|
1016
|
+
changes,
|
|
1017
|
+
activeChange,
|
|
1018
|
+
focusedChangeId,
|
|
1019
|
+
taskState,
|
|
1020
|
+
discovery,
|
|
1021
|
+
next,
|
|
1022
|
+
loopFeatureList,
|
|
1023
|
+
requirementGate,
|
|
1024
|
+
laneRequest,
|
|
1025
|
+
resolvedTarget,
|
|
1026
|
+
}) {
|
|
1027
|
+
if (
|
|
1028
|
+
['task-handle', 'work-unit'].includes(laneRequest?.selectorType)
|
|
1029
|
+
&& !resolvedTarget?.matched
|
|
1030
|
+
) {
|
|
1031
|
+
return buildUnresolvedContinuationRecommendation({
|
|
1032
|
+
message,
|
|
1033
|
+
request: laneRequest,
|
|
1034
|
+
resolution: resolvedTarget,
|
|
1035
|
+
activeChange,
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
if (requirementGate?.active && !laneRequest?.requested && !resolvedTarget?.matched) {
|
|
1039
|
+
return buildRequirementIntakeRecommendation({ gate: requirementGate, next, activeChange });
|
|
1040
|
+
}
|
|
1041
|
+
if (taskState?.nextTask) {
|
|
1042
|
+
const task = compactTask(taskState.nextTask);
|
|
1043
|
+
const totalTasks = Number(taskState.summary?.total ?? taskState.tasks?.length ?? 0);
|
|
1044
|
+
const pendingTasks = Number(taskState.summary?.pending ?? 0);
|
|
1045
|
+
const implementationTasks = Number(taskState.summary?.implementation?.total ?? 0);
|
|
1046
|
+
const pendingImplementationTasks = Number(taskState.summary?.implementation?.pending ?? 0);
|
|
1047
|
+
if (
|
|
1048
|
+
implementationTasks >= OPENPRD_LOOP_REQUIRED_IMPLEMENTATION_TASK_THRESHOLD
|
|
1049
|
+
|| pendingImplementationTasks >= OPENPRD_LOOP_REQUIRED_IMPLEMENTATION_TASK_THRESHOLD
|
|
1050
|
+
) {
|
|
1051
|
+
const loopReady = loopFeatureList?.changeId === taskState.changeId && Array.isArray(loopFeatureList.tasks);
|
|
1052
|
+
return {
|
|
1053
|
+
type: 'loop-task',
|
|
1054
|
+
title: `用 Loop 执行 ${task.id}: ${task.title}`,
|
|
1055
|
+
command: `openprd tasks . --change ${shellQuote(taskState.changeId)}`,
|
|
1056
|
+
preparationCommand: loopReady
|
|
1057
|
+
? `openprd loop . --next --item ${shellQuote(task.id)}`
|
|
1058
|
+
: `openprd loop . --plan --change ${shellQuote(taskState.changeId)}`,
|
|
1059
|
+
executionCommand: loopReady
|
|
1060
|
+
? `openprd loop . --run --agent codex --item ${shellQuote(task.id)}`
|
|
1061
|
+
: `openprd loop . --plan --change ${shellQuote(taskState.changeId)} && openprd loop . --run --agent codex --item ${shellQuote(task.id)}`,
|
|
1062
|
+
commitCommand: `openprd loop . --finish --item ${shellQuote(task.id)} --commit`,
|
|
1063
|
+
verifyCommand: `openprd loop . --verify --item ${shellQuote(task.id)}`,
|
|
1064
|
+
reason: `当前变更包含 ${implementationTasks} 个实质实现任务,达到 ${OPENPRD_LOOP_REQUIRED_IMPLEMENTATION_TASK_THRESHOLD} 个实现任务的拆分阈值;建议使用独立 worktree 和 OpenPrd Loop 单任务会话,且只有用户明确要求开发、继续任务或深度对标落地时才执行。`,
|
|
1065
|
+
changeId: taskState.changeId,
|
|
1066
|
+
task,
|
|
1067
|
+
coverageItem: null,
|
|
1068
|
+
intentGate: executionGate(),
|
|
1069
|
+
loop: {
|
|
1070
|
+
required: true,
|
|
1071
|
+
threshold: OPENPRD_LOOP_REQUIRED_IMPLEMENTATION_TASK_THRESHOLD,
|
|
1072
|
+
planned: loopReady,
|
|
1073
|
+
totalTasks,
|
|
1074
|
+
pendingTasks,
|
|
1075
|
+
implementationTasks,
|
|
1076
|
+
pendingImplementationTasks,
|
|
1077
|
+
worktreeRecommended: true,
|
|
1078
|
+
},
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
return {
|
|
1082
|
+
type: 'task',
|
|
1083
|
+
title: `推进 ${task.id}: ${task.title}`,
|
|
1084
|
+
command: `openprd tasks . --change ${shellQuote(taskState.changeId)}`,
|
|
1085
|
+
executionCommand: `openprd tasks . --change ${shellQuote(taskState.changeId)} --advance --verify --item ${shellQuote(task.id)}`,
|
|
1086
|
+
verifyCommand: task.verify ?? `openprd tasks . --change ${shellQuote(taskState.changeId)} --verify --item ${shellQuote(task.id)}`,
|
|
1087
|
+
reason: '存在一个依赖已就绪的 OpenPrd 任务;只有用户明确要求开发、实现或继续任务时才推进。',
|
|
1088
|
+
changeId: taskState.changeId,
|
|
1089
|
+
task,
|
|
1090
|
+
coverageItem: null,
|
|
1091
|
+
intentGate: executionGate(),
|
|
1092
|
+
loop: {
|
|
1093
|
+
required: false,
|
|
1094
|
+
threshold: OPENPRD_LOOP_REQUIRED_IMPLEMENTATION_TASK_THRESHOLD,
|
|
1095
|
+
totalTasks,
|
|
1096
|
+
pendingTasks,
|
|
1097
|
+
implementationTasks,
|
|
1098
|
+
pendingImplementationTasks,
|
|
1099
|
+
worktreeRecommended: false,
|
|
1100
|
+
},
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
if (taskState && taskState.summary?.pending === 0 && focusedChangeId) {
|
|
1104
|
+
return {
|
|
1105
|
+
type: 'change-review',
|
|
1106
|
+
title: `校验已完成的变更 ${focusedChangeId}`,
|
|
1107
|
+
command: `openprd change . --validate --change ${shellQuote(focusedChangeId)}`,
|
|
1108
|
+
verifyCommand: `openprd change . --validate --change ${shellQuote(focusedChangeId)}`,
|
|
1109
|
+
reason: '当前激活变更没有待处理的结构化任务。',
|
|
1110
|
+
changeId: focusedChangeId,
|
|
1111
|
+
task: null,
|
|
1112
|
+
coverageItem: null,
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
const prdPromotion = buildPrdPromotionRecommendation({ changes, next });
|
|
1116
|
+
if (prdPromotion) {
|
|
1117
|
+
return prdPromotion;
|
|
1118
|
+
}
|
|
1119
|
+
const nextCoverage = discovery?.coverageMatrix?.nextPendingItem;
|
|
1120
|
+
if (nextCoverage) {
|
|
1121
|
+
const item = compactCoverageItem(nextCoverage);
|
|
1122
|
+
return {
|
|
1123
|
+
type: 'discovery',
|
|
1124
|
+
title: `调研 ${item.title}`,
|
|
1125
|
+
command: 'openprd discovery . --verify',
|
|
1126
|
+
executionCommand: `openprd discovery . --advance --item ${shellQuote(item.id)} --claim <evidence-backed-claim> --evidence <path>`,
|
|
1127
|
+
verifyCommand: 'openprd discovery . --verify',
|
|
1128
|
+
reason: '存在一个待处理的 OpenPrd discovery 覆盖项;只有用户明确要求深度调研、对标、复刻或持续补全时才推进覆盖项。',
|
|
1129
|
+
changeId: focusedChangeId ?? activeChange,
|
|
1130
|
+
task: null,
|
|
1131
|
+
coverageItem: item,
|
|
1132
|
+
intentGate: executionGate(),
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
if (discovery?.coverageMatrix?.summary?.pending === 0 && discovery?.runId) {
|
|
1136
|
+
return {
|
|
1137
|
+
type: 'discovery-review',
|
|
1138
|
+
title: `校验 discovery run ${discovery.runId}`,
|
|
1139
|
+
command: 'openprd discovery . --verify',
|
|
1140
|
+
verifyCommand: 'openprd discovery . --verify',
|
|
1141
|
+
reason: '当前 discovery run 没有待处理覆盖项。',
|
|
1142
|
+
changeId: focusedChangeId ?? activeChange,
|
|
1143
|
+
task: null,
|
|
1144
|
+
coverageItem: null,
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
return {
|
|
1148
|
+
type: 'workflow',
|
|
1149
|
+
title: next?.recommendation?.nextAction ?? 'Inspect OpenPrd next action',
|
|
1150
|
+
command: next?.recommendation?.suggestedCommand ?? 'openprd next .',
|
|
1151
|
+
verifyCommand: 'openprd validate .',
|
|
1152
|
+
reason: next?.recommendation?.reason ?? 'No active task or discovery item was found.',
|
|
1153
|
+
changeId: focusedChangeId ?? activeChange,
|
|
1154
|
+
task: null,
|
|
1155
|
+
coverageItem: null,
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
async function buildRunContext(projectRoot, dependencies, options = {}) {
|
|
1160
|
+
const {
|
|
1161
|
+
listOpenPrdChangesWorkspace,
|
|
1162
|
+
listOpenSpecTaskWorkspace,
|
|
1163
|
+
nextWorkspace,
|
|
1164
|
+
resumeOpenSpecDiscoveryWorkspace,
|
|
1165
|
+
validateWorkspace,
|
|
1166
|
+
} = dependencies;
|
|
1167
|
+
await ensureRunHarness(projectRoot);
|
|
1168
|
+
const runState = await readRunState(projectRoot);
|
|
1169
|
+
const laneRequest = analyzeRunMessage(options.message);
|
|
1170
|
+
const validation = await validateWorkspace(projectRoot)
|
|
1171
|
+
.then(({ report }) => report)
|
|
1172
|
+
.catch((error) => ({
|
|
1173
|
+
valid: false,
|
|
1174
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
1175
|
+
warnings: [],
|
|
1176
|
+
checks: [],
|
|
1177
|
+
}));
|
|
1178
|
+
const next = await nextWorkspace(projectRoot).catch(() => null);
|
|
1179
|
+
const requirementGate = await readActiveRequirementGate(projectRoot);
|
|
1180
|
+
const changes = await listOpenPrdChangesWorkspace(projectRoot).catch(() => null);
|
|
1181
|
+
const activeChange = changes?.activeChange ?? null;
|
|
1182
|
+
const latestPrd = next?.analysisSnapshot
|
|
1183
|
+
? {
|
|
1184
|
+
versionId: next.analysisSnapshot.versionId ?? null,
|
|
1185
|
+
digest: next.analysisSnapshot.digest ?? null,
|
|
1186
|
+
workUnitId: next.analysisSnapshot.workUnitId ?? null,
|
|
1187
|
+
title: next.analysisSnapshot.title ?? null,
|
|
1188
|
+
status: next.analysisSnapshot.status ?? null,
|
|
1189
|
+
}
|
|
1190
|
+
: null;
|
|
1191
|
+
const loopFeatureList = await readJson(harnessFile(projectRoot, OPENPRD_HARNESS_LOOP_FEATURE_LIST)).catch(() => null);
|
|
1192
|
+
const shouldResolveTarget = Boolean(String(options.message ?? '').trim());
|
|
1193
|
+
const resolutionIndex = shouldResolveTarget
|
|
1194
|
+
? await buildRunResolutionIndex(projectRoot, changes, listOpenSpecTaskWorkspace)
|
|
1195
|
+
: null;
|
|
1196
|
+
const resolvedTarget = shouldResolveTarget
|
|
1197
|
+
? await resolveRunTarget({
|
|
1198
|
+
projectRoot,
|
|
1199
|
+
message: options.message,
|
|
1200
|
+
request: laneRequest,
|
|
1201
|
+
index: resolutionIndex,
|
|
1202
|
+
loopFeatureList,
|
|
1203
|
+
})
|
|
1204
|
+
: null;
|
|
1205
|
+
const focusedChangeId = selectFocusedChangeId(laneRequest, resolvedTarget, activeChange);
|
|
1206
|
+
const taskState = focusedChangeId
|
|
1207
|
+
? await listOpenSpecTaskWorkspace(projectRoot, { change: focusedChangeId }).catch(() => null)
|
|
1208
|
+
: null;
|
|
1209
|
+
const resumedDiscovery = await resumeOpenSpecDiscoveryWorkspace(projectRoot).catch(() => null);
|
|
1210
|
+
const discovery = shouldSurfaceDiscoveryInRunContext(resumedDiscovery) ? resumedDiscovery : null;
|
|
1211
|
+
const recommendation = buildRunRecommendation({
|
|
1212
|
+
message: options.message,
|
|
1213
|
+
changes,
|
|
1214
|
+
activeChange,
|
|
1215
|
+
focusedChangeId,
|
|
1216
|
+
taskState,
|
|
1217
|
+
discovery,
|
|
1218
|
+
next,
|
|
1219
|
+
loopFeatureList,
|
|
1220
|
+
requirementGate,
|
|
1221
|
+
laneRequest,
|
|
1222
|
+
resolvedTarget,
|
|
1223
|
+
});
|
|
1224
|
+
const nextTask = compactTask(taskState?.nextTask ?? null);
|
|
1225
|
+
const lane = buildRunLane({
|
|
1226
|
+
message: options.message,
|
|
1227
|
+
recommendation,
|
|
1228
|
+
activeChange,
|
|
1229
|
+
latestPrd,
|
|
1230
|
+
loopFeatureList,
|
|
1231
|
+
resolvedTarget,
|
|
1232
|
+
});
|
|
1233
|
+
const effectiveRecommendation = applyLaneToRecommendation(recommendation, lane);
|
|
1234
|
+
|
|
1235
|
+
const context = {
|
|
1236
|
+
ok: validation.valid,
|
|
1237
|
+
action: 'run-context',
|
|
1238
|
+
projectRoot,
|
|
1239
|
+
generatedAt: timestamp(),
|
|
1240
|
+
runState,
|
|
1241
|
+
validation: {
|
|
1242
|
+
valid: validation.valid,
|
|
1243
|
+
errors: validation.errors ?? [],
|
|
1244
|
+
warnings: validation.warnings ?? [],
|
|
1245
|
+
},
|
|
1246
|
+
workflow: next?.workflow ?? [],
|
|
1247
|
+
next: next?.recommendation ?? null,
|
|
1248
|
+
activeRequirementGate: requirementGate
|
|
1249
|
+
? {
|
|
1250
|
+
status: requirementGate.status ?? null,
|
|
1251
|
+
promptPreview: requirementGate.promptPreview ?? null,
|
|
1252
|
+
intakeMode: requirementGate.intakeMode ?? null,
|
|
1253
|
+
sessionId: requirementGate.sessionId ?? null,
|
|
1254
|
+
}
|
|
1255
|
+
: null,
|
|
1256
|
+
prdReviewState: next?.prdReviewState
|
|
1257
|
+
? {
|
|
1258
|
+
versionId: next.prdReviewState.versionId ?? null,
|
|
1259
|
+
status: next.prdReviewState.status ?? null,
|
|
1260
|
+
artifactExists: Boolean(next.prdReviewState.artifactExists),
|
|
1261
|
+
artifact: next.prdReviewState.artifact ?? null,
|
|
1262
|
+
shouldGateFreeze: Boolean(next.prdReviewState.shouldGateFreeze),
|
|
1263
|
+
}
|
|
1264
|
+
: null,
|
|
1265
|
+
latestPrd,
|
|
1266
|
+
activeChange,
|
|
1267
|
+
focus: {
|
|
1268
|
+
changeId: focusedChangeId,
|
|
1269
|
+
source: resolvedTarget?.source ?? null,
|
|
1270
|
+
sessionId: resolvedTarget?.sessionId ?? lane.target?.sessionId ?? null,
|
|
1271
|
+
taskHandle: resolvedTarget?.taskHandle ?? null,
|
|
1272
|
+
workUnitId: resolvedTarget?.workUnitId ?? null,
|
|
1273
|
+
matched: Boolean(resolvedTarget?.matched),
|
|
1274
|
+
reason: resolvedTarget?.reason ?? null,
|
|
1275
|
+
promptPreview: resolvedTarget?.promptPreview ?? null,
|
|
1276
|
+
},
|
|
1277
|
+
taskSummary: taskState?.summary ?? null,
|
|
1278
|
+
nextTask,
|
|
1279
|
+
blockedTasks: taskState?.blockedTasks ?? [],
|
|
1280
|
+
discovery: discovery
|
|
1281
|
+
? {
|
|
1282
|
+
runId: discovery.runId,
|
|
1283
|
+
mode: discovery.control?.mode ?? null,
|
|
1284
|
+
status: discovery.control?.status ?? null,
|
|
1285
|
+
iteration: discovery.control?.iteration ?? null,
|
|
1286
|
+
maxIterations: discovery.control?.maxIterations ?? null,
|
|
1287
|
+
summary: discovery.coverageMatrix?.summary ?? null,
|
|
1288
|
+
nextPendingItem: compactCoverageItem(discovery.coverageMatrix?.nextPendingItem ?? null),
|
|
1289
|
+
}
|
|
1290
|
+
: null,
|
|
1291
|
+
lane,
|
|
1292
|
+
recommendation: effectiveRecommendation,
|
|
1293
|
+
files: {
|
|
1294
|
+
runState: OPENPRD_HARNESS_RUN_STATE,
|
|
1295
|
+
iterations: OPENPRD_HARNESS_ITERATIONS,
|
|
1296
|
+
learnings: OPENPRD_HARNESS_LEARNINGS,
|
|
1297
|
+
},
|
|
1298
|
+
};
|
|
1299
|
+
|
|
1300
|
+
await writeRunState(projectRoot, {
|
|
1301
|
+
...runState,
|
|
1302
|
+
lastContextAt: context.generatedAt,
|
|
1303
|
+
lastRecommendation: effectiveRecommendation,
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
return context;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
async function recordRunHook(projectRoot, options = {}) {
|
|
1310
|
+
await ensureRunHarness(projectRoot);
|
|
1311
|
+
const state = await readRunState(projectRoot);
|
|
1312
|
+
const currentIteration = Number(state.currentIteration ?? 0) + 1;
|
|
1313
|
+
const event = {
|
|
1314
|
+
version: 1,
|
|
1315
|
+
at: timestamp(),
|
|
1316
|
+
iteration: currentIteration,
|
|
1317
|
+
type: 'hook',
|
|
1318
|
+
eventName: options.event ?? 'Unknown',
|
|
1319
|
+
risk: options.risk ?? 'unknown',
|
|
1320
|
+
outcome: options.outcome ?? 'unknown',
|
|
1321
|
+
preview: options.preview ?? null,
|
|
1322
|
+
};
|
|
1323
|
+
await appendJsonl(harnessFile(projectRoot, OPENPRD_HARNESS_ITERATIONS), event);
|
|
1324
|
+
await writeRunState(projectRoot, {
|
|
1325
|
+
...state,
|
|
1326
|
+
currentIteration,
|
|
1327
|
+
lastHookAt: event.at,
|
|
1328
|
+
lastOutcome: event.outcome,
|
|
1329
|
+
});
|
|
1330
|
+
if (options.learn) {
|
|
1331
|
+
await appendText(harnessFile(projectRoot, OPENPRD_HARNESS_LEARNINGS), `\n## ${event.at}\n\n- ${options.learn}\n`);
|
|
1332
|
+
}
|
|
1333
|
+
return {
|
|
1334
|
+
ok: true,
|
|
1335
|
+
action: 'run-record-hook',
|
|
1336
|
+
projectRoot,
|
|
1337
|
+
event,
|
|
1338
|
+
files: {
|
|
1339
|
+
runState: OPENPRD_HARNESS_RUN_STATE,
|
|
1340
|
+
iterations: OPENPRD_HARNESS_ITERATIONS,
|
|
1341
|
+
learnings: OPENPRD_HARNESS_LEARNINGS,
|
|
1342
|
+
},
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
async function verifyRunWorkspace(projectRoot, dependencies, options = {}) {
|
|
1347
|
+
const {
|
|
1348
|
+
checkStandardsWorkspace,
|
|
1349
|
+
validateOpenSpecChangeWorkspace,
|
|
1350
|
+
validateWorkspace,
|
|
1351
|
+
verifyOpenSpecDiscoveryWorkspace,
|
|
1352
|
+
verifyQualityWorkspace,
|
|
1353
|
+
} = dependencies;
|
|
1354
|
+
const context = await buildRunContext(projectRoot, dependencies, options);
|
|
1355
|
+
const standards = await checkStandardsWorkspace(projectRoot);
|
|
1356
|
+
const validation = await validateWorkspace(projectRoot).then(({ report }) => report);
|
|
1357
|
+
const checks = [
|
|
1358
|
+
{ name: 'standards', scope: 'task', ok: standards.ok, errors: standards.errors ?? [] },
|
|
1359
|
+
{ name: 'validate', scope: 'task', ok: validation.valid, errors: validation.errors ?? [] },
|
|
1360
|
+
];
|
|
1361
|
+
if (verifyQualityWorkspace) {
|
|
1362
|
+
const quality = await verifyQualityWorkspace(projectRoot, { strict: false }).catch((error) => ({
|
|
1363
|
+
ok: false,
|
|
1364
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
1365
|
+
}));
|
|
1366
|
+
const productionReady = quality.report?.readiness?.productionReady ?? null;
|
|
1367
|
+
const qualityErrors = [
|
|
1368
|
+
...(quality.errors ?? []),
|
|
1369
|
+
...(productionReady === false ? ['Quality report is not production-ready. Review required gates and evidence before claiming readiness.'] : []),
|
|
1370
|
+
];
|
|
1371
|
+
checks.push({
|
|
1372
|
+
name: 'quality',
|
|
1373
|
+
scope: 'workspace',
|
|
1374
|
+
ok: quality.ok && productionReady === true,
|
|
1375
|
+
errors: qualityErrors,
|
|
1376
|
+
reportPath: quality.reportPath ?? null,
|
|
1377
|
+
htmlPath: quality.htmlPath ?? null,
|
|
1378
|
+
productionReady,
|
|
1379
|
+
attentionGates: quality.report?.readiness?.attentionGates ?? [],
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
const changeToVerify = context.focus?.changeId ?? context.recommendation?.changeId ?? context.activeChange ?? null;
|
|
1383
|
+
if (changeToVerify) {
|
|
1384
|
+
const change = await validateOpenSpecChangeWorkspace(projectRoot, { change: changeToVerify });
|
|
1385
|
+
checks.push({ name: 'change', scope: 'task', ok: change.ok, errors: change.errors ?? [] });
|
|
1386
|
+
}
|
|
1387
|
+
if (context.discovery) {
|
|
1388
|
+
const discovery = await verifyOpenSpecDiscoveryWorkspace(projectRoot);
|
|
1389
|
+
checks.push({ name: 'discovery', scope: 'task', ok: discovery.ok, errors: discovery.verification.errors ?? [] });
|
|
1390
|
+
}
|
|
1391
|
+
const taskChecks = checks.filter((check) => check.scope !== 'workspace');
|
|
1392
|
+
const workspaceChecks = checks;
|
|
1393
|
+
const taskReady = taskChecks.every((check) => check.ok);
|
|
1394
|
+
const workspaceReady = workspaceChecks.every((check) => check.ok);
|
|
1395
|
+
const ok = taskReady;
|
|
1396
|
+
const qualityCheck = checks.find((check) => check.name === 'quality');
|
|
1397
|
+
const readiness = {
|
|
1398
|
+
taskReady,
|
|
1399
|
+
workspaceReady,
|
|
1400
|
+
releaseReady: workspaceReady,
|
|
1401
|
+
doctorReady: null,
|
|
1402
|
+
qualityProductionReady: qualityCheck?.productionReady ?? null,
|
|
1403
|
+
};
|
|
1404
|
+
const knowledgeSignal = {
|
|
1405
|
+
kind: 'run-verify',
|
|
1406
|
+
ok: workspaceReady,
|
|
1407
|
+
taskReady,
|
|
1408
|
+
workspaceReady,
|
|
1409
|
+
productionReady: qualityCheck?.productionReady ?? null,
|
|
1410
|
+
attentionGates: qualityCheck?.attentionGates ?? [],
|
|
1411
|
+
summary: taskReady
|
|
1412
|
+
? (workspaceReady ? 'run verify passed' : `run verify task-ready with workspace attention: ${workspaceChecks.filter((check) => !check.ok).map((check) => check.name).join(', ')}`)
|
|
1413
|
+
: `run verify failed: ${taskChecks.filter((check) => !check.ok).map((check) => check.name).join(', ')}`,
|
|
1414
|
+
};
|
|
1415
|
+
await recordKnowledgeReviewSignal(projectRoot, knowledgeSignal).catch(() => null);
|
|
1416
|
+
const reviewSource = (await exists(harnessFile(projectRoot, OPENPRD_HARNESS_TURN_STATE)))
|
|
1417
|
+
? OPENPRD_HARNESS_TURN_STATE
|
|
1418
|
+
: (qualityCheck?.reportPath ?? null);
|
|
1419
|
+
const knowledgeReview = await reviewKnowledgeWorkspace(projectRoot, {
|
|
1420
|
+
from: reviewSource,
|
|
1421
|
+
signal: knowledgeSignal,
|
|
1422
|
+
}).catch((error) => ({
|
|
1423
|
+
ok: false,
|
|
1424
|
+
action: 'quality-knowledge-review',
|
|
1425
|
+
skipped: false,
|
|
1426
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
1427
|
+
}));
|
|
1428
|
+
await appendJsonl(harnessFile(projectRoot, OPENPRD_HARNESS_ITERATIONS), {
|
|
1429
|
+
version: 1,
|
|
1430
|
+
at: timestamp(),
|
|
1431
|
+
type: 'verify',
|
|
1432
|
+
ok,
|
|
1433
|
+
readiness,
|
|
1434
|
+
checks: checks.map((check) => ({ name: check.name, scope: check.scope, ok: check.ok, errors: check.errors.length })),
|
|
1435
|
+
});
|
|
1436
|
+
const errors = taskChecks.flatMap((check) => check.errors.map((error) => `${check.name}: ${error}`));
|
|
1437
|
+
const warnings = workspaceChecks
|
|
1438
|
+
.filter((check) => check.scope === 'workspace' && !check.ok)
|
|
1439
|
+
.flatMap((check) => check.errors.map((error) => `${check.name}: ${error}`));
|
|
1440
|
+
return {
|
|
1441
|
+
ok,
|
|
1442
|
+
action: 'run-verify',
|
|
1443
|
+
projectRoot,
|
|
1444
|
+
context,
|
|
1445
|
+
checks,
|
|
1446
|
+
readiness,
|
|
1447
|
+
warnings,
|
|
1448
|
+
knowledgeReview,
|
|
1449
|
+
errors,
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
async function runWorkspaceImpl(projectRoot, options = {}, dependencies = {}) {
|
|
1454
|
+
if (options.recordHook) {
|
|
1455
|
+
return recordRunHook(projectRoot, options);
|
|
1456
|
+
}
|
|
1457
|
+
if (options.verify) {
|
|
1458
|
+
return verifyRunWorkspace(projectRoot, dependencies, options);
|
|
1459
|
+
}
|
|
1460
|
+
return buildRunContext(projectRoot, dependencies, options);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
|
|
1464
|
+
function createRunWorkspace(dependencies) {
|
|
1465
|
+
return function runWorkspace(projectRoot, options = {}) {
|
|
1466
|
+
return runWorkspaceImpl(projectRoot, options, dependencies);
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
export { createRunWorkspace };
|