@openprd/cli 0.1.0 → 0.1.8
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 +43 -69
- package/.openprd/README_EN.md +84 -0
- package/.openprd/benchmarks/index.md +7 -0
- package/.openprd/benchmarks/sources.yaml +25 -3
- package/.openprd/discovery/config.json +16 -2
- package/.openprd/engagements/active/flows.md +19 -14
- package/.openprd/engagements/active/handoff.md +11 -4
- package/.openprd/engagements/active/prd.md +99 -71
- package/.openprd/engagements/active/review.html +4 -4
- package/.openprd/engagements/active/roles.md +9 -8
- package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +4 -4
- package/.openprd/engagements/work-units/wu-20260602113956-a99b5b88.json +18 -0
- package/.openprd/engagements/work-units/wu-20260602122244-78656aaf.json +18 -0
- package/.openprd/engagements/work-units/wu-20260602122442-e96489e2.json +18 -0
- package/.openprd/engagements/work-units/wu-20260602132835-695429e8.json +18 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/candidate.json +78 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/diagnostic-report.json +129 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/root-cause-candidates.json +41 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/timeline.json +14 -0
- package/.openprd/knowledge/drafts/openprd-experience-diagnostic-candidate-turn-1780116203372-5f266a79e968c758/SKILL.md +49 -0
- package/.openprd/knowledge/index.json +44 -4
- package/.openprd/reviews/v0001.html +195 -129
- package/.openprd/reviews/v0002.html +1150 -0
- package/.openprd/reviews/v0003.html +1150 -0
- package/.openprd/reviews/v0004.html +1150 -0
- package/.openprd/reviews/v0005.html +1150 -0
- package/.openprd/standards/config.json +12 -9
- package/.openprd/state/changes.json +17 -2
- package/.openprd/state/current.json +399 -63
- package/.openprd/state/release-ledger.json +344 -0
- package/.openprd/state/version-index.json +52 -0
- package/.openprd/state/versions/v0002.json +264 -0
- package/.openprd/state/versions/v0002.md +183 -0
- package/.openprd/state/versions/v0003.json +269 -0
- package/.openprd/state/versions/v0003.md +188 -0
- package/.openprd/state/versions/v0004.json +274 -0
- package/.openprd/state/versions/v0004.md +193 -0
- package/.openprd/state/versions/v0005.json +299 -0
- package/.openprd/state/versions/v0005.md +189 -0
- package/.openprd/templates/agent/intake.md +5 -4
- package/.openprd/templates/b2b/intake.md +5 -4
- package/.openprd/templates/base/intake.md +10 -4
- package/.openprd/templates/company/README.md +9 -7
- package/.openprd/templates/company/README_EN.md +12 -0
- package/.openprd/templates/consumer/intake.md +5 -4
- package/.openprd/templates/industry/README.md +12 -10
- package/.openprd/templates/industry/README_EN.md +18 -0
- package/.openprd/templates/project/README.md +11 -9
- package/.openprd/templates/project/README_EN.md +16 -0
- package/.openprd/templates/session/README.md +11 -9
- package/.openprd/templates/session/README_EN.md +16 -0
- package/AGENTS.md +12 -8
- package/README.md +402 -441
- package/README_CN.md +4 -578
- package/README_EN.md +850 -0
- package/docs/assets/openprd-requirement-routing-en.png +0 -0
- package/docs/assets/openprd-requirement-routing-en.svg +102 -0
- 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 +102 -0
- package/package.json +6 -2
- package/scripts/dev-check-wrapup-copy.mjs +110 -0
- package/scripts/openprd-github-release-notes.mjs +99 -0
- package/scripts/quality-perf-check.mjs +203 -0
- package/skills/openprd-benchmark-router/SKILL.md +1 -0
- package/skills/openprd-benchmark-router/references/benchmark-sources.md +1 -0
- package/skills/openprd-benchmark-router/references/source-policy.md +2 -0
- package/skills/openprd-discovery-loop/SKILL.md +2 -2
- package/skills/openprd-harness/SKILL.md +46 -24
- package/skills/openprd-harness/references/workflow-gates.md +15 -0
- package/skills/openprd-quality/SKILL.md +10 -4
- package/skills/openprd-requirement-intake/SKILL.md +39 -23
- package/skills/openprd-requirement-intake/references/prd-template-lenses.md +6 -6
- package/skills/openprd-requirement-intake/references/routing-rubric.md +22 -8
- package/skills/openprd-router/SKILL.md +2 -2
- package/skills/openprd-shared/SKILL.md +51 -23
- package/skills/openprd-standards/SKILL.md +2 -1
- package/src/agent-integration.js +265 -65
- package/src/benchmark/constants.js +107 -0
- package/src/benchmark/operations.js +235 -0
- package/src/benchmark/registry.js +64 -0
- package/src/benchmark/render.js +115 -0
- package/src/benchmark/source.js +617 -0
- package/src/benchmark/storage.js +121 -0
- package/src/benchmark/verify.js +235 -0
- package/src/benchmark.js +50 -851
- package/src/change-summary.js +339 -0
- package/src/cli/args.js +67 -6
- package/src/cli/basic-print.js +365 -0
- package/src/cli/benchmark-print.js +91 -0
- package/src/cli/change-print.js +221 -0
- package/src/cli/doctor-print.js +268 -0
- package/src/cli/growth-print.js +176 -0
- package/src/cli/print.js +73 -1384
- package/src/cli/quality-print.js +284 -0
- package/src/cli/run-print.js +297 -0
- package/src/cli/shared-print.js +127 -0
- package/src/cli/workflow-print.js +195 -0
- package/src/codex-hook-runner-template.mjs +639 -117
- package/src/codex-runtime.js +324 -0
- package/src/dev-standards.js +178 -5
- package/src/diagram-core.js +5 -5
- package/src/discovery.js +2 -1
- package/src/execution-strategy.js +369 -0
- package/src/fleet.js +4 -0
- package/src/github-release.js +156 -0
- package/src/growth.js +311 -13
- package/src/html-artifact-utils.js +25 -0
- package/src/html-artifacts.js +157 -1596
- package/src/knowledge.js +1176 -75
- package/src/language-policy.js +2 -112
- package/src/learning-html-artifact.js +1031 -0
- package/src/learning-review.js +3 -2
- package/src/loop.js +280 -9
- package/src/openprd.js +341 -38
- package/src/openspec/change-validate.js +0 -9
- package/src/openspec/execute.js +79 -3
- package/src/openspec/generate.js +33 -20
- package/src/openspec/tasks.js +33 -2
- package/src/prd-core.js +10 -9
- package/src/product-type-copy.js +69 -0
- package/src/quality-html-artifact.js +108 -9
- package/src/quality-learning.js +30 -0
- package/src/quality-visual-review.js +237 -0
- package/src/quality.js +329 -43
- package/src/registry-hygiene.js +54 -0
- package/src/release-ledger.js +413 -0
- package/src/review-presentation.js +12 -6
- package/src/run-harness.js +722 -48
- package/src/self-update.js +1 -1
- package/src/session-binding.js +40 -3
- package/src/session-registry.js +159 -0
- package/src/standards.js +5 -3
- package/src/test-strategy.js +386 -0
- package/src/visual-compare.js +915 -34
- package/src/work-unit-migration.js +5 -1
- package/src/workspace-core.js +343 -19
- package/src/workspace-workflow.js +538 -134
package/src/run-harness.js
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { labelExecutionMode, taskExecutionStrategy, describeExecutionStrategy } from './execution-strategy.js';
|
|
2
4
|
import { appendJsonl, appendText, cjoin, exists, readJson, readJsonl, writeJson, writeText } from './fs-utils.js';
|
|
3
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
OPENPRD_HARNESS_TURN_STATE,
|
|
7
|
+
recordKnowledgeReviewSignal,
|
|
8
|
+
recordKnowledgeSkillAdoption,
|
|
9
|
+
resolveKnowledgeSkillMatches,
|
|
10
|
+
reviewKnowledgeWorkspace,
|
|
11
|
+
} from './knowledge.js';
|
|
4
12
|
import { readSessionBinding } from './session-binding.js';
|
|
13
|
+
import { readSessionRegistryEntry } from './session-registry.js';
|
|
5
14
|
import { timestamp } from './time.js';
|
|
15
|
+
import { readWorkspaceRegistry } from './workspace-registry.js';
|
|
6
16
|
|
|
7
17
|
const OPENPRD_HARNESS_DIR = cjoin('.openprd', 'harness');
|
|
8
18
|
const OPENPRD_HARNESS_RUN_STATE = cjoin(OPENPRD_HARNESS_DIR, 'run-state.json');
|
|
@@ -11,18 +21,28 @@ const OPENPRD_HARNESS_LEARNINGS = cjoin(OPENPRD_HARNESS_DIR, 'learnings.md');
|
|
|
11
21
|
const OPENPRD_HARNESS_LOOP_FEATURE_LIST = cjoin(OPENPRD_HARNESS_DIR, 'feature-list.json');
|
|
12
22
|
const OPENPRD_HARNESS_REQUIREMENT_GATE = cjoin(OPENPRD_HARNESS_DIR, 'requirement-gate.json');
|
|
13
23
|
const OPENPRD_HARNESS_REQUIREMENT_GATES_DIR = cjoin(OPENPRD_HARNESS_DIR, 'requirement-gates');
|
|
24
|
+
const OPENPRD_HARNESS_SESSION_BINDINGS_DIR = cjoin(OPENPRD_HARNESS_DIR, 'session-bindings');
|
|
14
25
|
const OPENPRD_HARNESS_EVENTS = cjoin(OPENPRD_HARNESS_DIR, 'events.jsonl');
|
|
15
26
|
const OPENPRD_WORK_UNITS_DIR = cjoin('.openprd', 'engagements', 'work-units');
|
|
27
|
+
const OPENPRD_PARALLEL_WORKER_IMPLEMENTATION_TASK_THRESHOLD = 3;
|
|
16
28
|
const OPENPRD_LOOP_REQUIRED_IMPLEMENTATION_TASK_THRESHOLD = 10;
|
|
17
29
|
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
30
|
const CONTINUATION_TASK_HANDLE_PATTERN = /\b[a-z0-9._-]+:T\d{3}\.\d{2}:[a-z0-9._-]+\b/i;
|
|
19
31
|
const CONTINUATION_WORK_UNIT_PATTERN = /\bwu-[a-z0-9._-]+\b/i;
|
|
20
|
-
const CONTINUATION_EXPLICIT_PATTERN = /(
|
|
32
|
+
const CONTINUATION_EXPLICIT_PATTERN = /(?:(?:继续|续做|接着做|继续执行|继续推进)(?:这个|这条|当前)?\s*(?:对话|任务|会话|记录|历史|Codex\s*任务)|(?:对话|任务|会话|记录|历史|Codex\s*任务).{0,6}(?:继续|续做|接着做|继续执行|继续推进)|^(?:继续|续做|接着做|继续执行|继续推进)\s*(?::|:))/i;
|
|
21
33
|
const CONTINUATION_CURRENT_PATTERN = /(继续当前|当前(这个|这条)?(任务|会话|记录|需求|变更)|current\s+(task|change|session)|resume current)/i;
|
|
34
|
+
const SHORT_AFFIRMATIVE_PATTERN = /^(可以|好|行|确认|没问题|OK|ok|yes|Yes|yep|Yep)[。!!,.,\s]*$/;
|
|
22
35
|
function harnessFile(projectRoot, relativePath) {
|
|
23
36
|
return cjoin(projectRoot, relativePath);
|
|
24
37
|
}
|
|
25
38
|
|
|
39
|
+
function rootsEqual(left, right) {
|
|
40
|
+
if (!left || !right) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
return path.resolve(left) === path.resolve(right);
|
|
44
|
+
}
|
|
45
|
+
|
|
26
46
|
async function ensureRunHarness(projectRoot) {
|
|
27
47
|
await fs.mkdir(harnessFile(projectRoot, OPENPRD_HARNESS_DIR), { recursive: true });
|
|
28
48
|
const statePath = harnessFile(projectRoot, OPENPRD_HARNESS_RUN_STATE);
|
|
@@ -74,6 +94,7 @@ function compactTask(task) {
|
|
|
74
94
|
if (!task) {
|
|
75
95
|
return null;
|
|
76
96
|
}
|
|
97
|
+
const executionStrategy = taskExecutionStrategy(task);
|
|
77
98
|
return {
|
|
78
99
|
id: task.id,
|
|
79
100
|
taskHandle: task.taskHandle ?? null,
|
|
@@ -85,6 +106,60 @@ function compactTask(task) {
|
|
|
85
106
|
oracle: task.metadata?.oracle ?? null,
|
|
86
107
|
deps: task.metadata?.deps ?? null,
|
|
87
108
|
type: task.metadata?.type ?? task.metadata?.category ?? task.metadata?.kind ?? null,
|
|
109
|
+
executionStrategy,
|
|
110
|
+
executionStrategyDescription: describeExecutionStrategy(executionStrategy),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function workerCandidateFromTask(task) {
|
|
115
|
+
const executionStrategy = taskExecutionStrategy(task);
|
|
116
|
+
if (executionStrategy.ownerRole !== 'worker') {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
id: task.id,
|
|
121
|
+
title: task.title,
|
|
122
|
+
parallelGroup: executionStrategy.parallelGroup,
|
|
123
|
+
writeScope: executionStrategy.writeScope,
|
|
124
|
+
localVerify: executionStrategy.localVerify,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function buildParallelPlan({ executionMode, taskState, focusTask, worktreeRecommended = false }) {
|
|
129
|
+
const eligible = executionMode !== 'serial';
|
|
130
|
+
const workerCandidates = (taskState?.tasks ?? [])
|
|
131
|
+
.filter((task) => !task.checked)
|
|
132
|
+
.map(workerCandidateFromTask)
|
|
133
|
+
.filter(Boolean);
|
|
134
|
+
const groups = [...new Set(workerCandidates.map((task) => task.parallelGroup))];
|
|
135
|
+
const focusCandidate = focusTask?.executionStrategy?.ownerRole === 'worker'
|
|
136
|
+
? {
|
|
137
|
+
id: focusTask.id,
|
|
138
|
+
title: focusTask.title,
|
|
139
|
+
parallelGroup: focusTask.executionStrategy.parallelGroup,
|
|
140
|
+
writeScope: focusTask.executionStrategy.writeScope,
|
|
141
|
+
localVerify: focusTask.executionStrategy.localVerify,
|
|
142
|
+
}
|
|
143
|
+
: workerCandidates.find((task) => task.id === focusTask?.id) ?? null;
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
eligible,
|
|
147
|
+
coordinator: 'main-agent',
|
|
148
|
+
integrationOwner: 'main-agent',
|
|
149
|
+
worktreeRecommended,
|
|
150
|
+
shardBasis: eligible ? 'write-scope-and-parallel-group' : 'single-thread',
|
|
151
|
+
suggestedWorkers: eligible ? Math.min(workerCandidates.length, worktreeRecommended ? 4 : 3) : 0,
|
|
152
|
+
workerTaskCount: workerCandidates.length,
|
|
153
|
+
groups,
|
|
154
|
+
focusTask: focusCandidate,
|
|
155
|
+
workerCandidates: workerCandidates.slice(0, worktreeRecommended ? 4 : 3),
|
|
156
|
+
summary: eligible
|
|
157
|
+
? (
|
|
158
|
+
worktreeRecommended
|
|
159
|
+
? '建议主 Agent 按 write-scope 和 parallel-group 把任务分给多个隔离 worker,会后由主 Agent 统一做集成审查和总验证。'
|
|
160
|
+
: '建议主 Agent 按 write-scope 和 parallel-group 分配边界清晰的 worker shard,worker 先做局部实现和局部验证,再由主 Agent 收口。'
|
|
161
|
+
)
|
|
162
|
+
: '当前任务保持主 Agent 串行推进即可。',
|
|
88
163
|
};
|
|
89
164
|
}
|
|
90
165
|
|
|
@@ -132,9 +207,83 @@ function reviewMarkCommand(snapshot) {
|
|
|
132
207
|
function executionGate() {
|
|
133
208
|
return {
|
|
134
209
|
requiresExplicitIntent: true,
|
|
210
|
+
confirmationChecklistRequired: true,
|
|
135
211
|
allowedIntents: ['开发', '实现', '修复', '继续任务', '落地执行', '深度调研', '深度对标', '复刻落地', '提交'],
|
|
136
212
|
readOnlyIntents: ['看看', '规划', '梳理', '分析', '评估', '预计动哪些文件', '怎么改', '代码审查'],
|
|
137
|
-
rule: '只有当用户当前明确要求实现、继续、深度调研、对标或提交时,才运行 executionCommand
|
|
213
|
+
rule: '只有当用户当前明确要求实现、继续、深度调研、对标或提交时,才运行 executionCommand。单纯的“请帮我实现/继续实现”只表示有执行意图,不表示可以跳过 requirement 摘要确认、`capture/classify/synthesize` 写入路径或 review;只有用户明确表示“不需要进行任何确认”时,才允许静默走完整 requirement write path,并对当前精确匹配的稳定 review artifact 记录确认。若还需要向用户索取执行授权,先展示 executionConfirmationChecklist,再请求明确确认;规划、分析、文件影响范围和审查类请求保持只读,并基于证据回答。',
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function buildExecutionConfirmationChecklist(recommendation) {
|
|
218
|
+
if (!recommendation?.executionCommand) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
const parallelPlan = recommendation.parallelPlan ?? null;
|
|
222
|
+
const scope = [
|
|
223
|
+
'只在这次已经确认的范围内继续推进',
|
|
224
|
+
parallelPlan?.eligible ? '如果需要分头推进,会先划清各自负责的部分,再统一收口' : null,
|
|
225
|
+
].filter(Boolean);
|
|
226
|
+
return {
|
|
227
|
+
required: true,
|
|
228
|
+
title: '开始动手前先确认这些',
|
|
229
|
+
objective: recommendation.title ?? '继续推进本次调整',
|
|
230
|
+
scope,
|
|
231
|
+
implementationItems: [
|
|
232
|
+
'我会先核对当前情况,再继续整理后续落地内容。',
|
|
233
|
+
'我只会在这次已经确认的范围内继续,不会顺手扩到别的事项。',
|
|
234
|
+
parallelPlan?.eligible ? '如果需要多人配合,我会先划清边界,再统一收口检查。' : null,
|
|
235
|
+
].filter(Boolean),
|
|
236
|
+
outOfScope: [
|
|
237
|
+
'不会默认处理这次范围以外的历史问题。',
|
|
238
|
+
'不会默认顺带做提交、发布或额外的全局调整。',
|
|
239
|
+
'如果真的需要扩大范围,我会先单独说明。',
|
|
240
|
+
].filter(Boolean),
|
|
241
|
+
verification: [
|
|
242
|
+
'完成后我会补做这次调整需要的检查。',
|
|
243
|
+
parallelPlan?.eligible ? '如果是分头推进,会先各自自检,再统一做总检查。' : null,
|
|
244
|
+
'在宣布完成前,我会再做一次整体核对。',
|
|
245
|
+
].filter(Boolean),
|
|
246
|
+
risks: [
|
|
247
|
+
'这一步会正式写入当前工作区。',
|
|
248
|
+
'如果牵出和本次无关的历史遗留问题,我会单列说明,不把它混成本次失败。',
|
|
249
|
+
parallelPlan?.eligible ? '如果多人同时推进,我会避免范围互相踩踏。' : null,
|
|
250
|
+
],
|
|
251
|
+
confirmationPrompt: '如果你希望我现在就按这次范围继续,我就直接往下做。',
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function recommendationNeedsExtraExecutionConfirmation(recommendation, requirementGate) {
|
|
256
|
+
if (!recommendation?.executionCommand) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
const reviewContinuationAuthorized = Boolean(requirementGate?.reviewActionAuthorization?.continueAfterReview);
|
|
260
|
+
const silentReviewRecordingAuthorized = requirementGate?.status === 'review-recording-authorized';
|
|
261
|
+
const implementationAlreadyAuthorized = requirementGate?.status === 'execution-authorized';
|
|
262
|
+
const explicitExecution = Boolean(requirementGate?.intent?.explicitExecution);
|
|
263
|
+
|
|
264
|
+
if (recommendation.type === 'prd-change') {
|
|
265
|
+
return !(reviewContinuationAuthorized || silentReviewRecordingAuthorized);
|
|
266
|
+
}
|
|
267
|
+
if (recommendation.type === 'task' || recommendation.type === 'loop-task') {
|
|
268
|
+
return !(implementationAlreadyAuthorized || (explicitExecution && (reviewContinuationAuthorized || silentReviewRecordingAuthorized)));
|
|
269
|
+
}
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function withExecutionConfirmationChecklist(recommendation, options = {}) {
|
|
274
|
+
if (!recommendationNeedsExtraExecutionConfirmation(recommendation, options.requirementGate)) {
|
|
275
|
+
return {
|
|
276
|
+
...recommendation,
|
|
277
|
+
executionConfirmationChecklist: null,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const checklist = buildExecutionConfirmationChecklist(recommendation);
|
|
281
|
+
if (!checklist) {
|
|
282
|
+
return recommendation;
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
...recommendation,
|
|
286
|
+
executionConfirmationChecklist: checklist,
|
|
138
287
|
};
|
|
139
288
|
}
|
|
140
289
|
|
|
@@ -160,7 +309,7 @@ function analyzeRunMessage(message = null) {
|
|
|
160
309
|
const workUnitId = text.match(CONTINUATION_WORK_UNIT_PATTERN)?.[0] ?? null;
|
|
161
310
|
const explicit = CONTINUATION_EXPLICIT_PATTERN.test(text);
|
|
162
311
|
const explicitCurrent = CONTINUATION_CURRENT_PATTERN.test(text);
|
|
163
|
-
const requested = explicit || Boolean(
|
|
312
|
+
const requested = explicit || Boolean(taskHandle || workUnitId);
|
|
164
313
|
if (!requested) {
|
|
165
314
|
return {
|
|
166
315
|
kind: 'default',
|
|
@@ -200,6 +349,14 @@ function normalizeSearchText(value) {
|
|
|
200
349
|
.trim();
|
|
201
350
|
}
|
|
202
351
|
|
|
352
|
+
function stripMarkdownText(value) {
|
|
353
|
+
return String(value ?? '')
|
|
354
|
+
.replace(/\[[^\]]*]\([^)]+\)/g, ' ')
|
|
355
|
+
.replace(/[`*_>#~-]/g, ' ')
|
|
356
|
+
.replace(/\s+/g, ' ')
|
|
357
|
+
.trim();
|
|
358
|
+
}
|
|
359
|
+
|
|
203
360
|
function uniqueItems(items) {
|
|
204
361
|
return [...new Set(items.filter(Boolean))];
|
|
205
362
|
}
|
|
@@ -263,6 +420,77 @@ function scoreSearchCandidate(query, fields = []) {
|
|
|
263
420
|
return bestScore;
|
|
264
421
|
}
|
|
265
422
|
|
|
423
|
+
function isShortAffirmativeMessage(message) {
|
|
424
|
+
return SHORT_AFFIRMATIVE_PATTERN.test(stripMarkdownText(message));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function requirementGateMatchesMessage(message, gate) {
|
|
428
|
+
if (!gate?.active) {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
const text = String(message ?? '').trim();
|
|
432
|
+
if (!text || isShortAffirmativeMessage(text)) {
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
const gateFields = [
|
|
436
|
+
gate.promptPreview,
|
|
437
|
+
gate.reviewActionAuthorization?.promptPreview,
|
|
438
|
+
].filter(Boolean);
|
|
439
|
+
if (gateFields.length === 0) {
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
const queryTokens = searchTokens(text);
|
|
443
|
+
const gateTokens = uniqueItems(gateFields.flatMap((field) => searchTokens(field)));
|
|
444
|
+
const sharedTokens = queryTokens.filter((token) => gateTokens.includes(token));
|
|
445
|
+
if (sharedTokens.some((token) => token.length >= 6)) {
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
const overlapBase = Math.min(queryTokens.length, gateTokens.length);
|
|
449
|
+
if (sharedTokens.length >= 2 && overlapBase > 0 && (sharedTokens.length / overlapBase) >= 0.5) {
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
return scoreSearchCandidate(text, gateFields) >= 80;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function assessRequirementGateRelevance({ message, gate, laneRequest, resolvedTarget }) {
|
|
456
|
+
if (!gate?.active) {
|
|
457
|
+
return {
|
|
458
|
+
active: false,
|
|
459
|
+
matchedCurrentMessage: false,
|
|
460
|
+
relevance: 'inactive',
|
|
461
|
+
reason: null,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
if (laneRequest?.requested || resolvedTarget?.matched) {
|
|
465
|
+
return {
|
|
466
|
+
active: true,
|
|
467
|
+
matchedCurrentMessage: false,
|
|
468
|
+
relevance: 'background',
|
|
469
|
+
reason: '当前请求正在显式继续历史会话/任务,或已定位到更具体目标;active requirement intake 仅作为背景提醒。',
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
const matchedCurrentMessage = requirementGateMatchesMessage(message, gate);
|
|
473
|
+
return {
|
|
474
|
+
active: true,
|
|
475
|
+
matchedCurrentMessage,
|
|
476
|
+
relevance: matchedCurrentMessage ? 'primary' : 'background',
|
|
477
|
+
reason: matchedCurrentMessage
|
|
478
|
+
? '当前请求与 active requirement intake 匹配,优先继续这条需求入口。'
|
|
479
|
+
: '当前请求与 active requirement intake 的摘要不匹配;旧 gate 仅作为背景提醒,不抢本轮默认路线。',
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function appendRequirementGateReminder(reason, requirementGateAssessment, requirementGate) {
|
|
484
|
+
if (requirementGateAssessment?.relevance !== 'background' || !requirementGate?.active) {
|
|
485
|
+
return reason;
|
|
486
|
+
}
|
|
487
|
+
const reminder = [
|
|
488
|
+
requirementGateAssessment.reason,
|
|
489
|
+
requirementGate?.promptPreview ? `历史需求摘要: ${requirementGate.promptPreview}` : null,
|
|
490
|
+
].filter(Boolean).join(' ');
|
|
491
|
+
return [reason, reminder].filter(Boolean).join(' ');
|
|
492
|
+
}
|
|
493
|
+
|
|
266
494
|
async function readFirstHeading(filePath, fallback = null) {
|
|
267
495
|
const text = await fs.readFile(filePath, 'utf8').catch(() => '');
|
|
268
496
|
const heading = text.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? null;
|
|
@@ -540,13 +768,43 @@ async function readSessionEvents(projectRoot, sessionId) {
|
|
|
540
768
|
return events.filter((event) => event?.sessionId === sessionId);
|
|
541
769
|
}
|
|
542
770
|
|
|
543
|
-
async function
|
|
771
|
+
async function readLoopFeatureList(projectRoot) {
|
|
772
|
+
return readJson(harnessFile(projectRoot, OPENPRD_HARNESS_LOOP_FEATURE_LIST)).catch(() => null);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
async function findSessionWorkspaceCandidates(projectRoot, sessionId, options = {}) {
|
|
776
|
+
const registry = await readWorkspaceRegistry(options).catch(() => null);
|
|
777
|
+
if (!registry) {
|
|
778
|
+
return [];
|
|
779
|
+
}
|
|
780
|
+
const currentRoot = path.resolve(projectRoot);
|
|
781
|
+
const candidates = [];
|
|
782
|
+
for (const entry of registry.entries) {
|
|
783
|
+
if (rootsEqual(entry.workspaceRoot, currentRoot)) {
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
const bindingPath = cjoin(entry.workspaceRoot, OPENPRD_HARNESS_SESSION_BINDINGS_DIR, `${sessionId}.json`);
|
|
787
|
+
const gatePath = cjoin(entry.workspaceRoot, OPENPRD_HARNESS_REQUIREMENT_GATES_DIR, `${sessionId}.json`);
|
|
788
|
+
if (await exists(bindingPath)) {
|
|
789
|
+
candidates.push({ workspaceRoot: entry.workspaceRoot, source: 'session-binding' });
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
if (await exists(gatePath)) {
|
|
793
|
+
candidates.push({ workspaceRoot: entry.workspaceRoot, source: 'requirement-gate' });
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return candidates;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
async function resolveSessionTargetInWorkspace(projectRoot, sessionId, index, loopFeatureList, options = {}) {
|
|
544
800
|
const binding = await readSessionBinding(projectRoot, sessionId);
|
|
545
801
|
const gate = await readSessionRequirementGate(projectRoot, sessionId);
|
|
546
802
|
const events = await readSessionEvents(projectRoot, sessionId);
|
|
547
803
|
const directBindingArtifacts = binding ? { sessionBinding: true } : {};
|
|
804
|
+
const workspaceRoot = path.resolve(projectRoot);
|
|
548
805
|
if (binding?.taskHandle) {
|
|
549
|
-
return
|
|
806
|
+
return {
|
|
807
|
+
...(resolveTaskHandleTarget(binding.taskHandle, index, loopFeatureList, {
|
|
550
808
|
source: 'session-binding',
|
|
551
809
|
sessionId,
|
|
552
810
|
workUnitId: binding.workUnitId ?? null,
|
|
@@ -557,7 +815,10 @@ async function resolveSessionTarget(projectRoot, sessionId, index, loopFeatureLi
|
|
|
557
815
|
events: events.length,
|
|
558
816
|
},
|
|
559
817
|
reason: `会话 ${sessionId} 的 lane 绑定命中任务句柄 ${binding.taskHandle}。`,
|
|
560
|
-
|
|
818
|
+
}) ?? {}),
|
|
819
|
+
workspaceRoot,
|
|
820
|
+
sameWorkspace: options.sameWorkspace ?? true,
|
|
821
|
+
};
|
|
561
822
|
}
|
|
562
823
|
if (binding?.changeId) {
|
|
563
824
|
return {
|
|
@@ -576,6 +837,8 @@ async function resolveSessionTarget(projectRoot, sessionId, index, loopFeatureLi
|
|
|
576
837
|
requirementGate: Boolean(gate),
|
|
577
838
|
events: events.length,
|
|
578
839
|
},
|
|
840
|
+
workspaceRoot,
|
|
841
|
+
sameWorkspace: options.sameWorkspace ?? true,
|
|
579
842
|
};
|
|
580
843
|
}
|
|
581
844
|
if (binding?.workUnitId) {
|
|
@@ -596,6 +859,8 @@ async function resolveSessionTarget(projectRoot, sessionId, index, loopFeatureLi
|
|
|
596
859
|
...boundWorkUnitTarget,
|
|
597
860
|
changeId: boundWorkUnitTarget.changeId ?? binding.changeId ?? null,
|
|
598
861
|
title: boundWorkUnitTarget.title ?? binding.title ?? null,
|
|
862
|
+
workspaceRoot,
|
|
863
|
+
sameWorkspace: options.sameWorkspace ?? true,
|
|
599
864
|
};
|
|
600
865
|
}
|
|
601
866
|
}
|
|
@@ -619,18 +884,22 @@ async function resolveSessionTarget(projectRoot, sessionId, index, loopFeatureLi
|
|
|
619
884
|
};
|
|
620
885
|
const taskHandle = extractFirstSelectorMatch(texts, CONTINUATION_TASK_HANDLE_PATTERN);
|
|
621
886
|
if (taskHandle) {
|
|
622
|
-
return
|
|
887
|
+
return {
|
|
888
|
+
...(resolveTaskHandleTarget(taskHandle, index, loopFeatureList, {
|
|
623
889
|
source: 'session',
|
|
624
890
|
sessionId,
|
|
625
891
|
promptPreview,
|
|
626
892
|
artifacts,
|
|
627
893
|
reason: `会话 ${sessionId} 的本地记录命中任务句柄 ${taskHandle}。`,
|
|
628
|
-
|
|
894
|
+
}) ?? {}),
|
|
895
|
+
workspaceRoot,
|
|
896
|
+
sameWorkspace: options.sameWorkspace ?? true,
|
|
897
|
+
};
|
|
629
898
|
}
|
|
630
899
|
const workUnitId = gate?.reviewActionAuthorization?.workUnitId
|
|
631
900
|
?? extractFirstSelectorMatch(texts, CONTINUATION_WORK_UNIT_PATTERN);
|
|
632
901
|
if (workUnitId) {
|
|
633
|
-
|
|
902
|
+
const resolvedWorkUnitTarget = await resolveWorkUnitTarget(projectRoot, workUnitId, index, {
|
|
634
903
|
source: 'session',
|
|
635
904
|
sessionId,
|
|
636
905
|
promptPreview,
|
|
@@ -638,6 +907,11 @@ async function resolveSessionTarget(projectRoot, sessionId, index, loopFeatureLi
|
|
|
638
907
|
query: texts.join(' '),
|
|
639
908
|
reason: `会话 ${sessionId} 的本地记录命中工作单元 ${workUnitId}。`,
|
|
640
909
|
});
|
|
910
|
+
return {
|
|
911
|
+
...resolvedWorkUnitTarget,
|
|
912
|
+
workspaceRoot,
|
|
913
|
+
sameWorkspace: options.sameWorkspace ?? true,
|
|
914
|
+
};
|
|
641
915
|
}
|
|
642
916
|
const semanticMatch = resolveSemanticTarget(texts.join(' '), index, {
|
|
643
917
|
source: 'session',
|
|
@@ -647,7 +921,11 @@ async function resolveSessionTarget(projectRoot, sessionId, index, loopFeatureLi
|
|
|
647
921
|
reason: `会话 ${sessionId} 的本地 requirement / hook 历史命中已有任务对象。`,
|
|
648
922
|
});
|
|
649
923
|
if (semanticMatch) {
|
|
650
|
-
return
|
|
924
|
+
return {
|
|
925
|
+
...semanticMatch,
|
|
926
|
+
workspaceRoot,
|
|
927
|
+
sameWorkspace: options.sameWorkspace ?? true,
|
|
928
|
+
};
|
|
651
929
|
}
|
|
652
930
|
return {
|
|
653
931
|
matched: false,
|
|
@@ -663,22 +941,98 @@ async function resolveSessionTarget(projectRoot, sessionId, index, loopFeatureLi
|
|
|
663
941
|
? `本地找到了会话 ${sessionId} 的 requirement gate / hook 事件,但还没有足够证据绑定到具体 change/task/work unit。`
|
|
664
942
|
: `本地没有会话 ${sessionId} 的 requirement gate、hook 事件或 work unit 绑定。`,
|
|
665
943
|
artifacts,
|
|
944
|
+
workspaceRoot,
|
|
945
|
+
sameWorkspace: options.sameWorkspace ?? true,
|
|
666
946
|
};
|
|
667
947
|
}
|
|
668
948
|
|
|
949
|
+
async function resolveSessionTarget(projectRoot, sessionId, index, loopFeatureList, options = {}) {
|
|
950
|
+
const registryEntry = await readSessionRegistryEntry(sessionId, options).catch(() => null);
|
|
951
|
+
if (registryEntry?.workspaceRoot && !rootsEqual(registryEntry.workspaceRoot, projectRoot)) {
|
|
952
|
+
const targetArtifacts = await options.resolveWorkspaceArtifacts?.(registryEntry.workspaceRoot) ?? {
|
|
953
|
+
index,
|
|
954
|
+
loopFeatureList: await readLoopFeatureList(registryEntry.workspaceRoot),
|
|
955
|
+
};
|
|
956
|
+
const resolved = await resolveSessionTargetInWorkspace(
|
|
957
|
+
registryEntry.workspaceRoot,
|
|
958
|
+
sessionId,
|
|
959
|
+
targetArtifacts.index ?? index,
|
|
960
|
+
targetArtifacts.loopFeatureList ?? loopFeatureList,
|
|
961
|
+
{ sameWorkspace: false },
|
|
962
|
+
);
|
|
963
|
+
return {
|
|
964
|
+
...resolved,
|
|
965
|
+
workspaceRoot: registryEntry.workspaceRoot,
|
|
966
|
+
registryEntry,
|
|
967
|
+
reason: [
|
|
968
|
+
`全局 session registry 已把会话 ${sessionId} 归属到 ${registryEntry.workspaceRoot}。`,
|
|
969
|
+
resolved.reason,
|
|
970
|
+
].filter(Boolean).join(' '),
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const localResolution = await resolveSessionTargetInWorkspace(projectRoot, sessionId, index, loopFeatureList, {
|
|
975
|
+
sameWorkspace: true,
|
|
976
|
+
});
|
|
977
|
+
if (localResolution.matched || registryEntry) {
|
|
978
|
+
return {
|
|
979
|
+
...localResolution,
|
|
980
|
+
registryEntry,
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const candidates = await findSessionWorkspaceCandidates(projectRoot, sessionId, options);
|
|
985
|
+
if (candidates.length === 1) {
|
|
986
|
+
const candidate = candidates[0];
|
|
987
|
+
const targetArtifacts = await options.resolveWorkspaceArtifacts?.(candidate.workspaceRoot) ?? {
|
|
988
|
+
index,
|
|
989
|
+
loopFeatureList: await readLoopFeatureList(candidate.workspaceRoot),
|
|
990
|
+
};
|
|
991
|
+
const resolved = await resolveSessionTargetInWorkspace(
|
|
992
|
+
candidate.workspaceRoot,
|
|
993
|
+
sessionId,
|
|
994
|
+
targetArtifacts.index ?? index,
|
|
995
|
+
targetArtifacts.loopFeatureList ?? loopFeatureList,
|
|
996
|
+
{ sameWorkspace: false },
|
|
997
|
+
);
|
|
998
|
+
return {
|
|
999
|
+
...resolved,
|
|
1000
|
+
workspaceRoot: candidate.workspaceRoot,
|
|
1001
|
+
candidates,
|
|
1002
|
+
reason: [
|
|
1003
|
+
`全局 session registry 还没有会话 ${sessionId},已根据 repo-local 线索定位到候选工作区 ${candidate.workspaceRoot}。`,
|
|
1004
|
+
resolved.reason,
|
|
1005
|
+
].filter(Boolean).join(' '),
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
if (candidates.length > 1) {
|
|
1009
|
+
return {
|
|
1010
|
+
...localResolution,
|
|
1011
|
+
source: 'session-candidates',
|
|
1012
|
+
workspaceRoot: null,
|
|
1013
|
+
candidates,
|
|
1014
|
+
reason: `全局 session registry 还没有会话 ${sessionId},并且在多个工作区都找到了候选线索:${candidates.map((item) => item.workspaceRoot).join('、')}。`,
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
return localResolution;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
669
1020
|
async function resolveRunTarget({
|
|
670
1021
|
projectRoot,
|
|
671
1022
|
message,
|
|
672
1023
|
request,
|
|
673
1024
|
index,
|
|
674
1025
|
loopFeatureList,
|
|
1026
|
+
resolveWorkspaceArtifacts,
|
|
675
1027
|
}) {
|
|
676
1028
|
const text = String(message ?? '').trim();
|
|
677
1029
|
if (!text) {
|
|
678
1030
|
return null;
|
|
679
1031
|
}
|
|
680
1032
|
if (request.sessionId) {
|
|
681
|
-
return resolveSessionTarget(projectRoot, request.sessionId, index, loopFeatureList
|
|
1033
|
+
return resolveSessionTarget(projectRoot, request.sessionId, index, loopFeatureList, {
|
|
1034
|
+
resolveWorkspaceArtifacts,
|
|
1035
|
+
});
|
|
682
1036
|
}
|
|
683
1037
|
if (request.taskHandle) {
|
|
684
1038
|
return resolveTaskHandleTarget(request.taskHandle, index, loopFeatureList);
|
|
@@ -694,8 +1048,8 @@ async function resolveRunTarget({
|
|
|
694
1048
|
return resolveSemanticTarget(text, index);
|
|
695
1049
|
}
|
|
696
1050
|
|
|
697
|
-
function selectFocusedChangeId(request, resolvedTarget, activeChange) {
|
|
698
|
-
if (resolvedTarget?.changeId) {
|
|
1051
|
+
function selectFocusedChangeId(projectRoot, request, resolvedTarget, activeChange) {
|
|
1052
|
+
if (resolvedTarget?.changeId && (!resolvedTarget.workspaceRoot || rootsEqual(resolvedTarget.workspaceRoot, projectRoot))) {
|
|
699
1053
|
return resolvedTarget.changeId;
|
|
700
1054
|
}
|
|
701
1055
|
if (request.sessionId || request.taskHandle || request.workUnitId) {
|
|
@@ -733,7 +1087,7 @@ function describeRunLane(lane) {
|
|
|
733
1087
|
return `继续已有任务 (${selectorLabel}: ${target})`;
|
|
734
1088
|
}
|
|
735
1089
|
|
|
736
|
-
function buildRunLane({ message, recommendation, activeChange, latestPrd, loopFeatureList, resolvedTarget }) {
|
|
1090
|
+
function buildRunLane({ message, recommendation, activeChange, latestPrd, loopFeatureList, resolvedTarget, projectRoot }) {
|
|
737
1091
|
const request = analyzeRunMessage(message);
|
|
738
1092
|
if (!request.requested) {
|
|
739
1093
|
if (resolvedTarget?.matched) {
|
|
@@ -743,6 +1097,7 @@ function buildRunLane({ message, recommendation, activeChange, latestPrd, loopFe
|
|
|
743
1097
|
taskId: resolvedTarget.taskId ?? recommendation?.task?.id ?? null,
|
|
744
1098
|
changeId: resolvedTarget.changeId ?? recommendation?.changeId ?? null,
|
|
745
1099
|
workUnitId: resolvedTarget.workUnitId ?? latestPrd?.workUnitId ?? null,
|
|
1100
|
+
workspaceRoot: resolvedTarget.workspaceRoot ?? projectRoot,
|
|
746
1101
|
};
|
|
747
1102
|
const lane = {
|
|
748
1103
|
kind: 'targeted',
|
|
@@ -753,6 +1108,7 @@ function buildRunLane({ message, recommendation, activeChange, latestPrd, loopFe
|
|
|
753
1108
|
matched: Boolean(target.sessionId || target.taskHandle || target.taskId || target.changeId || target.workUnitId),
|
|
754
1109
|
resolution: resolvedTarget,
|
|
755
1110
|
activeChange,
|
|
1111
|
+
currentProjectRoot: projectRoot,
|
|
756
1112
|
};
|
|
757
1113
|
return {
|
|
758
1114
|
...lane,
|
|
@@ -776,6 +1132,7 @@ function buildRunLane({ message, recommendation, activeChange, latestPrd, loopFe
|
|
|
776
1132
|
taskId: resolvedTarget?.taskId ?? null,
|
|
777
1133
|
changeId: resolvedTarget?.changeId ?? null,
|
|
778
1134
|
workUnitId: resolvedTarget?.workUnitId ?? null,
|
|
1135
|
+
workspaceRoot: resolvedTarget?.workspaceRoot ?? projectRoot,
|
|
779
1136
|
};
|
|
780
1137
|
matched = Boolean(resolvedTarget?.matched);
|
|
781
1138
|
} else if (request.selectorType === 'task-handle') {
|
|
@@ -785,6 +1142,7 @@ function buildRunLane({ message, recommendation, activeChange, latestPrd, loopFe
|
|
|
785
1142
|
taskId: resolvedTarget?.taskId ?? matchedLoopTask?.id ?? null,
|
|
786
1143
|
changeId: resolvedTarget?.changeId ?? matchedLoopTask?.changeId ?? null,
|
|
787
1144
|
workUnitId: resolvedTarget?.workUnitId ?? null,
|
|
1145
|
+
workspaceRoot: resolvedTarget?.workspaceRoot ?? projectRoot,
|
|
788
1146
|
};
|
|
789
1147
|
matched = Boolean(resolvedTarget?.matched || matchedLoopTask);
|
|
790
1148
|
} else if (request.selectorType === 'work-unit') {
|
|
@@ -794,6 +1152,7 @@ function buildRunLane({ message, recommendation, activeChange, latestPrd, loopFe
|
|
|
794
1152
|
taskId: resolvedTarget?.taskId ?? null,
|
|
795
1153
|
changeId: resolvedTarget?.changeId ?? null,
|
|
796
1154
|
workUnitId: resolvedTarget?.workUnitId ?? request.workUnitId ?? null,
|
|
1155
|
+
workspaceRoot: resolvedTarget?.workspaceRoot ?? projectRoot,
|
|
797
1156
|
};
|
|
798
1157
|
matched = Boolean(resolvedTarget?.matched);
|
|
799
1158
|
} else {
|
|
@@ -803,6 +1162,7 @@ function buildRunLane({ message, recommendation, activeChange, latestPrd, loopFe
|
|
|
803
1162
|
taskId: recommendation?.task?.id ?? null,
|
|
804
1163
|
changeId: recommendation?.changeId ?? activeChange ?? null,
|
|
805
1164
|
workUnitId: latestPrd?.workUnitId ?? null,
|
|
1165
|
+
workspaceRoot: projectRoot,
|
|
806
1166
|
};
|
|
807
1167
|
matched = Boolean(target.taskHandle || target.taskId || target.changeId || target.workUnitId);
|
|
808
1168
|
}
|
|
@@ -812,6 +1172,7 @@ function buildRunLane({ message, recommendation, activeChange, latestPrd, loopFe
|
|
|
812
1172
|
matched,
|
|
813
1173
|
resolution: resolvedTarget ?? null,
|
|
814
1174
|
activeChange,
|
|
1175
|
+
currentProjectRoot: projectRoot,
|
|
815
1176
|
};
|
|
816
1177
|
return {
|
|
817
1178
|
...lane,
|
|
@@ -821,6 +1182,8 @@ function buildRunLane({ message, recommendation, activeChange, latestPrd, loopFe
|
|
|
821
1182
|
|
|
822
1183
|
function buildSessionContinuationRecommendation(recommendation, lane) {
|
|
823
1184
|
const sessionId = lane?.target?.sessionId ?? lane?.sessionId ?? lane?.selector ?? null;
|
|
1185
|
+
const targetWorkspaceRoot = lane?.target?.workspaceRoot ?? lane?.resolution?.workspaceRoot ?? lane?.currentProjectRoot ?? null;
|
|
1186
|
+
const crossWorkspace = Boolean(targetWorkspaceRoot && lane?.currentProjectRoot && !rootsEqual(targetWorkspaceRoot, lane.currentProjectRoot));
|
|
824
1187
|
const recoveredTarget = [
|
|
825
1188
|
lane?.target?.changeId ? `变更 ${lane.target.changeId}` : null,
|
|
826
1189
|
lane?.target?.taskHandle ? `任务句柄 ${lane.target.taskHandle}` : null,
|
|
@@ -830,11 +1193,16 @@ function buildSessionContinuationRecommendation(recommendation, lane) {
|
|
|
830
1193
|
type: 'session-continuation',
|
|
831
1194
|
title: sessionId ? `恢复历史会话 ${sessionId}` : '恢复历史会话',
|
|
832
1195
|
command: sessionId
|
|
833
|
-
?
|
|
1196
|
+
? (
|
|
1197
|
+
crossWorkspace
|
|
1198
|
+
? `openprd run ${shellQuote(targetWorkspaceRoot)} --context --message ${shellQuote(sessionId)}`
|
|
1199
|
+
: `openprd run . --context --message ${shellQuote(sessionId)}`
|
|
1200
|
+
)
|
|
834
1201
|
: 'openprd run . --context',
|
|
835
1202
|
verifyCommand: 'openprd run . --verify',
|
|
836
1203
|
reason: [
|
|
837
|
-
'当前请求给出的是工具无关的会话 ID
|
|
1204
|
+
'当前请求给出的是工具无关的会话 ID;先按全局 session registry 和 repo-local 线索恢复该会话历史,再决定后续任务对象。',
|
|
1205
|
+
crossWorkspace ? `该会话归属到工作区 ${targetWorkspaceRoot},不能继续复用当前工作区的 active 状态。` : null,
|
|
838
1206
|
recoveredTarget ? `本地已恢复到 ${recoveredTarget}。` : (lane?.resolution?.reason ?? '本地还没有足够证据把这个会话绑定到具体 change/task/work unit。'),
|
|
839
1207
|
lane?.resolution?.promptPreview ? `会话摘要: ${lane.resolution.promptPreview}` : null,
|
|
840
1208
|
'不能用相似历史、当前 active change 或当前 requirement gate 替代这个会话 ID。',
|
|
@@ -852,6 +1220,11 @@ function buildSessionContinuationRecommendation(recommendation, lane) {
|
|
|
852
1220
|
: null,
|
|
853
1221
|
coverageItem: null,
|
|
854
1222
|
continuationTarget: lane.target ?? null,
|
|
1223
|
+
isolation: {
|
|
1224
|
+
required: true,
|
|
1225
|
+
worktreeRecommended: true,
|
|
1226
|
+
reason: crossWorkspace ? '跨工作区恢复时先回到正确 workspace,再在隔离环境里继续实现。' : '历史会话恢复默认建议使用独立 session / cwd,避免共享执行线内容。',
|
|
1227
|
+
},
|
|
855
1228
|
previousRecommendation: recommendation
|
|
856
1229
|
? {
|
|
857
1230
|
type: recommendation.type ?? null,
|
|
@@ -881,6 +1254,9 @@ function buildUnresolvedContinuationRecommendation({ message, request, resolutio
|
|
|
881
1254
|
`当前请求显式给出了${selectorLabel},但本地 OpenPrd 索引还不能把它精确绑定到 change/task/work unit。`,
|
|
882
1255
|
resolution?.reason ?? null,
|
|
883
1256
|
activeChange ? `当前工作区 active change ${activeChange} 只作为背景提醒,不会自动顶替这个显式目标。` : null,
|
|
1257
|
+
Array.isArray(resolution?.candidates) && resolution.candidates.length > 0
|
|
1258
|
+
? `候选工作区: ${resolution.candidates.map((item) => item.workspaceRoot).join('、')}。`
|
|
1259
|
+
: null,
|
|
884
1260
|
].filter(Boolean).join(' '),
|
|
885
1261
|
changeId: null,
|
|
886
1262
|
task: null,
|
|
@@ -958,11 +1334,11 @@ function buildPrdPromotionRecommendation({ changes, next }) {
|
|
|
958
1334
|
|
|
959
1335
|
return {
|
|
960
1336
|
type: 'prd-change',
|
|
961
|
-
title:
|
|
1337
|
+
title: '整理本次调整,并拆出后续任务',
|
|
962
1338
|
command: 'openprd review . --open',
|
|
963
1339
|
executionCommand: `openprd change . --generate --change ${shellQuote(suggestedChangeId)}`,
|
|
964
1340
|
verifyCommand: `openprd change . --validate --change ${shellQuote(suggestedChangeId)}`,
|
|
965
|
-
reason: '
|
|
1341
|
+
reason: '这版需求已经确认,下一步先整理本次调整范围,再拆出可直接执行的后续任务。',
|
|
966
1342
|
changeId: suggestedChangeId,
|
|
967
1343
|
task: null,
|
|
968
1344
|
coverageItem: null,
|
|
@@ -983,9 +1359,9 @@ function buildRequirementIntakeRecommendation({ gate, next, activeChange }) {
|
|
|
983
1359
|
'clarify-user': '继续本轮需求入口澄清',
|
|
984
1360
|
classify: '补齐本轮需求的产品类型',
|
|
985
1361
|
interview: '补齐本轮需求的关键事实',
|
|
986
|
-
synthesize: '
|
|
1362
|
+
synthesize: '生成本轮需求的确认稿',
|
|
987
1363
|
diagram: '生成本轮需求的可视化评审',
|
|
988
|
-
review: '
|
|
1364
|
+
review: '查看并确认本轮需求稿',
|
|
989
1365
|
freeze: '进入本轮需求定稿前检查',
|
|
990
1366
|
handoff: '导出本轮需求交接包',
|
|
991
1367
|
};
|
|
@@ -995,8 +1371,8 @@ function buildRequirementIntakeRecommendation({ gate, next, activeChange }) {
|
|
|
995
1371
|
command: next?.recommendation?.suggestedCommand ?? 'openprd clarify .',
|
|
996
1372
|
verifyCommand: 'openprd run . --verify',
|
|
997
1373
|
reason: [
|
|
998
|
-
'
|
|
999
|
-
activeChange ?
|
|
1374
|
+
'当前有一条还在推进中的新需求;先把需求澄清、确认,再整理本次调整和后续任务。',
|
|
1375
|
+
activeChange ? `之前还有一项历史事项 ${activeChange},这里只把它当背景提醒,不抢这次主线。` : null,
|
|
1000
1376
|
next?.recommendation?.reason ?? null,
|
|
1001
1377
|
].filter(Boolean).join(' '),
|
|
1002
1378
|
changeId: null,
|
|
@@ -1011,7 +1387,86 @@ function buildRequirementIntakeRecommendation({ gate, next, activeChange }) {
|
|
|
1011
1387
|
};
|
|
1012
1388
|
}
|
|
1013
1389
|
|
|
1390
|
+
function buildStoredVerificationRecommendation(runState, options = {}) {
|
|
1391
|
+
const lastVerification = runState?.lastVerification;
|
|
1392
|
+
if (!lastVerification || lastVerification.taskReady !== true) {
|
|
1393
|
+
return null;
|
|
1394
|
+
}
|
|
1395
|
+
const workspaceReady = lastVerification.workspaceReady === true;
|
|
1396
|
+
const workspaceAttention = lastVerification.workspaceAttention ?? null;
|
|
1397
|
+
const changeId = lastVerification.changeId ?? options.focusedChangeId ?? options.activeChange ?? null;
|
|
1398
|
+
return {
|
|
1399
|
+
type: workspaceReady ? 'verification-ready' : 'verification-workspace-attention',
|
|
1400
|
+
title: workspaceReady ? '当前项目已完成并通过验证' : '当前任务已完成,工作区还有待处理项',
|
|
1401
|
+
command: 'openprd run . --verify',
|
|
1402
|
+
verifyCommand: 'openprd run . --verify',
|
|
1403
|
+
reason: workspaceReady
|
|
1404
|
+
? '最近一次 run verify 已经闭环,当前没有待执行任务或待澄清入口;除非有新需求进入,否则优先复用已沉淀结果。'
|
|
1405
|
+
: (workspaceAttention?.detail ?? '最近一次 run verify 显示任务级验证已通过,但工作区级别还有待补证据或待收口项。'),
|
|
1406
|
+
changeId,
|
|
1407
|
+
task: null,
|
|
1408
|
+
coverageItem: null,
|
|
1409
|
+
verification: lastVerification,
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
function buildVerificationRecommendation({ changeId, readiness, workspaceAttention, knowledgeReview, qualityCheck }) {
|
|
1414
|
+
if (readiness.taskReady !== true) {
|
|
1415
|
+
return {
|
|
1416
|
+
type: 'verification-fix',
|
|
1417
|
+
title: '先修复当前验证失败项',
|
|
1418
|
+
command: 'openprd run . --verify',
|
|
1419
|
+
verifyCommand: 'openprd run . --verify',
|
|
1420
|
+
reason: '最近一次 run verify 没有通过当前任务级验证,需要先修复标准、变更或任务级检查失败项。',
|
|
1421
|
+
changeId,
|
|
1422
|
+
task: null,
|
|
1423
|
+
coverageItem: null,
|
|
1424
|
+
verification: {
|
|
1425
|
+
...readiness,
|
|
1426
|
+
workspaceAttention,
|
|
1427
|
+
knowledgeCandidateId: knowledgeReview?.candidateId ?? null,
|
|
1428
|
+
qualityReportPath: qualityCheck?.reportPath ?? null,
|
|
1429
|
+
},
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
if (readiness.workspaceReady === true) {
|
|
1433
|
+
return {
|
|
1434
|
+
type: 'verification-ready',
|
|
1435
|
+
title: '当前项目已完成并通过验证',
|
|
1436
|
+
command: 'openprd run . --verify',
|
|
1437
|
+
verifyCommand: 'openprd run . --verify',
|
|
1438
|
+
reason: '最近一次 run verify、quality、standards 和变更验证都已闭环;当前没有新的待执行动作时,不应该再回到 clarify-user。',
|
|
1439
|
+
changeId,
|
|
1440
|
+
task: null,
|
|
1441
|
+
coverageItem: null,
|
|
1442
|
+
verification: {
|
|
1443
|
+
...readiness,
|
|
1444
|
+
workspaceAttention,
|
|
1445
|
+
knowledgeCandidateId: knowledgeReview?.candidateId ?? null,
|
|
1446
|
+
qualityReportPath: qualityCheck?.reportPath ?? null,
|
|
1447
|
+
},
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
return {
|
|
1451
|
+
type: 'verification-workspace-attention',
|
|
1452
|
+
title: '当前任务已完成,工作区还有待处理项',
|
|
1453
|
+
command: 'openprd run . --verify',
|
|
1454
|
+
verifyCommand: 'openprd run . --verify',
|
|
1455
|
+
reason: workspaceAttention?.detail ?? '最近一次 run verify 显示当前任务已通过,但工作区级别还有待补证据或待收口项。',
|
|
1456
|
+
changeId,
|
|
1457
|
+
task: null,
|
|
1458
|
+
coverageItem: null,
|
|
1459
|
+
verification: {
|
|
1460
|
+
...readiness,
|
|
1461
|
+
workspaceAttention,
|
|
1462
|
+
knowledgeCandidateId: knowledgeReview?.candidateId ?? null,
|
|
1463
|
+
qualityReportPath: qualityCheck?.reportPath ?? null,
|
|
1464
|
+
},
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1014
1468
|
function buildRunRecommendation({
|
|
1469
|
+
projectRoot,
|
|
1015
1470
|
message,
|
|
1016
1471
|
changes,
|
|
1017
1472
|
activeChange,
|
|
@@ -1021,8 +1476,10 @@ function buildRunRecommendation({
|
|
|
1021
1476
|
next,
|
|
1022
1477
|
loopFeatureList,
|
|
1023
1478
|
requirementGate,
|
|
1479
|
+
requirementGateAssessment,
|
|
1024
1480
|
laneRequest,
|
|
1025
1481
|
resolvedTarget,
|
|
1482
|
+
runState,
|
|
1026
1483
|
}) {
|
|
1027
1484
|
if (
|
|
1028
1485
|
['task-handle', 'work-unit'].includes(laneRequest?.selectorType)
|
|
@@ -1035,7 +1492,7 @@ function buildRunRecommendation({
|
|
|
1035
1492
|
activeChange,
|
|
1036
1493
|
});
|
|
1037
1494
|
}
|
|
1038
|
-
if (
|
|
1495
|
+
if (requirementGateAssessment?.relevance === 'primary') {
|
|
1039
1496
|
return buildRequirementIntakeRecommendation({ gate: requirementGate, next, activeChange });
|
|
1040
1497
|
}
|
|
1041
1498
|
if (taskState?.nextTask) {
|
|
@@ -1044,6 +1501,29 @@ function buildRunRecommendation({
|
|
|
1044
1501
|
const pendingTasks = Number(taskState.summary?.pending ?? 0);
|
|
1045
1502
|
const implementationTasks = Number(taskState.summary?.implementation?.total ?? 0);
|
|
1046
1503
|
const pendingImplementationTasks = Number(taskState.summary?.implementation?.pending ?? 0);
|
|
1504
|
+
const laneRequiresIsolation = Boolean(
|
|
1505
|
+
resolvedTarget?.workspaceRoot && !rootsEqual(resolvedTarget.workspaceRoot, projectRoot)
|
|
1506
|
+
|| ['session', 'task-handle', 'work-unit'].includes(laneRequest?.selectorType ?? '')
|
|
1507
|
+
);
|
|
1508
|
+
const executionMode = laneRequiresIsolation
|
|
1509
|
+
? 'parallel-workers-isolated'
|
|
1510
|
+
: (
|
|
1511
|
+
implementationTasks >= OPENPRD_LOOP_REQUIRED_IMPLEMENTATION_TASK_THRESHOLD
|
|
1512
|
+
|| pendingImplementationTasks >= OPENPRD_LOOP_REQUIRED_IMPLEMENTATION_TASK_THRESHOLD
|
|
1513
|
+
)
|
|
1514
|
+
? 'parallel-workers-isolated'
|
|
1515
|
+
: (
|
|
1516
|
+
implementationTasks >= OPENPRD_PARALLEL_WORKER_IMPLEMENTATION_TASK_THRESHOLD
|
|
1517
|
+
|| pendingImplementationTasks >= OPENPRD_PARALLEL_WORKER_IMPLEMENTATION_TASK_THRESHOLD
|
|
1518
|
+
)
|
|
1519
|
+
? 'parallel-workers'
|
|
1520
|
+
: 'serial';
|
|
1521
|
+
const parallelPlan = buildParallelPlan({
|
|
1522
|
+
executionMode,
|
|
1523
|
+
taskState,
|
|
1524
|
+
focusTask: task,
|
|
1525
|
+
worktreeRecommended: laneRequiresIsolation || executionMode === 'parallel-workers-isolated',
|
|
1526
|
+
});
|
|
1047
1527
|
if (
|
|
1048
1528
|
implementationTasks >= OPENPRD_LOOP_REQUIRED_IMPLEMENTATION_TASK_THRESHOLD
|
|
1049
1529
|
|| pendingImplementationTasks >= OPENPRD_LOOP_REQUIRED_IMPLEMENTATION_TASK_THRESHOLD
|
|
@@ -1051,7 +1531,7 @@ function buildRunRecommendation({
|
|
|
1051
1531
|
const loopReady = loopFeatureList?.changeId === taskState.changeId && Array.isArray(loopFeatureList.tasks);
|
|
1052
1532
|
return {
|
|
1053
1533
|
type: 'loop-task',
|
|
1054
|
-
title:
|
|
1534
|
+
title: `继续推进:${task.title}`,
|
|
1055
1535
|
command: `openprd tasks . --change ${shellQuote(taskState.changeId)}`,
|
|
1056
1536
|
preparationCommand: loopReady
|
|
1057
1537
|
? `openprd loop . --next --item ${shellQuote(task.id)}`
|
|
@@ -1061,11 +1541,15 @@ function buildRunRecommendation({
|
|
|
1061
1541
|
: `openprd loop . --plan --change ${shellQuote(taskState.changeId)} && openprd loop . --run --agent codex --item ${shellQuote(task.id)}`,
|
|
1062
1542
|
commitCommand: `openprd loop . --finish --item ${shellQuote(task.id)} --commit`,
|
|
1063
1543
|
verifyCommand: `openprd loop . --verify --item ${shellQuote(task.id)}`,
|
|
1064
|
-
reason:
|
|
1544
|
+
reason: laneRequiresIsolation
|
|
1545
|
+
? '这件事来自指定历史记录,最好放到单独环境里接着做,避免和别的事项串线。'
|
|
1546
|
+
: '待落地内容比较多,适合拆成一个个独立小任务推进,再统一收口检查。',
|
|
1065
1547
|
changeId: taskState.changeId,
|
|
1066
1548
|
task,
|
|
1067
1549
|
coverageItem: null,
|
|
1068
1550
|
intentGate: executionGate(),
|
|
1551
|
+
executionMode,
|
|
1552
|
+
parallelPlan,
|
|
1069
1553
|
loop: {
|
|
1070
1554
|
required: true,
|
|
1071
1555
|
threshold: OPENPRD_LOOP_REQUIRED_IMPLEMENTATION_TASK_THRESHOLD,
|
|
@@ -1078,17 +1562,27 @@ function buildRunRecommendation({
|
|
|
1078
1562
|
},
|
|
1079
1563
|
};
|
|
1080
1564
|
}
|
|
1565
|
+
const lightweightReason = laneRequiresIsolation
|
|
1566
|
+
? '这件事来自指定历史记录,最好放到单独环境里继续,避免和别的事项串线。'
|
|
1567
|
+
: executionMode === 'parallel-workers'
|
|
1568
|
+
? '待处理的落地内容比较多,适合先分头推进,再统一收口检查。'
|
|
1569
|
+
: '已经有一项可以继续推进的后续任务;只要用户明确要继续,就可以往下做。';
|
|
1081
1570
|
return {
|
|
1082
1571
|
type: 'task',
|
|
1083
|
-
title:
|
|
1572
|
+
title: `继续推进:${task.title}`,
|
|
1084
1573
|
command: `openprd tasks . --change ${shellQuote(taskState.changeId)}`,
|
|
1574
|
+
preparationCommand: executionMode === 'parallel-workers'
|
|
1575
|
+
? `openprd loop . --plan --change ${shellQuote(taskState.changeId)}`
|
|
1576
|
+
: null,
|
|
1085
1577
|
executionCommand: `openprd tasks . --change ${shellQuote(taskState.changeId)} --advance --verify --item ${shellQuote(task.id)}`,
|
|
1086
1578
|
verifyCommand: task.verify ?? `openprd tasks . --change ${shellQuote(taskState.changeId)} --verify --item ${shellQuote(task.id)}`,
|
|
1087
|
-
reason:
|
|
1579
|
+
reason: lightweightReason,
|
|
1088
1580
|
changeId: taskState.changeId,
|
|
1089
1581
|
task,
|
|
1090
1582
|
coverageItem: null,
|
|
1091
1583
|
intentGate: executionGate(),
|
|
1584
|
+
executionMode,
|
|
1585
|
+
parallelPlan,
|
|
1092
1586
|
loop: {
|
|
1093
1587
|
required: false,
|
|
1094
1588
|
threshold: OPENPRD_LOOP_REQUIRED_IMPLEMENTATION_TASK_THRESHOLD,
|
|
@@ -1096,17 +1590,17 @@ function buildRunRecommendation({
|
|
|
1096
1590
|
pendingTasks,
|
|
1097
1591
|
implementationTasks,
|
|
1098
1592
|
pendingImplementationTasks,
|
|
1099
|
-
worktreeRecommended:
|
|
1593
|
+
worktreeRecommended: laneRequiresIsolation,
|
|
1100
1594
|
},
|
|
1101
1595
|
};
|
|
1102
1596
|
}
|
|
1103
1597
|
if (taskState && taskState.summary?.pending === 0 && focusedChangeId) {
|
|
1104
1598
|
return {
|
|
1105
1599
|
type: 'change-review',
|
|
1106
|
-
title:
|
|
1600
|
+
title: '检查本次调整是否都已补齐',
|
|
1107
1601
|
command: `openprd change . --validate --change ${shellQuote(focusedChangeId)}`,
|
|
1108
1602
|
verifyCommand: `openprd change . --validate --change ${shellQuote(focusedChangeId)}`,
|
|
1109
|
-
reason: '
|
|
1603
|
+
reason: '当前这次调整里的后续任务已经处理完了。',
|
|
1110
1604
|
changeId: focusedChangeId,
|
|
1111
1605
|
task: null,
|
|
1112
1606
|
coverageItem: null,
|
|
@@ -1121,11 +1615,11 @@ function buildRunRecommendation({
|
|
|
1121
1615
|
const item = compactCoverageItem(nextCoverage);
|
|
1122
1616
|
return {
|
|
1123
1617
|
type: 'discovery',
|
|
1124
|
-
title:
|
|
1618
|
+
title: `继续补充调研:${item.title}`,
|
|
1125
1619
|
command: 'openprd discovery . --verify',
|
|
1126
1620
|
executionCommand: `openprd discovery . --advance --item ${shellQuote(item.id)} --claim <evidence-backed-claim> --evidence <path>`,
|
|
1127
1621
|
verifyCommand: 'openprd discovery . --verify',
|
|
1128
|
-
reason: '
|
|
1622
|
+
reason: '还有一个待补的调研点;只有用户明确要求继续深挖、对标或复刻时再推进。',
|
|
1129
1623
|
changeId: focusedChangeId ?? activeChange,
|
|
1130
1624
|
task: null,
|
|
1131
1625
|
coverageItem: item,
|
|
@@ -1135,21 +1629,35 @@ function buildRunRecommendation({
|
|
|
1135
1629
|
if (discovery?.coverageMatrix?.summary?.pending === 0 && discovery?.runId) {
|
|
1136
1630
|
return {
|
|
1137
1631
|
type: 'discovery-review',
|
|
1138
|
-
title:
|
|
1632
|
+
title: '检查这轮调研是否已经收口',
|
|
1139
1633
|
command: 'openprd discovery . --verify',
|
|
1140
1634
|
verifyCommand: 'openprd discovery . --verify',
|
|
1141
|
-
reason: '
|
|
1635
|
+
reason: '当前这轮调研已经没有待补项了。',
|
|
1142
1636
|
changeId: focusedChangeId ?? activeChange,
|
|
1143
1637
|
task: null,
|
|
1144
1638
|
coverageItem: null,
|
|
1145
1639
|
};
|
|
1146
1640
|
}
|
|
1641
|
+
const storedVerificationRecommendation = (
|
|
1642
|
+
requirementGateAssessment?.relevance !== 'primary'
|
|
1643
|
+
&& !laneRequest?.requested
|
|
1644
|
+
&& !resolvedTarget?.matched
|
|
1645
|
+
&& (next?.recommendation?.nextAction ?? 'clarify-user') === 'clarify-user'
|
|
1646
|
+
)
|
|
1647
|
+
? buildStoredVerificationRecommendation(runState, {
|
|
1648
|
+
activeChange,
|
|
1649
|
+
focusedChangeId,
|
|
1650
|
+
})
|
|
1651
|
+
: null;
|
|
1652
|
+
if (storedVerificationRecommendation) {
|
|
1653
|
+
return storedVerificationRecommendation;
|
|
1654
|
+
}
|
|
1147
1655
|
return {
|
|
1148
1656
|
type: 'workflow',
|
|
1149
|
-
title: next?.recommendation?.nextAction ?? '
|
|
1657
|
+
title: next?.recommendation?.nextAction ?? '查看当前建议下一步',
|
|
1150
1658
|
command: next?.recommendation?.suggestedCommand ?? 'openprd next .',
|
|
1151
1659
|
verifyCommand: 'openprd validate .',
|
|
1152
|
-
reason: next?.recommendation?.reason ?? '
|
|
1660
|
+
reason: next?.recommendation?.reason ?? '当前没有找到可以直接继续推进的任务或调研项。',
|
|
1153
1661
|
changeId: focusedChangeId ?? activeChange,
|
|
1154
1662
|
task: null,
|
|
1155
1663
|
coverageItem: null,
|
|
@@ -1188,11 +1696,25 @@ async function buildRunContext(projectRoot, dependencies, options = {}) {
|
|
|
1188
1696
|
status: next.analysisSnapshot.status ?? null,
|
|
1189
1697
|
}
|
|
1190
1698
|
: null;
|
|
1191
|
-
const
|
|
1699
|
+
const resolutionCache = new Map();
|
|
1700
|
+
async function resolveWorkspaceArtifacts(targetProjectRoot) {
|
|
1701
|
+
const key = path.resolve(targetProjectRoot);
|
|
1702
|
+
if (resolutionCache.has(key)) {
|
|
1703
|
+
return resolutionCache.get(key);
|
|
1704
|
+
}
|
|
1705
|
+
const targetChanges = await listOpenPrdChangesWorkspace(targetProjectRoot).catch(() => null);
|
|
1706
|
+
const artifacts = {
|
|
1707
|
+
changes: targetChanges,
|
|
1708
|
+
index: await buildRunResolutionIndex(targetProjectRoot, targetChanges, listOpenSpecTaskWorkspace).catch(() => null),
|
|
1709
|
+
loopFeatureList: await readLoopFeatureList(targetProjectRoot),
|
|
1710
|
+
};
|
|
1711
|
+
resolutionCache.set(key, artifacts);
|
|
1712
|
+
return artifacts;
|
|
1713
|
+
}
|
|
1714
|
+
const currentWorkspaceArtifacts = await resolveWorkspaceArtifacts(projectRoot);
|
|
1715
|
+
const loopFeatureList = currentWorkspaceArtifacts.loopFeatureList;
|
|
1192
1716
|
const shouldResolveTarget = Boolean(String(options.message ?? '').trim());
|
|
1193
|
-
const resolutionIndex = shouldResolveTarget
|
|
1194
|
-
? await buildRunResolutionIndex(projectRoot, changes, listOpenSpecTaskWorkspace)
|
|
1195
|
-
: null;
|
|
1717
|
+
const resolutionIndex = shouldResolveTarget ? currentWorkspaceArtifacts.index : null;
|
|
1196
1718
|
const resolvedTarget = shouldResolveTarget
|
|
1197
1719
|
? await resolveRunTarget({
|
|
1198
1720
|
projectRoot,
|
|
@@ -1200,15 +1722,23 @@ async function buildRunContext(projectRoot, dependencies, options = {}) {
|
|
|
1200
1722
|
request: laneRequest,
|
|
1201
1723
|
index: resolutionIndex,
|
|
1202
1724
|
loopFeatureList,
|
|
1725
|
+
resolveWorkspaceArtifacts,
|
|
1203
1726
|
})
|
|
1204
1727
|
: null;
|
|
1205
|
-
const
|
|
1728
|
+
const requirementGateAssessment = assessRequirementGateRelevance({
|
|
1729
|
+
message: options.message,
|
|
1730
|
+
gate: requirementGate,
|
|
1731
|
+
laneRequest,
|
|
1732
|
+
resolvedTarget,
|
|
1733
|
+
});
|
|
1734
|
+
const focusedChangeId = selectFocusedChangeId(projectRoot, laneRequest, resolvedTarget, activeChange);
|
|
1206
1735
|
const taskState = focusedChangeId
|
|
1207
1736
|
? await listOpenSpecTaskWorkspace(projectRoot, { change: focusedChangeId }).catch(() => null)
|
|
1208
1737
|
: null;
|
|
1209
1738
|
const resumedDiscovery = await resumeOpenSpecDiscoveryWorkspace(projectRoot).catch(() => null);
|
|
1210
1739
|
const discovery = shouldSurfaceDiscoveryInRunContext(resumedDiscovery) ? resumedDiscovery : null;
|
|
1211
1740
|
const recommendation = buildRunRecommendation({
|
|
1741
|
+
projectRoot,
|
|
1212
1742
|
message: options.message,
|
|
1213
1743
|
changes,
|
|
1214
1744
|
activeChange,
|
|
@@ -1218,8 +1748,10 @@ async function buildRunContext(projectRoot, dependencies, options = {}) {
|
|
|
1218
1748
|
next,
|
|
1219
1749
|
loopFeatureList,
|
|
1220
1750
|
requirementGate,
|
|
1751
|
+
requirementGateAssessment,
|
|
1221
1752
|
laneRequest,
|
|
1222
1753
|
resolvedTarget,
|
|
1754
|
+
runState,
|
|
1223
1755
|
});
|
|
1224
1756
|
const nextTask = compactTask(taskState?.nextTask ?? null);
|
|
1225
1757
|
const lane = buildRunLane({
|
|
@@ -1229,8 +1761,58 @@ async function buildRunContext(projectRoot, dependencies, options = {}) {
|
|
|
1229
1761
|
latestPrd,
|
|
1230
1762
|
loopFeatureList,
|
|
1231
1763
|
resolvedTarget,
|
|
1764
|
+
projectRoot,
|
|
1232
1765
|
});
|
|
1233
|
-
const
|
|
1766
|
+
const recommendationWithGateReminder = {
|
|
1767
|
+
...recommendation,
|
|
1768
|
+
reason: appendRequirementGateReminder(
|
|
1769
|
+
recommendation.reason,
|
|
1770
|
+
requirementGateAssessment,
|
|
1771
|
+
requirementGate,
|
|
1772
|
+
),
|
|
1773
|
+
};
|
|
1774
|
+
const effectiveRecommendation = withExecutionConfirmationChecklist(
|
|
1775
|
+
applyLaneToRecommendation(recommendationWithGateReminder, lane),
|
|
1776
|
+
{ requirementGate },
|
|
1777
|
+
);
|
|
1778
|
+
const knowledgeSkillMatches = await resolveKnowledgeSkillMatches(projectRoot, {
|
|
1779
|
+
message: options.message,
|
|
1780
|
+
prompt: options.message,
|
|
1781
|
+
recommendationTitle: effectiveRecommendation.title,
|
|
1782
|
+
recommendationReason: effectiveRecommendation.reason,
|
|
1783
|
+
activeChange,
|
|
1784
|
+
nextTaskTitle: nextTask?.title,
|
|
1785
|
+
relatedFiles: [
|
|
1786
|
+
focusedChangeId ? `openprd/changes/${focusedChangeId}/tasks.md` : null,
|
|
1787
|
+
activeChange ? `openprd/changes/${activeChange}` : null,
|
|
1788
|
+
].filter(Boolean),
|
|
1789
|
+
limit: options.hookInject ? 4 : 3,
|
|
1790
|
+
}).catch(() => ({ matched: [], summary: { matched: 0 } }));
|
|
1791
|
+
const knowledgeAdoption = knowledgeSkillMatches.matched?.length > 0
|
|
1792
|
+
? await recordKnowledgeSkillAdoption(projectRoot, {
|
|
1793
|
+
matches: knowledgeSkillMatches.matched,
|
|
1794
|
+
stages: options.hookInject ? ['hit', 'referenced', 'injected'] : ['hit', 'referenced'],
|
|
1795
|
+
source: options.hookInject ? 'run-context-hook' : 'run-context',
|
|
1796
|
+
sessionId: lane.target?.sessionId ?? resolvedTarget?.sessionId ?? requirementGate?.sessionId ?? null,
|
|
1797
|
+
promptPreview: options.message,
|
|
1798
|
+
}).catch(() => null)
|
|
1799
|
+
: null;
|
|
1800
|
+
const knowledgeStageBump = {
|
|
1801
|
+
hitCount: options.hookInject ? 1 : 1,
|
|
1802
|
+
referencedCount: 1,
|
|
1803
|
+
injectedCount: options.hookInject ? 1 : 0,
|
|
1804
|
+
};
|
|
1805
|
+
const renderedKnowledgeSkills = (knowledgeSkillMatches.matched ?? []).map((skill) => ({
|
|
1806
|
+
...skill,
|
|
1807
|
+
adoption: skill.adoption
|
|
1808
|
+
? {
|
|
1809
|
+
...skill.adoption,
|
|
1810
|
+
hitCount: Number(skill.adoption.hitCount ?? 0) + knowledgeStageBump.hitCount,
|
|
1811
|
+
referencedCount: Number(skill.adoption.referencedCount ?? 0) + knowledgeStageBump.referencedCount,
|
|
1812
|
+
injectedCount: Number(skill.adoption.injectedCount ?? 0) + knowledgeStageBump.injectedCount,
|
|
1813
|
+
}
|
|
1814
|
+
: skill.adoption,
|
|
1815
|
+
}));
|
|
1234
1816
|
|
|
1235
1817
|
const context = {
|
|
1236
1818
|
ok: validation.valid,
|
|
@@ -1251,6 +1833,9 @@ async function buildRunContext(projectRoot, dependencies, options = {}) {
|
|
|
1251
1833
|
promptPreview: requirementGate.promptPreview ?? null,
|
|
1252
1834
|
intakeMode: requirementGate.intakeMode ?? null,
|
|
1253
1835
|
sessionId: requirementGate.sessionId ?? null,
|
|
1836
|
+
relevance: requirementGateAssessment.relevance,
|
|
1837
|
+
matchedCurrentMessage: requirementGateAssessment.matchedCurrentMessage,
|
|
1838
|
+
relevanceReason: requirementGateAssessment.reason,
|
|
1254
1839
|
}
|
|
1255
1840
|
: null,
|
|
1256
1841
|
prdReviewState: next?.prdReviewState
|
|
@@ -1267,6 +1852,7 @@ async function buildRunContext(projectRoot, dependencies, options = {}) {
|
|
|
1267
1852
|
focus: {
|
|
1268
1853
|
changeId: focusedChangeId,
|
|
1269
1854
|
source: resolvedTarget?.source ?? null,
|
|
1855
|
+
workspaceRoot: resolvedTarget?.workspaceRoot ?? projectRoot,
|
|
1270
1856
|
sessionId: resolvedTarget?.sessionId ?? lane.target?.sessionId ?? null,
|
|
1271
1857
|
taskHandle: resolvedTarget?.taskHandle ?? null,
|
|
1272
1858
|
workUnitId: resolvedTarget?.workUnitId ?? null,
|
|
@@ -1290,6 +1876,14 @@ async function buildRunContext(projectRoot, dependencies, options = {}) {
|
|
|
1290
1876
|
: null,
|
|
1291
1877
|
lane,
|
|
1292
1878
|
recommendation: effectiveRecommendation,
|
|
1879
|
+
knowledgeSkills: {
|
|
1880
|
+
matched: renderedKnowledgeSkills,
|
|
1881
|
+
summary: {
|
|
1882
|
+
matched: renderedKnowledgeSkills.length,
|
|
1883
|
+
hookInjected: Boolean(options.hookInject && renderedKnowledgeSkills.length > 0),
|
|
1884
|
+
adoption: knowledgeAdoption?.summary ?? null,
|
|
1885
|
+
},
|
|
1886
|
+
},
|
|
1293
1887
|
files: {
|
|
1294
1888
|
runState: OPENPRD_HARNESS_RUN_STATE,
|
|
1295
1889
|
iterations: OPENPRD_HARNESS_ITERATIONS,
|
|
@@ -1351,6 +1945,51 @@ async function verifyRunWorkspace(projectRoot, dependencies, options = {}) {
|
|
|
1351
1945
|
verifyOpenSpecDiscoveryWorkspace,
|
|
1352
1946
|
verifyQualityWorkspace,
|
|
1353
1947
|
} = dependencies;
|
|
1948
|
+
const summarizeWorkspaceAttention = (quality) => {
|
|
1949
|
+
const report = quality?.report ?? null;
|
|
1950
|
+
const attentionGates = Array.isArray(report?.readiness?.attentionGates)
|
|
1951
|
+
? report.readiness.attentionGates.filter(Boolean)
|
|
1952
|
+
: [];
|
|
1953
|
+
const activeTasks = report?.evalHarness?.featureCoverage?.activeTasks ?? null;
|
|
1954
|
+
if (attentionGates.length === 1 && attentionGates[0] === 'feature-coverage' && activeTasks?.pending > 0) {
|
|
1955
|
+
const activeChange = activeTasks.activeChange ?? null;
|
|
1956
|
+
const total = Number(activeTasks.total ?? 0);
|
|
1957
|
+
const done = Number(activeTasks.done ?? 0);
|
|
1958
|
+
const pending = Number(activeTasks.pending ?? 0);
|
|
1959
|
+
const blocked = Number(activeTasks.blocked ?? 0);
|
|
1960
|
+
const progress = total > 0 ? `${done}/${total}` : `${done}`;
|
|
1961
|
+
const changeLabel = activeChange ? `active change ${activeChange}` : 'the active task ledger';
|
|
1962
|
+
return {
|
|
1963
|
+
kind: 'feature-coverage-ledger',
|
|
1964
|
+
gate: 'feature-coverage',
|
|
1965
|
+
gates: attentionGates,
|
|
1966
|
+
activeChange,
|
|
1967
|
+
total,
|
|
1968
|
+
done,
|
|
1969
|
+
pending,
|
|
1970
|
+
blocked,
|
|
1971
|
+
summary: 'feature-coverage ledger remains open',
|
|
1972
|
+
detail: `${changeLabel} still has ${pending} pending tasks (${progress} done)${blocked > 0 ? `, with ${blocked} blocked` : ''}. This usually means task bookkeeping or coverage evidence is incomplete, not that the current implementation failed.`,
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
if (attentionGates.length > 0) {
|
|
1976
|
+
return {
|
|
1977
|
+
kind: 'quality-gates',
|
|
1978
|
+
gates: attentionGates,
|
|
1979
|
+
summary: `quality attention gates: ${attentionGates.join(', ')}`,
|
|
1980
|
+
detail: `Current task verification passed, but workspace-level quality still needs evidence for: ${attentionGates.join(', ')}.`,
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
if (quality?.report?.readiness?.productionReady === false) {
|
|
1984
|
+
return {
|
|
1985
|
+
kind: 'quality-readiness',
|
|
1986
|
+
gates: [],
|
|
1987
|
+
summary: 'quality report is not production-ready',
|
|
1988
|
+
detail: 'Current task verification passed, but the workspace-level quality report still needs attention before overall readiness can be claimed.',
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
return null;
|
|
1992
|
+
};
|
|
1354
1993
|
const context = await buildRunContext(projectRoot, dependencies, options);
|
|
1355
1994
|
const standards = await checkStandardsWorkspace(projectRoot);
|
|
1356
1995
|
const validation = await validateWorkspace(projectRoot).then(({ report }) => report);
|
|
@@ -1364,9 +2003,12 @@ async function verifyRunWorkspace(projectRoot, dependencies, options = {}) {
|
|
|
1364
2003
|
errors: [error instanceof Error ? error.message : String(error)],
|
|
1365
2004
|
}));
|
|
1366
2005
|
const productionReady = quality.report?.readiness?.productionReady ?? null;
|
|
2006
|
+
const workspaceAttention = summarizeWorkspaceAttention(quality);
|
|
1367
2007
|
const qualityErrors = [
|
|
1368
2008
|
...(quality.errors ?? []),
|
|
1369
|
-
...(productionReady === false
|
|
2009
|
+
...(productionReady === false
|
|
2010
|
+
? [workspaceAttention?.detail ?? 'Quality report is not production-ready. Review required gates and evidence before claiming readiness.']
|
|
2011
|
+
: []),
|
|
1370
2012
|
];
|
|
1371
2013
|
checks.push({
|
|
1372
2014
|
name: 'quality',
|
|
@@ -1377,6 +2019,7 @@ async function verifyRunWorkspace(projectRoot, dependencies, options = {}) {
|
|
|
1377
2019
|
htmlPath: quality.htmlPath ?? null,
|
|
1378
2020
|
productionReady,
|
|
1379
2021
|
attentionGates: quality.report?.readiness?.attentionGates ?? [],
|
|
2022
|
+
workspaceAttention,
|
|
1380
2023
|
});
|
|
1381
2024
|
}
|
|
1382
2025
|
const changeToVerify = context.focus?.changeId ?? context.recommendation?.changeId ?? context.activeChange ?? null;
|
|
@@ -1392,8 +2035,16 @@ async function verifyRunWorkspace(projectRoot, dependencies, options = {}) {
|
|
|
1392
2035
|
const workspaceChecks = checks;
|
|
1393
2036
|
const taskReady = taskChecks.every((check) => check.ok);
|
|
1394
2037
|
const workspaceReady = workspaceChecks.every((check) => check.ok);
|
|
1395
|
-
const ok = taskReady;
|
|
1396
2038
|
const qualityCheck = checks.find((check) => check.name === 'quality');
|
|
2039
|
+
const workspaceAttention = taskReady && !workspaceReady
|
|
2040
|
+
? (qualityCheck?.workspaceAttention ?? {
|
|
2041
|
+
kind: 'workspace-checks',
|
|
2042
|
+
checks: workspaceChecks.filter((check) => !check.ok).map((check) => check.name),
|
|
2043
|
+
summary: `workspace attention: ${workspaceChecks.filter((check) => !check.ok).map((check) => check.name).join(', ')}`,
|
|
2044
|
+
detail: `Current task verification passed, but workspace-level checks still need attention: ${workspaceChecks.filter((check) => !check.ok).map((check) => check.name).join(', ')}.`,
|
|
2045
|
+
})
|
|
2046
|
+
: null;
|
|
2047
|
+
const ok = taskReady;
|
|
1397
2048
|
const readiness = {
|
|
1398
2049
|
taskReady,
|
|
1399
2050
|
workspaceReady,
|
|
@@ -1408,8 +2059,9 @@ async function verifyRunWorkspace(projectRoot, dependencies, options = {}) {
|
|
|
1408
2059
|
workspaceReady,
|
|
1409
2060
|
productionReady: qualityCheck?.productionReady ?? null,
|
|
1410
2061
|
attentionGates: qualityCheck?.attentionGates ?? [],
|
|
2062
|
+
workspaceAttentionKind: workspaceAttention?.kind ?? null,
|
|
1411
2063
|
summary: taskReady
|
|
1412
|
-
? (workspaceReady ? 'run verify passed' : `run verify task-ready with workspace attention: ${workspaceChecks.filter((check) => !check.ok).map((check) => check.name).join(', ')}`)
|
|
2064
|
+
? (workspaceReady ? 'run verify passed' : `run verify task-ready with workspace attention: ${workspaceAttention?.summary ?? workspaceChecks.filter((check) => !check.ok).map((check) => check.name).join(', ')}`)
|
|
1413
2065
|
: `run verify failed: ${taskChecks.filter((check) => !check.ok).map((check) => check.name).join(', ')}`,
|
|
1414
2066
|
};
|
|
1415
2067
|
await recordKnowledgeReviewSignal(projectRoot, knowledgeSignal).catch(() => null);
|
|
@@ -1422,9 +2074,29 @@ async function verifyRunWorkspace(projectRoot, dependencies, options = {}) {
|
|
|
1422
2074
|
}).catch((error) => ({
|
|
1423
2075
|
ok: false,
|
|
1424
2076
|
action: 'quality-knowledge-review',
|
|
1425
|
-
|
|
1426
|
-
|
|
2077
|
+
skipped: false,
|
|
2078
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
1427
2079
|
}));
|
|
2080
|
+
const verificationRecommendation = buildVerificationRecommendation({
|
|
2081
|
+
changeId: changeToVerify,
|
|
2082
|
+
readiness,
|
|
2083
|
+
workspaceAttention,
|
|
2084
|
+
knowledgeReview,
|
|
2085
|
+
qualityCheck,
|
|
2086
|
+
});
|
|
2087
|
+
const runState = await readRunState(projectRoot);
|
|
2088
|
+
await writeRunState(projectRoot, {
|
|
2089
|
+
...runState,
|
|
2090
|
+
lastVerificationAt: timestamp(),
|
|
2091
|
+
lastVerification: {
|
|
2092
|
+
...readiness,
|
|
2093
|
+
changeId: changeToVerify,
|
|
2094
|
+
workspaceAttention,
|
|
2095
|
+
knowledgeCandidateId: knowledgeReview?.candidateId ?? null,
|
|
2096
|
+
qualityReportPath: qualityCheck?.reportPath ?? null,
|
|
2097
|
+
},
|
|
2098
|
+
lastRecommendation: verificationRecommendation,
|
|
2099
|
+
});
|
|
1428
2100
|
await appendJsonl(harnessFile(projectRoot, OPENPRD_HARNESS_ITERATIONS), {
|
|
1429
2101
|
version: 1,
|
|
1430
2102
|
at: timestamp(),
|
|
@@ -1444,8 +2116,10 @@ async function verifyRunWorkspace(projectRoot, dependencies, options = {}) {
|
|
|
1444
2116
|
context,
|
|
1445
2117
|
checks,
|
|
1446
2118
|
readiness,
|
|
2119
|
+
workspaceAttention,
|
|
1447
2120
|
warnings,
|
|
1448
2121
|
knowledgeReview,
|
|
2122
|
+
recommendation: verificationRecommendation,
|
|
1449
2123
|
errors,
|
|
1450
2124
|
};
|
|
1451
2125
|
}
|