@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.
Files changed (138) hide show
  1. package/.openprd/README.md +43 -69
  2. package/.openprd/README_EN.md +84 -0
  3. package/.openprd/benchmarks/index.md +7 -0
  4. package/.openprd/benchmarks/sources.yaml +25 -3
  5. package/.openprd/discovery/config.json +16 -2
  6. package/.openprd/engagements/active/flows.md +19 -14
  7. package/.openprd/engagements/active/handoff.md +11 -4
  8. package/.openprd/engagements/active/prd.md +99 -71
  9. package/.openprd/engagements/active/review.html +4 -4
  10. package/.openprd/engagements/active/roles.md +9 -8
  11. package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +4 -4
  12. package/.openprd/engagements/work-units/wu-20260602113956-a99b5b88.json +18 -0
  13. package/.openprd/engagements/work-units/wu-20260602122244-78656aaf.json +18 -0
  14. package/.openprd/engagements/work-units/wu-20260602122442-e96489e2.json +18 -0
  15. package/.openprd/engagements/work-units/wu-20260602132835-695429e8.json +18 -0
  16. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/candidate.json +78 -0
  17. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/diagnostic-report.json +129 -0
  18. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/root-cause-candidates.json +41 -0
  19. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/timeline.json +14 -0
  20. package/.openprd/knowledge/drafts/openprd-experience-diagnostic-candidate-turn-1780116203372-5f266a79e968c758/SKILL.md +49 -0
  21. package/.openprd/knowledge/index.json +44 -4
  22. package/.openprd/reviews/v0001.html +195 -129
  23. package/.openprd/reviews/v0002.html +1150 -0
  24. package/.openprd/reviews/v0003.html +1150 -0
  25. package/.openprd/reviews/v0004.html +1150 -0
  26. package/.openprd/reviews/v0005.html +1150 -0
  27. package/.openprd/standards/config.json +12 -9
  28. package/.openprd/state/changes.json +17 -2
  29. package/.openprd/state/current.json +399 -63
  30. package/.openprd/state/release-ledger.json +344 -0
  31. package/.openprd/state/version-index.json +52 -0
  32. package/.openprd/state/versions/v0002.json +264 -0
  33. package/.openprd/state/versions/v0002.md +183 -0
  34. package/.openprd/state/versions/v0003.json +269 -0
  35. package/.openprd/state/versions/v0003.md +188 -0
  36. package/.openprd/state/versions/v0004.json +274 -0
  37. package/.openprd/state/versions/v0004.md +193 -0
  38. package/.openprd/state/versions/v0005.json +299 -0
  39. package/.openprd/state/versions/v0005.md +189 -0
  40. package/.openprd/templates/agent/intake.md +5 -4
  41. package/.openprd/templates/b2b/intake.md +5 -4
  42. package/.openprd/templates/base/intake.md +10 -4
  43. package/.openprd/templates/company/README.md +9 -7
  44. package/.openprd/templates/company/README_EN.md +12 -0
  45. package/.openprd/templates/consumer/intake.md +5 -4
  46. package/.openprd/templates/industry/README.md +12 -10
  47. package/.openprd/templates/industry/README_EN.md +18 -0
  48. package/.openprd/templates/project/README.md +11 -9
  49. package/.openprd/templates/project/README_EN.md +16 -0
  50. package/.openprd/templates/session/README.md +11 -9
  51. package/.openprd/templates/session/README_EN.md +16 -0
  52. package/AGENTS.md +12 -8
  53. package/README.md +402 -441
  54. package/README_CN.md +4 -578
  55. package/README_EN.md +850 -0
  56. package/docs/assets/openprd-requirement-routing-en.png +0 -0
  57. package/docs/assets/openprd-requirement-routing-en.svg +102 -0
  58. package/docs/assets/openprd-requirement-routing-zh-refined.png +0 -0
  59. package/docs/assets/openprd-requirement-routing-zh.png +0 -0
  60. package/docs/assets/openprd-requirement-routing-zh.svg +102 -0
  61. package/package.json +6 -2
  62. package/scripts/dev-check-wrapup-copy.mjs +110 -0
  63. package/scripts/openprd-github-release-notes.mjs +99 -0
  64. package/scripts/quality-perf-check.mjs +203 -0
  65. package/skills/openprd-benchmark-router/SKILL.md +1 -0
  66. package/skills/openprd-benchmark-router/references/benchmark-sources.md +1 -0
  67. package/skills/openprd-benchmark-router/references/source-policy.md +2 -0
  68. package/skills/openprd-discovery-loop/SKILL.md +2 -2
  69. package/skills/openprd-harness/SKILL.md +46 -24
  70. package/skills/openprd-harness/references/workflow-gates.md +15 -0
  71. package/skills/openprd-quality/SKILL.md +10 -4
  72. package/skills/openprd-requirement-intake/SKILL.md +39 -23
  73. package/skills/openprd-requirement-intake/references/prd-template-lenses.md +6 -6
  74. package/skills/openprd-requirement-intake/references/routing-rubric.md +22 -8
  75. package/skills/openprd-router/SKILL.md +2 -2
  76. package/skills/openprd-shared/SKILL.md +51 -23
  77. package/skills/openprd-standards/SKILL.md +2 -1
  78. package/src/agent-integration.js +265 -65
  79. package/src/benchmark/constants.js +107 -0
  80. package/src/benchmark/operations.js +235 -0
  81. package/src/benchmark/registry.js +64 -0
  82. package/src/benchmark/render.js +115 -0
  83. package/src/benchmark/source.js +617 -0
  84. package/src/benchmark/storage.js +121 -0
  85. package/src/benchmark/verify.js +235 -0
  86. package/src/benchmark.js +50 -851
  87. package/src/change-summary.js +339 -0
  88. package/src/cli/args.js +67 -6
  89. package/src/cli/basic-print.js +365 -0
  90. package/src/cli/benchmark-print.js +91 -0
  91. package/src/cli/change-print.js +221 -0
  92. package/src/cli/doctor-print.js +268 -0
  93. package/src/cli/growth-print.js +176 -0
  94. package/src/cli/print.js +73 -1384
  95. package/src/cli/quality-print.js +284 -0
  96. package/src/cli/run-print.js +297 -0
  97. package/src/cli/shared-print.js +127 -0
  98. package/src/cli/workflow-print.js +195 -0
  99. package/src/codex-hook-runner-template.mjs +639 -117
  100. package/src/codex-runtime.js +324 -0
  101. package/src/dev-standards.js +178 -5
  102. package/src/diagram-core.js +5 -5
  103. package/src/discovery.js +2 -1
  104. package/src/execution-strategy.js +369 -0
  105. package/src/fleet.js +4 -0
  106. package/src/github-release.js +156 -0
  107. package/src/growth.js +311 -13
  108. package/src/html-artifact-utils.js +25 -0
  109. package/src/html-artifacts.js +157 -1596
  110. package/src/knowledge.js +1176 -75
  111. package/src/language-policy.js +2 -112
  112. package/src/learning-html-artifact.js +1031 -0
  113. package/src/learning-review.js +3 -2
  114. package/src/loop.js +280 -9
  115. package/src/openprd.js +341 -38
  116. package/src/openspec/change-validate.js +0 -9
  117. package/src/openspec/execute.js +79 -3
  118. package/src/openspec/generate.js +33 -20
  119. package/src/openspec/tasks.js +33 -2
  120. package/src/prd-core.js +10 -9
  121. package/src/product-type-copy.js +69 -0
  122. package/src/quality-html-artifact.js +108 -9
  123. package/src/quality-learning.js +30 -0
  124. package/src/quality-visual-review.js +237 -0
  125. package/src/quality.js +329 -43
  126. package/src/registry-hygiene.js +54 -0
  127. package/src/release-ledger.js +413 -0
  128. package/src/review-presentation.js +12 -6
  129. package/src/run-harness.js +722 -48
  130. package/src/self-update.js +1 -1
  131. package/src/session-binding.js +40 -3
  132. package/src/session-registry.js +159 -0
  133. package/src/standards.js +5 -3
  134. package/src/test-strategy.js +386 -0
  135. package/src/visual-compare.js +915 -34
  136. package/src/work-unit-migration.js +5 -1
  137. package/src/workspace-core.js +343 -19
  138. package/src/workspace-workflow.js +538 -134
