@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,11 +1,12 @@
1
1
  import crypto from 'node:crypto';
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
+ import { appendText, exists, readJson, readText, readYaml, writeJson, writeText, writeYaml } from './fs-utils.js';
5
+ import { openArtifactInBrowser, writeHtmlArtifact } from './html-artifacts.js';
6
+ import { learningPackagePaths, renderLearningArtifact } from './learning-html-artifact.js';
4
7
  import { buildPrdSnapshot, formatVersionId } from './prd-core.js';
5
8
  import { compactTimestamp, timestamp } from './time.js';
6
9
  import { loadLatestVersionSnapshot, loadWorkspace, readVersionIndex, resolveActiveTemplatePack, resolveCurrentProductType } from './workspace-core.js';
7
- import { appendText, exists, readJson, readText, readYaml, writeJson, writeText, writeYaml } from './fs-utils.js';
8
- import { learningPackagePaths, openArtifactInBrowser, renderLearningArtifact, writeHtmlArtifact } from './html-artifacts.js';
9
10
 
10
11
  const LEARNING_REVIEW_SCHEMA_VERSION = 1;
11
12
  const LEARNING_AGENT_CONTEXT_SCHEMA = 'openprd.learning-agent-context.v1';
package/src/loop.js CHANGED
@@ -1,13 +1,37 @@
1
+ /*
2
+ * 核心功能
3
+ * 编排 OpenPrd loop 的规划、提示词生成、Agent 子会话运行、finish 和回归报告。
4
+ *
5
+ * 输入
6
+ * 接收项目路径、change/task 选择、Agent 类型、执行/修复参数和 loop 状态文件。
7
+ *
8
+ * 输出
9
+ * 写入 loop prompt、session 事件、进度状态和测试报告,并导出 loop workspace 函数。
10
+ *
11
+ * 定位
12
+ * 位于 OpenPrd 长程单任务执行层,连接 OpenSpec task、quality、learning review 与 Agent runtime。
13
+ *
14
+ * 依赖
15
+ * 依赖 openspec、quality、learning-review、knowledge、html-artifacts 和 codex-runtime 模块。
16
+ *
17
+ * 维护规则
18
+ * 新增执行入口必须保持 dry-run 可见、失败可诊断;真实 Codex 子会话启动前需保留 runtime preflight。
19
+ */
1
20
  import fs from 'node:fs/promises';
2
21
  import path from 'node:path';
3
22
  import { spawn } from 'node:child_process';
4
23
  import { pathToFileURL } from 'node:url';
24
+ import { buildTaskCommitMessage } from './change-summary.js';
25
+ import { ensureCodexCliReady } from './codex-runtime.js';
26
+ import { describeExecutionStrategy, labelOwnerRole, taskExecutionStrategy } from './execution-strategy.js';
5
27
  import { defaultRegressionArtifactPath, renderRegressionArtifact, writeHtmlArtifact } from './html-artifacts.js';
6
28
  import { OPENPRD_HARNESS_TURN_STATE, recordKnowledgeReviewSignal, reviewKnowledgeWorkspace } from './knowledge.js';
7
29
  import { generateLearningReviewWorkspace } from './learning-review.js';
8
30
  import { listOpenSpecTaskWorkspace, advanceOpenSpecTaskWorkspace, verifyOpenSpecTaskWorkspace } from './openspec/execute.js';
9
31
  import { validateOpenSpecChangeWorkspace } from './openspec/change-validate.js';
10
32
  import { verifyQualityWorkspace } from './quality.js';
33
+ import { appendReleaseEntry, getCurrentReleaseEntry, loadReleaseLedger, saveReleaseLedger, updateReleaseTag } from './release-ledger.js';
34
+ import { describeTestStrategy, taskTestStrategy } from './test-strategy.js';
11
35
  import { timestamp } from './time.js';
12
36
 
13
37
  const LOOP_FEATURE_LIST = path.join('.openprd', 'harness', 'feature-list.json');
