@openprd/cli 0.1.1 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.openprd/README.md +43 -69
- package/.openprd/README_EN.md +84 -0
- package/.openprd/benchmarks/index.md +7 -0
- package/.openprd/benchmarks/sources.yaml +25 -3
- package/.openprd/discovery/config.json +16 -2
- package/.openprd/engagements/active/flows.md +19 -14
- package/.openprd/engagements/active/handoff.md +11 -4
- package/.openprd/engagements/active/prd.md +99 -71
- package/.openprd/engagements/active/review.html +4 -4
- package/.openprd/engagements/active/roles.md +9 -8
- package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +4 -4
- package/.openprd/engagements/work-units/wu-20260602113956-a99b5b88.json +18 -0
- package/.openprd/engagements/work-units/wu-20260602122244-78656aaf.json +18 -0
- package/.openprd/engagements/work-units/wu-20260602122442-e96489e2.json +18 -0
- package/.openprd/engagements/work-units/wu-20260602132835-695429e8.json +18 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/candidate.json +78 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/diagnostic-report.json +129 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/root-cause-candidates.json +41 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/timeline.json +14 -0
- package/.openprd/knowledge/drafts/openprd-experience-diagnostic-candidate-turn-1780116203372-5f266a79e968c758/SKILL.md +49 -0
- package/.openprd/knowledge/index.json +44 -4
- package/.openprd/reviews/v0001.html +195 -129
- package/.openprd/reviews/v0002.html +1150 -0
- package/.openprd/reviews/v0003.html +1150 -0
- package/.openprd/reviews/v0004.html +1150 -0
- package/.openprd/reviews/v0005.html +1150 -0
- package/.openprd/standards/config.json +12 -9
- package/.openprd/state/changes.json +17 -2
- package/.openprd/state/current.json +399 -63
- package/.openprd/state/release-ledger.json +344 -0
- package/.openprd/state/version-index.json +52 -0
- package/.openprd/state/versions/v0002.json +264 -0
- package/.openprd/state/versions/v0002.md +183 -0
- package/.openprd/state/versions/v0003.json +269 -0
- package/.openprd/state/versions/v0003.md +188 -0
- package/.openprd/state/versions/v0004.json +274 -0
- package/.openprd/state/versions/v0004.md +193 -0
- package/.openprd/state/versions/v0005.json +299 -0
- package/.openprd/state/versions/v0005.md +189 -0
- package/.openprd/templates/agent/intake.md +5 -4
- package/.openprd/templates/b2b/intake.md +5 -4
- package/.openprd/templates/base/intake.md +10 -4
- package/.openprd/templates/company/README.md +9 -7
- package/.openprd/templates/company/README_EN.md +12 -0
- package/.openprd/templates/consumer/intake.md +5 -4
- package/.openprd/templates/industry/README.md +12 -10
- package/.openprd/templates/industry/README_EN.md +18 -0
- package/.openprd/templates/project/README.md +11 -9
- package/.openprd/templates/project/README_EN.md +16 -0
- package/.openprd/templates/session/README.md +11 -9
- package/.openprd/templates/session/README_EN.md +16 -0
- package/AGENTS.md +12 -8
- package/README.md +399 -438
- package/README_CN.md +4 -578
- package/README_EN.md +850 -0
- package/docs/assets/openprd-requirement-routing-en.png +0 -0
- package/docs/assets/openprd-requirement-routing-en.svg +102 -0
- package/docs/assets/openprd-requirement-routing-zh-refined.png +0 -0
- package/docs/assets/openprd-requirement-routing-zh.png +0 -0
- package/docs/assets/openprd-requirement-routing-zh.svg +102 -0
- package/package.json +6 -2
- package/scripts/dev-check-wrapup-copy.mjs +110 -0
- package/scripts/openprd-github-release-notes.mjs +99 -0
- package/scripts/quality-perf-check.mjs +203 -0
- package/skills/openprd-benchmark-router/SKILL.md +1 -0
- package/skills/openprd-benchmark-router/references/benchmark-sources.md +1 -0
- package/skills/openprd-benchmark-router/references/source-policy.md +2 -0
- package/skills/openprd-discovery-loop/SKILL.md +2 -2
- package/skills/openprd-harness/SKILL.md +46 -24
- package/skills/openprd-harness/references/workflow-gates.md +15 -0
- package/skills/openprd-quality/SKILL.md +10 -4
- package/skills/openprd-requirement-intake/SKILL.md +31 -20
- package/skills/openprd-requirement-intake/references/prd-template-lenses.md +6 -6
- package/skills/openprd-requirement-intake/references/routing-rubric.md +10 -2
- package/skills/openprd-router/SKILL.md +2 -2
- package/skills/openprd-shared/SKILL.md +51 -23
- package/skills/openprd-standards/SKILL.md +2 -1
- package/src/agent-integration.js +265 -65
- package/src/benchmark/constants.js +107 -0
- package/src/benchmark/operations.js +235 -0
- package/src/benchmark/registry.js +64 -0
- package/src/benchmark/render.js +115 -0
- package/src/benchmark/source.js +617 -0
- package/src/benchmark/storage.js +121 -0
- package/src/benchmark/verify.js +235 -0
- package/src/benchmark.js +50 -851
- package/src/change-summary.js +339 -0
- package/src/cli/args.js +67 -6
- package/src/cli/basic-print.js +365 -0
- package/src/cli/benchmark-print.js +91 -0
- package/src/cli/change-print.js +221 -0
- package/src/cli/doctor-print.js +268 -0
- package/src/cli/growth-print.js +176 -0
- package/src/cli/print.js +73 -1384
- package/src/cli/quality-print.js +284 -0
- package/src/cli/run-print.js +297 -0
- package/src/cli/shared-print.js +127 -0
- package/src/cli/workflow-print.js +195 -0
- package/src/codex-hook-runner-template.mjs +639 -117
- package/src/codex-runtime.js +324 -0
- package/src/dev-standards.js +178 -5
- package/src/diagram-core.js +5 -5
- package/src/discovery.js +2 -1
- package/src/execution-strategy.js +369 -0
- package/src/fleet.js +4 -0
- package/src/github-release.js +156 -0
- package/src/growth.js +311 -13
- package/src/html-artifact-utils.js +25 -0
- package/src/html-artifacts.js +157 -1596
- package/src/knowledge.js +1176 -75
- package/src/language-policy.js +2 -112
- package/src/learning-html-artifact.js +1031 -0
- package/src/learning-review.js +3 -2
- package/src/loop.js +280 -9
- package/src/openprd.js +341 -38
- package/src/openspec/change-validate.js +0 -9
- package/src/openspec/execute.js +79 -3
- package/src/openspec/generate.js +33 -20
- package/src/openspec/tasks.js +33 -2
- package/src/prd-core.js +10 -9
- package/src/product-type-copy.js +69 -0
- package/src/quality-html-artifact.js +108 -9
- package/src/quality-learning.js +30 -0
- package/src/quality-visual-review.js +237 -0
- package/src/quality.js +329 -43
- package/src/registry-hygiene.js +54 -0
- package/src/release-ledger.js +413 -0
- package/src/review-presentation.js +12 -6
- package/src/run-harness.js +722 -48
- package/src/session-binding.js +40 -3
- package/src/session-registry.js +159 -0
- package/src/standards.js +5 -3
- package/src/test-strategy.js +386 -0
- package/src/visual-compare.js +915 -34
- package/src/work-unit-migration.js +5 -1
- package/src/workspace-core.js +343 -19
- package/src/workspace-workflow.js +538 -134
package/src/learning-review.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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.
|
|
458
|
+
'1. 先按本任务测试策略选择最小足够证据:小范围逻辑优先单测,契约/跨模块用集成,用户主路径或运行态用端到端/专项验证。',
|
|
459
|
+
'2. 必须运行本任务的自测命令,并把结果作为 task-scoped evidence 记录。',
|
|
460
|
+
'3. 不要在每个 task 中运行全局 `openprd run . --verify`;它只用于无下一任务的阶段收口或高风险动作前。',
|
|
419
461
|
...frontendStrategy,
|
|
420
|
-
'
|
|
462
|
+
'6. 阶段性测试报告会由 `openprd loop . --finish` 写入 `.openprd/harness/test-reports/`,并与本任务改动一起进入 commit。',
|
|
421
463
|
'',
|
|
422
464
|
'## 收尾步骤',
|
|
423
465
|
'',
|
|
424
|
-
'1.
|
|
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
|
|
1250
|
-
: await
|
|
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
|
};
|