@@ -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 { OPENPRD_HARNESS_TURN_STATE, recordKnowledgeReviewSignal, reviewKnowledgeWorkspace } from './knowledge.js';
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 = /(继续(这个|这条|当前)?(对话|任务|会话|记录|历史)?|续做|接着做|继续执行|继续推进)/i;
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(sessionId || taskHandle || workUnitId);
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 resolveSessionTarget(projectRoot, sessionId, index, loopFeatureList) {
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 resolveTaskHandleTarget(binding.taskHandle, index, loopFeatureList, {
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 resolveTaskHandleTarget(taskHandle, index, loopFeatureList, {
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
- return resolveWorkUnitTarget(projectRoot, workUnitId, index, {
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 semanticMatch;
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
- ? `openprd run . --context --message ${shellQuote(sessionId)}`
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: `生成 ${suggestedChangeId} 的 change 和任务拆解`,
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: '最新 PRD review.html 已确认,但还没有 active change;进入实现前需要先生成 change、spec 和结构化任务。',
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: '生成本轮需求的 PRD 评审稿',
1362
+ synthesize: '生成本轮需求的确认稿',
987
1363
  diagram: '生成本轮需求的可视化评审',
988
- review: '确认本轮需求的 review.html',
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
- '当前有 active requirement intake;先围绕本轮需求完成澄清、评审、change 和任务拆解。',
999
- activeChange ? `历史 active change ${activeChange} 仅作为提醒,不抢本轮默认执行路线。` : null,
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 (requirementGate?.active && !laneRequest?.requested && !resolvedTarget?.matched) {
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: `用 Loop 执行 ${task.id}: ${task.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: `当前变更包含 ${implementationTasks} 个实质实现任务,达到 ${OPENPRD_LOOP_REQUIRED_IMPLEMENTATION_TASK_THRESHOLD} 个实现任务的拆分阈值;建议使用独立 worktree 和 OpenPrd Loop 单任务会话,且只有用户明确要求开发、继续任务或深度对标落地时才执行。`,
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: `推进 ${task.id}: ${task.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: '存在一个依赖已就绪的 OpenPrd 任务;只有用户明确要求开发、实现或继续任务时才推进。',
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: false,
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: `校验已完成的变更 ${focusedChangeId}`,
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: `调研 ${item.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: '存在一个待处理的 OpenPrd discovery 覆盖项;只有用户明确要求深度调研、对标、复刻或持续补全时才推进覆盖项。',
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: `校验 discovery run ${discovery.runId}`,
1632
+ title: '检查这轮调研是否已经收口',
1139
1633
  command: 'openprd discovery . --verify',
1140
1634
  verifyCommand: 'openprd discovery . --verify',
1141
- reason: '当前 discovery run 没有待处理覆盖项。',
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 ?? 'Inspect OpenPrd next action',
1657
+ title: next?.recommendation?.nextAction ?? '查看当前建议下一步',
1150
1658
  command: next?.recommendation?.suggestedCommand ?? 'openprd next .',
1151
1659
  verifyCommand: 'openprd validate .',
1152
- reason: next?.recommendation?.reason ?? 'No active task or discovery item was found.',
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 loopFeatureList = await readJson(harnessFile(projectRoot, OPENPRD_HARNESS_LOOP_FEATURE_LIST)).catch(() => null);
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 focusedChangeId = selectFocusedChangeId(laneRequest, resolvedTarget, activeChange);
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 effectiveRecommendation = applyLaneToRecommendation(recommendation, lane);
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 ? ['Quality report is not production-ready. Review required gates and evidence before claiming readiness.'] : []),
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
- skipped: false,
1426
- errors: [error instanceof Error ? error.message : String(error)],
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
  }