@@ -207,6 +231,8 @@ async function appendFailedApproach(projectRoot, payload) {
207
231
  function featureTaskFromOpenSpecTask(task, changeId) {
208
232
  const deps = taskDeps(task);
209
233
  const taskSlug = slugifyLoopToken(task.title ?? task.id ?? 'task', 'task');
234
+ const testStrategy = taskTestStrategy(task);
235
+ const executionStrategy = taskExecutionStrategy(task);
210
236
  return {
211
237
  id: task.id,
212
238
  title: task.title,
@@ -222,13 +248,22 @@ function featureTaskFromOpenSpecTask(task, changeId) {
222
248
  done: task.metadata?.done ?? null,
223
249
  verify: task.metadata?.verify ?? null,
224
250
  oracle: task.metadata?.oracle ?? null,
225
- commitMessage: `Complete ${task.id}: ${task.title}`,
251
+ testStrategy,
252
+ testStrategyDescription: describeTestStrategy(testStrategy),
253
+ executionStrategy,
254
+ executionStrategyDescription: describeExecutionStrategy(executionStrategy),
255
+ commitMessage: buildTaskCommitMessage(task),
226
256
  sessionScope: [
227
257
  '只处理这个任务,不要在同一会话继续下一个任务。',
228
258
  '完成代码后必须先自测,失败就修复并重新自测。',
229
- '代码修改完成后、最终回复前,针对本轮实际 touched code files 运行 `openprd dev-check . <file...>`;若出现 attention warning,说明局部职责、影响范围,以及是否已拆分或为什么窄修暂不拆。',
259
+ '代码修改完成后、最终回复前,针对本轮实际 touched code files 运行 `openprd dev-check . <file...>`;若出现需要关注的文件,最终回复直接复用 dev-check 生成的 **后续建议** 表格说明影响对象、关注程度、本次处理结果和后续建议,并保留“关注程度”列里的完整风险标签,不要缩成纯 emoji;如果你改写了“预警原因 / 本次处理结果 / 后续建议”,先用 `node scripts/dev-check-wrapup-copy.mjs --validate` 校验每格不超过 20 字;若报错,按提示缩短后重试。',
230
260
  '涉及前端界面时,在 Codex 客户端优先使用 Computer Use;在 Codex CLI 或 Claude Code 中优先使用 Playwright、MCP 或等价浏览器自动化。',
231
261
  '纯后端、脚本或库任务使用最贴近项目的脚本、单测、集成测试或命令行验证。',
262
+ `本任务测试策略: ${describeTestStrategy(testStrategy)}`,
263
+ `本任务执行策略: ${describeExecutionStrategy(executionStrategy)}`,
264
+ executionStrategy.ownerRole === 'worker'
265
+ ? `当前会话角色: ${labelOwnerRole(executionStrategy.ownerRole)};写入范围限制为 ${executionStrategy.writeScope.join(', ')},最终集成和总验证由主 Agent 负责。`
266
+ : `当前会话角色: ${labelOwnerRole(executionStrategy.ownerRole)};由主 Agent 直接推进并负责最终集成。`,
232
267
  '涉及后端、脚本、Agent、工具链、服务或数据处理变更时,把 CLI 与 API 视为同级接入面;检查命令入口、参数、输出契约、`help`/`doctor`/`dry-run`/`status` 与接口协议、返回结构、身份边界是否受影响,并同步更新 `docs/basic/backend-structure.md`;若某一面不适用也要明确写原因。',
233
268
  '新增或修改文件时先做文档影响判定:缺少 docs/basic、文件说明书或文件夹 README 就补齐;已有文档若因本任务职责、流程、结构、依赖或产品行为变化而过期,就同步更新。',
234
269
  ],
@@ -402,6 +437,12 @@ function renderLoopPrompt({ agent, projectRoot, featureList, task, dependency, m
402
437
  `跨对话继续请引用: ${task.taskHandle}`,
403
438
  `完成条件: ${task.done ?? '未指定'}`,
404
439
  `自测命令: ${task.verify ?? '未指定'}`,
440
+ `测试策略: ${task.testStrategyDescription ?? describeTestStrategy(taskTestStrategy(task))}`,
441
+ `执行策略: ${task.executionStrategyDescription ?? describeExecutionStrategy(task.executionStrategy ?? taskExecutionStrategy(task))}`,
442
+ `当前角色: ${labelOwnerRole(task.executionStrategy?.ownerRole ?? taskExecutionStrategy(task).ownerRole)}`,
443
+ `写入范围: ${(task.executionStrategy?.writeScope ?? taskExecutionStrategy(task).writeScope).join(', ')}`,
444
+ `局部验证: ${task.executionStrategy?.localVerify ?? taskExecutionStrategy(task).localVerify}`,
445
+ `最终集成 owner: ${task.executionStrategy?.integrationOwner ?? taskExecutionStrategy(task).integrationOwner}`,
405
446
  `对照基准: ${task.oracle ?? '未指定'}`,
406
447
  `依赖是否就绪: ${dependency?.ready ? '是' : '否'}`,
407
448
  dependency?.missing?.length ? `缺失依赖: ${dependency.missing.join(', ')}` : '',
@@ -410,18 +451,19 @@ function renderLoopPrompt({ agent, projectRoot, featureList, task, dependency, m
410
451
  '',
411
452
  '不要开始下一个任务。如果发现任务仍然过大,先拆分任务文件,并只完成最小可用切片。',
412
453
  task.oracle ? '如果任务定义了对照基准,必须显式对照 reference/oracle,并把偏差、死路或替代方案记到 `.openprd/harness/failed-approaches.md`。' : '',
413
- '代码修改完成后、最终回复前,针对本轮实际 touched code files 运行 `openprd dev-check . <file...>`;若出现 attention warning,说明局部职责、影响范围,以及是否已拆分或为什么窄修暂不拆。',
454
+ '代码修改完成后、最终回复前,针对本轮实际 touched code files 运行 `openprd dev-check . <file...>`;若出现需要关注的文件,最终回复直接复用 dev-check 生成的 **后续建议** 表格说明影响对象、关注程度、本次处理结果和后续建议,并保留“关注程度”列里的完整风险标签,不要缩成纯 emoji;如果你改写了“预警原因 / 本次处理结果 / 后续建议”,先用 `node scripts/dev-check-wrapup-copy.mjs --validate` 校验每格不超过 20 字;若报错,按提示缩短后重试。',
414
455
  '',
415
456
  '## 自测与界面验证要求',
416
457
  '',
417
- '1. 必须运行本任务的自测命令。',
418
- '2. 必须运行 `openprd run . --verify`。',
458
+ '1. 先按本任务测试策略选择最小足够证据:小范围逻辑优先单测,契约/跨模块用集成,用户主路径或运行态用端到端/专项验证。',
459
+ '2. 必须运行本任务的自测命令,并把结果作为 task-scoped evidence 记录。',
460
+ '3. 不要在每个 task 中运行全局 `openprd run . --verify`;它只用于无下一任务的阶段收口或高风险动作前。',
419
461
  ...frontendStrategy,
420
- '5. 阶段性测试报告会由 `openprd loop . --finish` 写入 `.openprd/harness/test-reports/`,并与本任务改动一起进入 commit。',
462
+ '6. 阶段性测试报告会由 `openprd loop . --finish` 写入 `.openprd/harness/test-reports/`,并与本任务改动一起进入 commit。',
421
463
  '',
422
464
  '## 收尾步骤',
423
465
  '',
424
- '1. 确认自测、界面验证和 OpenPrd verify 都已经通过。',
466
+ '1. 确认本任务自测、界面验证和 evidence 记录都已经通过。',
425
467
  '2. 留下简洁总结,说明改动文件和验证结果。',
426
468
  '3. 如果这是手动执行 prompt,用以下命令结束任务并提交:',
427
469
  task.oracle
@@ -444,6 +486,8 @@ function renderLoopPrompt({ agent, projectRoot, featureList, task, dependency, m
444
486
  done: task.done,
445
487
  verify: task.verify,
446
488
  oracle: task.oracle,
489
+ testStrategy: task.testStrategy ?? taskTestStrategy(task),
490
+ executionStrategy: task.executionStrategy ?? taskExecutionStrategy(task),
447
491
  },
448
492
  }, null, 2),
449
493
  '',
@@ -497,6 +541,158 @@ async function gitCommit(projectRoot, message) {
497
541
  };
498
542
  }
499
543
 
544
+ async function gitCheckTagName(projectRoot, tagName) {
545
+ return runCommand('git', ['check-ref-format', '--allow-onelevel', `refs/tags/${tagName}`], { cwd: projectRoot });
546
+ }
547
+
548
+ async function gitReadLocalTagSha(projectRoot, tagName) {
549
+ const result = await runCommand('git', ['rev-parse', '-q', '--verify', `refs/tags/${tagName}`], { cwd: projectRoot });
550
+ if (!result.ok) return null;
551
+ return result.stdout.trim() || null;
552
+ }
553
+
554
+ async function gitReadRemoteTagSha(projectRoot, tagName) {
555
+ const remote = await runCommand('git', ['remote', 'get-url', 'origin'], { cwd: projectRoot });
556
+ if (!remote.ok) {
557
+ return { status: 'no-remote', sha: null, warning: null };
558
+ }
559
+ const result = await runCommand('git', ['ls-remote', '--tags', '--refs', 'origin', `refs/tags/${tagName}`], { cwd: projectRoot });
560
+ if (!result.ok) {
561
+ return {
562
+ status: 'unknown',
563
+ sha: null,
564
+ warning: `无法确认远端 tag ${tagName} 的状态;本地 tag 仍会按当前 commit 更新。`,
565
+ };
566
+ }
567
+ const line = result.stdout.trim();
568
+ if (!line) {
569
+ return { status: 'absent', sha: null, warning: null };
570
+ }
571
+ return { status: 'present', sha: line.split(/\s+/u)[0] ?? null, warning: null };
572
+ }
573
+
574
+ async function syncLocalVersionTag(projectRoot, version, sha) {
575
+ const tagName = String(version ?? '').trim();
576
+ if (!tagName || !sha) {
577
+ return { ok: true, skipped: true, tagName: tagName || null, warning: null };
578
+ }
579
+
580
+ const valid = await gitCheckTagName(projectRoot, tagName);
581
+ if (!valid.ok) {
582
+ return {
583
+ ok: false,
584
+ skipped: true,
585
+ tagName,
586
+ warning: `项目版本 ${tagName} 不能安全地作为 git tag 名称;已跳过本地 tag 更新。`,
587
+ };
588
+ }
589
+
590
+ const localSha = await gitReadLocalTagSha(projectRoot, tagName);
591
+ const remote = await gitReadRemoteTagSha(projectRoot, tagName);
592
+ if (remote.status === 'present' && remote.sha && remote.sha !== sha) {
593
+ return {
594
+ ok: false,
595
+ skipped: true,
596
+ tagName,
597
+ localSha,
598
+ remoteSha: remote.sha,
599
+ remoteStatus: remote.status,
600
+ warning: `远端已有同名 tag ${tagName} 指向 ${remote.sha.slice(0, 7)};为避免改写历史,已跳过本地 tag 移动。`,
601
+ };
602
+ }
603
+
604
+ if (localSha === sha) {
605
+ return {
606
+ ok: true,
607
+ skipped: false,
608
+ tagName,
609
+ localSha,
610
+ remoteSha: remote.sha,
611
+ remoteStatus: remote.status,
612
+ warning: remote.warning,
613
+ };
614
+ }
615
+
616
+ const command = localSha ? ['tag', '-f', tagName, sha] : ['tag', tagName, sha];
617
+ const result = await runCommand('git', command, { cwd: projectRoot });
618
+ if (!result.ok) {
619
+ return {
620
+ ok: false,
621
+ skipped: true,
622
+ tagName,
623
+ localSha,
624
+ remoteSha: remote.sha,
625
+ remoteStatus: remote.status,
626
+ warning: `git tag ${tagName} 更新失败:${trimOutput(result.stderr || result.stdout)}`,
627
+ };
628
+ }
629
+
630
+ const nextLocalSha = await gitReadLocalTagSha(projectRoot, tagName);
631
+ return {
632
+ ok: true,
633
+ skipped: false,
634
+ tagName,
635
+ localSha: nextLocalSha,
636
+ remoteSha: remote.sha,
637
+ remoteStatus: remote.status,
638
+ warning: remote.warning,
639
+ };
640
+ }
641
+
642
+ async function updateReleaseLedgerAfterFinish(projectRoot, task, commitSha = null) {
643
+ const loaded = await loadReleaseLedger(projectRoot);
644
+ const current = getCurrentReleaseEntry(loaded.ledger);
645
+ if (!loaded.ledger.enabled || !current?.version) {
646
+ return null;
647
+ }
648
+ if (current.status === 'released') {
649
+ return {
650
+ version: current.version,
651
+ skipped: true,
652
+ warnings: [`项目版本 ${current.version} 已标记为 released;本次任务不会自动累计到这个版本。`],
653
+ tag: null,
654
+ };
655
+ }
656
+
657
+ let ledger = loaded.ledger;
658
+ const appended = appendReleaseEntry(ledger, task.done ?? task.title ?? task.id, {
659
+ version: current.version,
660
+ fallbackType: '调整',
661
+ source: {
662
+ kind: 'loop-finish',
663
+ changeId: task.changeId ?? null,
664
+ taskId: task.id ?? null,
665
+ taskHandle: task.taskHandle ?? null,
666
+ commitSha: commitSha ?? null,
667
+ },
668
+ });
669
+ ledger = appended.ledger;
670
+
671
+ let tag = null;
672
+ if (commitSha) {
673
+ tag = await syncLocalVersionTag(projectRoot, current.version, commitSha);
674
+ const tagged = updateReleaseTag(ledger, {
675
+ version: current.version,
676
+ name: tag.tagName ?? current.version,
677
+ localSha: tag.localSha ?? null,
678
+ remoteSha: tag.remoteSha ?? null,
679
+ remoteStatus: tag.remoteStatus ?? null,
680
+ warning: tag.warning ?? null,
681
+ updatedAt: timestamp(),
682
+ });
683
+ ledger = tagged.ledger;
684
+ }
685
+
686
+ await saveReleaseLedger(projectRoot, ledger);
687
+ return {
688
+ version: current.version,
689
+ skipped: false,
690
+ added: appended.added,
691
+ warnings: tag?.warning ? [tag.warning] : [],
692
+ tag,
693
+ };
694
+ }
695
+
500
696
  function trimOutput(value) {
501
697
  const text = String(value ?? '').trim();
502
698
  if (!text) return '无';
@@ -558,6 +754,7 @@ async function writeTestReport(projectRoot, { task, agent, advanced, change }) {
558
754
  kind: inferUiVerificationHint(task, agent).includes('前端界面任务') ? 'ui-regression' : 'command-regression',
559
755
  verifyCommand: advanced.verification?.command ?? task.verify ?? '未指定',
560
756
  oracle: task.oracle ?? null,
757
+ testStrategy: task.testStrategy ?? taskTestStrategy(task),
561
758
  summary: {
562
759
  total: 1,
563
760
  passed: advanced.verification?.ok ? 1 : 0,
@@ -585,6 +782,7 @@ async function writeTestReport(projectRoot, { task, agent, advanced, change }) {
585
782
  `- 变更: ${task.changeId}`,
586
783
  `- 完成条件: ${task.done ?? '未指定'}`,
587
784
  `- 自测命令: ${advanced.verification?.command ?? task.verify ?? '未指定'}`,
785
+ `- 测试策略: ${task.testStrategyDescription ?? describeTestStrategy(task.testStrategy ?? taskTestStrategy(task))}`,
588
786
  `- 对照基准: ${task.oracle ?? '未指定'}`,
589
787
  `- 自测结果: ${advanced.verification?.ok ? '通过' : '失败或未运行'}`,
590
788
  `- Change 校验: ${change.ok ? '通过' : '失败'}`,
@@ -645,6 +843,8 @@ export async function initLoopWorkspace(projectRoot, options = {}) {
645
843
  requireVerify: true,
646
844
  requireCommit: true,
647
845
  continuity: 'files-and-git-history',
846
+ executionModes: ['serial', 'parallel-workers', 'parallel-workers-isolated'],
847
+ coordinationRule: 'main-agent assigns bounded worker shards and owns final review/integration',
648
848
  },
649
849
  source: 'openprd loop init',
650
850
  tasks: [],
@@ -695,6 +895,9 @@ export async function planLoopWorkspace(projectRoot, options = {}) {
695
895
  continuity: 'files-and-git-history',
696
896
  agentSessionRule: 'start a new Codex or Claude session for exactly one task',
697
897
  testReportRule: 'write one staged test report before each task commit',
898
+ executionModes: ['serial', 'parallel-workers', 'parallel-workers-isolated'],
899
+ coordinationRule: 'main-agent assigns bounded worker shards and owns final review/integration',
900
+ workerContract: ['write-scope', 'owner-role', 'local-verify', 'integration-owner'],
698
901
  },
699
902
  tasks,
700
903
  };
@@ -998,6 +1201,16 @@ export async function finishLoopWorkspace(projectRoot, options = {}) {
998
1201
  }
999
1202
  }
1000
1203
 
1204
+ const projectRelease = await updateReleaseLedgerAfterFinish(
1205
+ projectRoot,
1206
+ task,
1207
+ commit && !commit.skipped ? commit.sha : null,
1208
+ ).catch((error) => ({
1209
+ skipped: true,
1210
+ warnings: [error instanceof Error ? error.message : String(error)],
1211
+ tag: null,
1212
+ }));
1213
+
1001
1214
  const updatedList = updateTask(featureList, task.id, {
1002
1215
  status: 'done',
1003
1216
  lastVerifiedAt: timestamp(),
@@ -1118,6 +1331,9 @@ export async function finishLoopWorkspace(projectRoot, options = {}) {
1118
1331
  ? null
1119
1332
  : `项目经验草案: ${path.relative(projectRoot, knowledgeReview.files?.draftSkill ?? knowledgeReview.files?.candidateDir ?? '') || '已生成'}。`,
1120
1333
  commit ? `Commit: ${commit.skipped ? '跳过' : commit.sha}` : 'Commit: 未请求。',
1334
+ projectRelease?.version ? `项目版本: ${projectRelease.version}。` : null,
1335
+ projectRelease?.tag?.tagName ? `版本 tag: ${projectRelease.tag.tagName}${projectRelease.tag.localSha ? ` -> ${projectRelease.tag.localSha}` : ''}。` : null,
1336
+ ...(projectRelease?.warnings ?? []).map((warning) => `版本轨道: ${warning}`),
1121
1337
  ]));
1122
1338
  await appendJsonl(harnessPath(projectRoot, LOOP_SESSIONS), {
1123
1339
  version: 1,
@@ -1130,6 +1346,7 @@ export async function finishLoopWorkspace(projectRoot, options = {}) {
1130
1346
  ok: true,
1131
1347
  oracle: task.oracle ?? null,
1132
1348
  commit: commit ? { ok: commit.ok, skipped: commit.skipped, sha: commit.sha ?? null } : null,
1349
+ projectRelease: projectRelease ?? null,
1133
1350
  testReport: testReport.markdownPath,
1134
1351
  regressionHtml: testReport.htmlPath,
1135
1352
  quality: quality
@@ -1183,6 +1400,7 @@ export async function finishLoopWorkspace(projectRoot, options = {}) {
1183
1400
  advanced,
1184
1401
  change,
1185
1402
  commit,
1403
+ projectRelease,
1186
1404
  testReport: testReport.markdownPath,
1187
1405
  regressionHtml: testReport.htmlPath,
1188
1406
  quality,
@@ -1209,6 +1427,47 @@ export async function runLoopWorkspace(projectRoot, options = {}) {
1209
1427
  shell: true,
1210
1428
  }
1211
1429
  : defaultAgentInvocation(agent, projectRoot, promptResult.promptPath);
1430
+ const codexPreflight = agent === 'codex' && !options.agentCommand && !options.dryRun
1431
+ ? await ensureCodexCliReady({
1432
+ cwd: projectRoot,
1433
+ repair: Boolean(options.repairAgent),
1434
+ runCommand: options.codexRunCommand,
1435
+ packageManager: options.packageManager,
1436
+ })
1437
+ : null;
1438
+
1439
+ if (codexPreflight && !codexPreflight.ok) {
1440
+ await appendJsonl(harnessPath(projectRoot, LOOP_SESSIONS), {
1441
+ version: 1,
1442
+ at: timestamp(),
1443
+ action: codexPreflight.repairAttempted ? 'agent-preflight-repair-failed' : 'agent-preflight-failed',
1444
+ agent,
1445
+ taskId: promptResult.task.id,
1446
+ taskHandle: promptResult.task.taskHandle,
1447
+ taskTitle: promptResult.task.title,
1448
+ ok: false,
1449
+ preflight: {
1450
+ ok: codexPreflight.preflight.ok,
1451
+ diagnosticType: codexPreflight.preflight.diagnostic?.type ?? null,
1452
+ missingPackage: codexPreflight.preflight.diagnostic?.missingPackage ?? null,
1453
+ repairAttempted: codexPreflight.repairAttempted,
1454
+ },
1455
+ });
1456
+ return {
1457
+ ok: false,
1458
+ action: 'loop-run',
1459
+ projectRoot,
1460
+ agent,
1461
+ task: promptResult.task,
1462
+ promptPath: promptResult.promptPath,
1463
+ invocation,
1464
+ codexRuntime: codexPreflight,
1465
+ preflight: codexPreflight.preflight,
1466
+ repair: codexPreflight.repair,
1467
+ repairAttempted: codexPreflight.repairAttempted,
1468
+ errors: codexPreflight.errors,
1469
+ };
1470
+ }
1212
1471
 
1213
1472
  const sessionEvent = {
1214
1473
  version: 1,
@@ -1221,6 +1480,11 @@ export async function runLoopWorkspace(projectRoot, options = {}) {
1221
1480
  changeId: promptResult.task.changeId,
1222
1481
  promptPath: promptResult.promptPath,
1223
1482
  invocation: invocation.display,
1483
+ preflight: codexPreflight ? {
1484
+ ok: codexPreflight.preflight.ok,
1485
+ command: codexPreflight.preflight.command.display,
1486
+ repairAttempted: codexPreflight.repairAttempted,
1487
+ } : null,
1224
1488
  };
1225
1489
  await appendJsonl(harnessPath(projectRoot, LOOP_SESSIONS), sessionEvent);
1226
1490
  await updateLoopState(projectRoot, {
@@ -1241,13 +1505,16 @@ export async function runLoopWorkspace(projectRoot, options = {}) {
1241
1505
  task: promptResult.task,
1242
1506
  promptPath: promptResult.promptPath,
1243
1507
  invocation,
1508
+ codexRuntime: codexPreflight,
1509
+ preflight: codexPreflight?.preflight ?? null,
1244
1510
  prompt: promptResult.prompt,
1245
1511
  };
1246
1512
  }
1247
1513
 
1514
+ const runAgentCommand = options.agentRunCommand ?? runCommand;
1248
1515
  const run = invocation.shell
1249
- ? await runCommand(invocation.command, [], { cwd: projectRoot, shell: true, stdin: prompt })
1250
- : await runCommand(invocation.command, invocation.args, { cwd: projectRoot, stdin: prompt });
1516
+ ? await runAgentCommand(invocation.command, [], { cwd: projectRoot, shell: true, stdin: prompt })
1517
+ : await runAgentCommand(invocation.command, invocation.args, { cwd: projectRoot, stdin: prompt });
1251
1518
  await appendJsonl(harnessPath(projectRoot, LOOP_SESSIONS), {
1252
1519
  version: 1,
1253
1520
  at: timestamp(),
@@ -1284,6 +1551,10 @@ export async function runLoopWorkspace(projectRoot, options = {}) {
1284
1551
  agent,
1285
1552
  task: promptResult.task,
1286
1553
  run,
1554
+ codexRuntime: codexPreflight,
1555
+ preflight: codexPreflight?.preflight ?? null,
1556
+ repair: codexPreflight?.repair ?? null,
1557
+ repairAttempted: Boolean(codexPreflight?.repairAttempted),
1287
1558
  finish,
1288
1559
  errors: finish.errors ?? [],
1289
1560
  };