@ifrankwang/openspec-agents 0.150.2 → 0.151.1
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/.dsh-plugin/opx-tools.mjs +84 -21
- package/.mcp-server/cli.mjs +85 -22
- package/assets/agents/openspec-reviewer.md +4 -3
- package/assets/skills/api-test/SKILL.md +18 -1
- package/assets/skills/test-design/SKILL.md +8 -6
- package/assets/workflows/review-simple.yaml +5 -3
- package/assets/workflows/review.yaml +8 -4
- package/assets/workflows/task-simple.yaml +8 -5
- package/assets/workflows/task.yaml +11 -6
- package/dsh/cordis.patch.yml +4 -3
- package/package.json +1 -1
- package/src/core/git.ts +95 -24
- package/src/core/tools/submit.ts +37 -9
|
@@ -205,6 +205,31 @@ async function isWorktreeClean(worktree) {
|
|
|
205
205
|
const out = await runGit(worktree, ["status", "--porcelain"]);
|
|
206
206
|
return out.length === 0;
|
|
207
207
|
}
|
|
208
|
+
function isDocLikePath(p) {
|
|
209
|
+
return p.startsWith("openspec/") || /\.md$/i.test(p);
|
|
210
|
+
}
|
|
211
|
+
async function listDirtyNonDocFiles(worktree) {
|
|
212
|
+
const res = await runGitChecked(worktree, ["status", "--porcelain", "-uall"]);
|
|
213
|
+
if (!res.success)
|
|
214
|
+
return [];
|
|
215
|
+
const seen = new Set;
|
|
216
|
+
const files = [];
|
|
217
|
+
for (const entry of parseStatusEntries(res.stdout)) {
|
|
218
|
+
for (const p of entry.paths) {
|
|
219
|
+
if (!p)
|
|
220
|
+
continue;
|
|
221
|
+
if (p.startsWith("openspec/states/"))
|
|
222
|
+
continue;
|
|
223
|
+
if (isDocLikePath(p))
|
|
224
|
+
continue;
|
|
225
|
+
if (seen.has(p))
|
|
226
|
+
continue;
|
|
227
|
+
seen.add(p);
|
|
228
|
+
files.push(p);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return files;
|
|
232
|
+
}
|
|
208
233
|
async function markTaskGroupCheckboxesComplete(worktree, changeId, taskGroupId) {
|
|
209
234
|
const tasksMdPath = path.join(worktree, "openspec", "changes", changeId, "tasks.md");
|
|
210
235
|
let content;
|
|
@@ -615,27 +640,48 @@ async function reconcileMainPollution(mainRepo, worktreePath, opts) {
|
|
|
615
640
|
}
|
|
616
641
|
}
|
|
617
642
|
async function autoCommitWorktreeChanges(wtPath, agent, stepId) {
|
|
618
|
-
const statusRes = await runGitChecked(wtPath, ["status", "--porcelain"]);
|
|
643
|
+
const statusRes = await runGitChecked(wtPath, ["status", "--porcelain", "-uall"]);
|
|
619
644
|
if (!statusRes.success) {
|
|
620
645
|
return { status: "failed", files: [], untrackedFiles: [], stderr: statusRes.stderr };
|
|
621
646
|
}
|
|
622
|
-
const
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
if (
|
|
647
|
+
const trackedFiles = [];
|
|
648
|
+
const untrackedDocFiles = [];
|
|
649
|
+
const untrackedOtherFiles = [];
|
|
650
|
+
const seen = new Set;
|
|
651
|
+
const pushUnique = (list, path2) => {
|
|
652
|
+
if (seen.has(path2))
|
|
653
|
+
return;
|
|
654
|
+
seen.add(path2);
|
|
655
|
+
list.push(path2);
|
|
656
|
+
};
|
|
657
|
+
for (const entry of parseStatusEntries(statusRes.stdout)) {
|
|
658
|
+
const isUntracked = entry.x === "?" && entry.y === "?";
|
|
659
|
+
if (isUntracked) {
|
|
660
|
+
for (const path2 of entry.paths) {
|
|
661
|
+
if (!path2 || path2.startsWith("openspec/states/"))
|
|
662
|
+
continue;
|
|
663
|
+
pushUnique(isDocLikePath(path2) ? untrackedDocFiles : untrackedOtherFiles, path2);
|
|
664
|
+
}
|
|
628
665
|
continue;
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
666
|
+
}
|
|
667
|
+
const newPath = entry.paths[entry.paths.length - 1];
|
|
668
|
+
if (!newPath || newPath.startsWith("openspec/states/"))
|
|
669
|
+
continue;
|
|
670
|
+
pushUnique(trackedFiles, newPath);
|
|
633
671
|
}
|
|
634
|
-
if (
|
|
635
|
-
return { status: "skipped", files: [], untrackedFiles };
|
|
672
|
+
if (trackedFiles.length === 0 && untrackedDocFiles.length === 0) {
|
|
673
|
+
return { status: "skipped", files: [], untrackedFiles: untrackedOtherFiles };
|
|
674
|
+
}
|
|
675
|
+
const committedFiles = [...trackedFiles, ...untrackedDocFiles];
|
|
636
676
|
const addResult = await runGitChecked(wtPath, ["add", "-u", "--", ".", ":(exclude)openspec/states"]);
|
|
637
677
|
if (!addResult.success) {
|
|
638
|
-
return { status: "failed", files, untrackedFiles, stderr: addResult.stderr };
|
|
678
|
+
return { status: "failed", files: committedFiles, untrackedFiles: untrackedOtherFiles, stderr: addResult.stderr };
|
|
679
|
+
}
|
|
680
|
+
if (untrackedDocFiles.length > 0) {
|
|
681
|
+
const addDocResult = await runGitChecked(wtPath, ["add", "--", ...untrackedDocFiles]);
|
|
682
|
+
if (!addDocResult.success) {
|
|
683
|
+
return { status: "failed", files: committedFiles, untrackedFiles: untrackedOtherFiles, stderr: addDocResult.stderr };
|
|
684
|
+
}
|
|
639
685
|
}
|
|
640
686
|
const commitResult = await runGitChecked(wtPath, [
|
|
641
687
|
"commit",
|
|
@@ -643,9 +689,9 @@ async function autoCommitWorktreeChanges(wtPath, agent, stepId) {
|
|
|
643
689
|
`docs(opx): direct fixes by ${agent} (${stepId})`
|
|
644
690
|
]);
|
|
645
691
|
if (!commitResult.success) {
|
|
646
|
-
return { status: "failed", files, untrackedFiles, stderr: commitResult.stderr };
|
|
692
|
+
return { status: "failed", files: committedFiles, untrackedFiles: untrackedOtherFiles, stderr: commitResult.stderr };
|
|
647
693
|
}
|
|
648
|
-
return { status: "committed", files, untrackedFiles };
|
|
694
|
+
return { status: "committed", files: committedFiles, untrackedFiles: untrackedOtherFiles };
|
|
649
695
|
}
|
|
650
696
|
|
|
651
697
|
// src/core/state.ts
|
|
@@ -8327,6 +8373,22 @@ async function agentSubmitExecute(params, ctx) {
|
|
|
8327
8373
|
}
|
|
8328
8374
|
const newChildren = (params.new_children ?? []).map((nc) => buildIssueChild(nc, ctx.agent));
|
|
8329
8375
|
if (stepPhase === "review") {
|
|
8376
|
+
if (isReviewerFamily(ctx.agent)) {
|
|
8377
|
+
const dirtyNonDoc = await listDirtyNonDocFiles(wtPath);
|
|
8378
|
+
if (dirtyNonDoc.length > 0) {
|
|
8379
|
+
const fileLines = dirtyNonDoc.map((f) => ` - ${f}`).join(`
|
|
8380
|
+
`);
|
|
8381
|
+
throw new Error(`提交被拒绝:工作区存在未提交的代码类改动。
|
|
8382
|
+
` + `可能是反向探针改坏的实现未还原,或违反了「只报不改」直接修改了代码。
|
|
8383
|
+
` + `涉及文件:
|
|
8384
|
+
${fileLines}
|
|
8385
|
+
` + `请先清理后再提交:
|
|
8386
|
+
` + ` - 已跟踪文件的改动:git checkout -- <文件> 还原
|
|
8387
|
+
` + ` - 新增的临时文件:直接删除
|
|
8388
|
+
` + `文档类改动(openspec/ 下文件与 .md 文件)不受此限,提交时会自动 commit;
|
|
8389
|
+
` + `确属必要的代码修正,请自行 commit 后再提交。`);
|
|
8390
|
+
}
|
|
8391
|
+
}
|
|
8330
8392
|
const stepCfg = workflow.stepMap.get(params.step_id).step;
|
|
8331
8393
|
const agentCfg = stepCfg.agents.find((a) => a.id === ctx.agent);
|
|
8332
8394
|
handleReviewParams(item, params, newChildren, params.step_id, {
|
|
@@ -8336,11 +8398,12 @@ async function agentSubmitExecute(params, ctx) {
|
|
|
8336
8398
|
} else if (stepPhase === "todo") {
|
|
8337
8399
|
handleAnalyzeParams(item, params);
|
|
8338
8400
|
} else if (stepPhase === "in_progress") {
|
|
8339
|
-
if (state.mode === "simple"
|
|
8401
|
+
if (params.step_id === "implement" && (state.mode === "simple" || params.verdict === "passed")) {
|
|
8340
8402
|
const clean = await isWorktreeClean(wtPath);
|
|
8341
8403
|
if (!clean) {
|
|
8342
|
-
throw new Error(`simple 模式 implement 提交被拒绝:工作区存在未提交内容。
|
|
8343
|
-
请先 git commit 提交全部变更后再提交 implement。`
|
|
8404
|
+
throw new Error(state.mode === "simple" ? `simple 模式 implement 提交被拒绝:工作区存在未提交内容。
|
|
8405
|
+
请先 git commit 提交全部变更后再提交 implement。` : `full 模式 implement 提交被拒绝:工作区存在未提交内容。
|
|
8406
|
+
请先 git commit 后再提交 implement。`);
|
|
8344
8407
|
}
|
|
8345
8408
|
}
|
|
8346
8409
|
handleImplementParams(item, params);
|
|
@@ -8370,10 +8433,10 @@ async function agentSubmitExecute(params, ctx) {
|
|
|
8370
8433
|
if (ac.status === "committed") {
|
|
8371
8434
|
autoCommitLines.push(`- ✅ worktree 直改修正已自动提交(${ac.files.length} 个文件,commit message: \`docs(opx): direct fixes by ${ctx.agent} (${params.step_id})\`)`);
|
|
8372
8435
|
} else if (ac.status === "failed") {
|
|
8373
|
-
autoCommitLines.push(`- ⚠️ worktree 直改修正自动提交失败:${ac.stderr || "未知错误"}
|
|
8436
|
+
autoCommitLines.push(`- ⚠️ worktree 直改修正自动提交失败:${ac.stderr || "未知错误"}。请编排者安排补提交(在 worktree 内手工 add 并 commit:已跟踪改动用 git add -u,未跟踪文档用 git add -- <文件>)`);
|
|
8374
8437
|
}
|
|
8375
8438
|
if (ac.untrackedFiles.length > 0) {
|
|
8376
|
-
autoCommitLines.push(`- ⚠️
|
|
8439
|
+
autoCommitLines.push(`- ⚠️ 存在未跟踪的非文档类文件未纳入自动提交(文档类已自动提交;这些须由编排者/开发者自行处理):${ac.untrackedFiles.join("、")}`);
|
|
8377
8440
|
}
|
|
8378
8441
|
}
|
|
8379
8442
|
if (stepPhase === "todo" && params.step_id === "analyze" && params.verdict === "passed" && wtPath && typeof item.metadata["branch_name"] === "string") {
|
package/.mcp-server/cli.mjs
CHANGED
|
@@ -21856,6 +21856,31 @@ async function isWorktreeClean(worktree) {
|
|
|
21856
21856
|
const out = await runGit(worktree, ["status", "--porcelain"]);
|
|
21857
21857
|
return out.length === 0;
|
|
21858
21858
|
}
|
|
21859
|
+
function isDocLikePath(p) {
|
|
21860
|
+
return p.startsWith("openspec/") || /\.md$/i.test(p);
|
|
21861
|
+
}
|
|
21862
|
+
async function listDirtyNonDocFiles(worktree) {
|
|
21863
|
+
const res = await runGitChecked(worktree, ["status", "--porcelain", "-uall"]);
|
|
21864
|
+
if (!res.success)
|
|
21865
|
+
return [];
|
|
21866
|
+
const seen = new Set;
|
|
21867
|
+
const files = [];
|
|
21868
|
+
for (const entry of parseStatusEntries(res.stdout)) {
|
|
21869
|
+
for (const p of entry.paths) {
|
|
21870
|
+
if (!p)
|
|
21871
|
+
continue;
|
|
21872
|
+
if (p.startsWith("openspec/states/"))
|
|
21873
|
+
continue;
|
|
21874
|
+
if (isDocLikePath(p))
|
|
21875
|
+
continue;
|
|
21876
|
+
if (seen.has(p))
|
|
21877
|
+
continue;
|
|
21878
|
+
seen.add(p);
|
|
21879
|
+
files.push(p);
|
|
21880
|
+
}
|
|
21881
|
+
}
|
|
21882
|
+
return files;
|
|
21883
|
+
}
|
|
21859
21884
|
async function markTaskGroupCheckboxesComplete(worktree, changeId, taskGroupId) {
|
|
21860
21885
|
const tasksMdPath = path.join(worktree, "openspec", "changes", changeId, "tasks.md");
|
|
21861
21886
|
let content;
|
|
@@ -22266,27 +22291,48 @@ async function reconcileMainPollution(mainRepo, worktreePath, opts) {
|
|
|
22266
22291
|
}
|
|
22267
22292
|
}
|
|
22268
22293
|
async function autoCommitWorktreeChanges(wtPath, agent, stepId) {
|
|
22269
|
-
const statusRes = await runGitChecked(wtPath, ["status", "--porcelain"]);
|
|
22294
|
+
const statusRes = await runGitChecked(wtPath, ["status", "--porcelain", "-uall"]);
|
|
22270
22295
|
if (!statusRes.success) {
|
|
22271
22296
|
return { status: "failed", files: [], untrackedFiles: [], stderr: statusRes.stderr };
|
|
22272
22297
|
}
|
|
22273
|
-
const
|
|
22274
|
-
const
|
|
22275
|
-
|
|
22276
|
-
|
|
22277
|
-
|
|
22278
|
-
if (
|
|
22298
|
+
const trackedFiles = [];
|
|
22299
|
+
const untrackedDocFiles = [];
|
|
22300
|
+
const untrackedOtherFiles = [];
|
|
22301
|
+
const seen = new Set;
|
|
22302
|
+
const pushUnique = (list, path2) => {
|
|
22303
|
+
if (seen.has(path2))
|
|
22304
|
+
return;
|
|
22305
|
+
seen.add(path2);
|
|
22306
|
+
list.push(path2);
|
|
22307
|
+
};
|
|
22308
|
+
for (const entry of parseStatusEntries(statusRes.stdout)) {
|
|
22309
|
+
const isUntracked = entry.x === "?" && entry.y === "?";
|
|
22310
|
+
if (isUntracked) {
|
|
22311
|
+
for (const path2 of entry.paths) {
|
|
22312
|
+
if (!path2 || path2.startsWith("openspec/states/"))
|
|
22313
|
+
continue;
|
|
22314
|
+
pushUnique(isDocLikePath(path2) ? untrackedDocFiles : untrackedOtherFiles, path2);
|
|
22315
|
+
}
|
|
22279
22316
|
continue;
|
|
22280
|
-
|
|
22281
|
-
|
|
22282
|
-
|
|
22283
|
-
|
|
22317
|
+
}
|
|
22318
|
+
const newPath = entry.paths[entry.paths.length - 1];
|
|
22319
|
+
if (!newPath || newPath.startsWith("openspec/states/"))
|
|
22320
|
+
continue;
|
|
22321
|
+
pushUnique(trackedFiles, newPath);
|
|
22284
22322
|
}
|
|
22285
|
-
if (
|
|
22286
|
-
return { status: "skipped", files: [], untrackedFiles };
|
|
22323
|
+
if (trackedFiles.length === 0 && untrackedDocFiles.length === 0) {
|
|
22324
|
+
return { status: "skipped", files: [], untrackedFiles: untrackedOtherFiles };
|
|
22325
|
+
}
|
|
22326
|
+
const committedFiles = [...trackedFiles, ...untrackedDocFiles];
|
|
22287
22327
|
const addResult = await runGitChecked(wtPath, ["add", "-u", "--", ".", ":(exclude)openspec/states"]);
|
|
22288
22328
|
if (!addResult.success) {
|
|
22289
|
-
return { status: "failed", files, untrackedFiles, stderr: addResult.stderr };
|
|
22329
|
+
return { status: "failed", files: committedFiles, untrackedFiles: untrackedOtherFiles, stderr: addResult.stderr };
|
|
22330
|
+
}
|
|
22331
|
+
if (untrackedDocFiles.length > 0) {
|
|
22332
|
+
const addDocResult = await runGitChecked(wtPath, ["add", "--", ...untrackedDocFiles]);
|
|
22333
|
+
if (!addDocResult.success) {
|
|
22334
|
+
return { status: "failed", files: committedFiles, untrackedFiles: untrackedOtherFiles, stderr: addDocResult.stderr };
|
|
22335
|
+
}
|
|
22290
22336
|
}
|
|
22291
22337
|
const commitResult = await runGitChecked(wtPath, [
|
|
22292
22338
|
"commit",
|
|
@@ -22294,9 +22340,9 @@ async function autoCommitWorktreeChanges(wtPath, agent, stepId) {
|
|
|
22294
22340
|
`docs(opx): direct fixes by ${agent} (${stepId})`
|
|
22295
22341
|
]);
|
|
22296
22342
|
if (!commitResult.success) {
|
|
22297
|
-
return { status: "failed", files, untrackedFiles, stderr: commitResult.stderr };
|
|
22343
|
+
return { status: "failed", files: committedFiles, untrackedFiles: untrackedOtherFiles, stderr: commitResult.stderr };
|
|
22298
22344
|
}
|
|
22299
|
-
return { status: "committed", files, untrackedFiles };
|
|
22345
|
+
return { status: "committed", files: committedFiles, untrackedFiles: untrackedOtherFiles };
|
|
22300
22346
|
}
|
|
22301
22347
|
|
|
22302
22348
|
// src/core/state.ts
|
|
@@ -29978,6 +30024,22 @@ async function agentSubmitExecute(params, ctx) {
|
|
|
29978
30024
|
}
|
|
29979
30025
|
const newChildren = (params.new_children ?? []).map((nc) => buildIssueChild(nc, ctx.agent));
|
|
29980
30026
|
if (stepPhase === "review") {
|
|
30027
|
+
if (isReviewerFamily(ctx.agent)) {
|
|
30028
|
+
const dirtyNonDoc = await listDirtyNonDocFiles(wtPath);
|
|
30029
|
+
if (dirtyNonDoc.length > 0) {
|
|
30030
|
+
const fileLines = dirtyNonDoc.map((f) => ` - ${f}`).join(`
|
|
30031
|
+
`);
|
|
30032
|
+
throw new Error(`提交被拒绝:工作区存在未提交的代码类改动。
|
|
30033
|
+
` + `可能是反向探针改坏的实现未还原,或违反了「只报不改」直接修改了代码。
|
|
30034
|
+
` + `涉及文件:
|
|
30035
|
+
${fileLines}
|
|
30036
|
+
` + `请先清理后再提交:
|
|
30037
|
+
` + ` - 已跟踪文件的改动:git checkout -- <文件> 还原
|
|
30038
|
+
` + ` - 新增的临时文件:直接删除
|
|
30039
|
+
` + `文档类改动(openspec/ 下文件与 .md 文件)不受此限,提交时会自动 commit;
|
|
30040
|
+
` + `确属必要的代码修正,请自行 commit 后再提交。`);
|
|
30041
|
+
}
|
|
30042
|
+
}
|
|
29981
30043
|
const stepCfg = workflow.stepMap.get(params.step_id).step;
|
|
29982
30044
|
const agentCfg = stepCfg.agents.find((a) => a.id === ctx.agent);
|
|
29983
30045
|
handleReviewParams(item, params, newChildren, params.step_id, {
|
|
@@ -29987,11 +30049,12 @@ async function agentSubmitExecute(params, ctx) {
|
|
|
29987
30049
|
} else if (stepPhase === "todo") {
|
|
29988
30050
|
handleAnalyzeParams(item, params);
|
|
29989
30051
|
} else if (stepPhase === "in_progress") {
|
|
29990
|
-
if (state.mode === "simple"
|
|
30052
|
+
if (params.step_id === "implement" && (state.mode === "simple" || params.verdict === "passed")) {
|
|
29991
30053
|
const clean = await isWorktreeClean(wtPath);
|
|
29992
30054
|
if (!clean) {
|
|
29993
|
-
throw new Error(`simple 模式 implement 提交被拒绝:工作区存在未提交内容。
|
|
29994
|
-
请先 git commit 提交全部变更后再提交 implement。`
|
|
30055
|
+
throw new Error(state.mode === "simple" ? `simple 模式 implement 提交被拒绝:工作区存在未提交内容。
|
|
30056
|
+
请先 git commit 提交全部变更后再提交 implement。` : `full 模式 implement 提交被拒绝:工作区存在未提交内容。
|
|
30057
|
+
请先 git commit 后再提交 implement。`);
|
|
29995
30058
|
}
|
|
29996
30059
|
}
|
|
29997
30060
|
handleImplementParams(item, params);
|
|
@@ -30021,10 +30084,10 @@ async function agentSubmitExecute(params, ctx) {
|
|
|
30021
30084
|
if (ac.status === "committed") {
|
|
30022
30085
|
autoCommitLines.push(`- ✅ worktree 直改修正已自动提交(${ac.files.length} 个文件,commit message: \`docs(opx): direct fixes by ${ctx.agent} (${params.step_id})\`)`);
|
|
30023
30086
|
} else if (ac.status === "failed") {
|
|
30024
|
-
autoCommitLines.push(`- ⚠️ worktree 直改修正自动提交失败:${ac.stderr || "未知错误"}
|
|
30087
|
+
autoCommitLines.push(`- ⚠️ worktree 直改修正自动提交失败:${ac.stderr || "未知错误"}。请编排者安排补提交(在 worktree 内手工 add 并 commit:已跟踪改动用 git add -u,未跟踪文档用 git add -- <文件>)`);
|
|
30025
30088
|
}
|
|
30026
30089
|
if (ac.untrackedFiles.length > 0) {
|
|
30027
|
-
autoCommitLines.push(`- ⚠️
|
|
30090
|
+
autoCommitLines.push(`- ⚠️ 存在未跟踪的非文档类文件未纳入自动提交(文档类已自动提交;这些须由编排者/开发者自行处理):${ac.untrackedFiles.join("、")}`);
|
|
30028
30091
|
}
|
|
30029
30092
|
}
|
|
30030
30093
|
if (stepPhase === "todo" && params.step_id === "analyze" && params.verdict === "passed" && wtPath && typeof item.metadata["branch_name"] === "string") {
|
|
@@ -30629,7 +30692,7 @@ async function ensureDefaultUnattended(args, ctx) {
|
|
|
30629
30692
|
}
|
|
30630
30693
|
} catch {}
|
|
30631
30694
|
}
|
|
30632
|
-
var PKG_VERSION = "0.
|
|
30695
|
+
var PKG_VERSION = "0.151.1";
|
|
30633
30696
|
function buildMcpServer(worktree, opts = {}) {
|
|
30634
30697
|
const mcp = new McpServer({ name: "openspec-agents", version: PKG_VERSION });
|
|
30635
30698
|
for (const [name, spec] of Object.entries(TOOL_SPECS)) {
|
|
@@ -34,6 +34,7 @@ permission:
|
|
|
34
34
|
适用于全部审查身份(含架构师身份与 full 模式的维度审查):
|
|
35
35
|
|
|
36
36
|
- 影响代码运行的逻辑类问题:只上报 issue,不直接修改相关代码
|
|
37
|
+
- 反向探针例外:仅任务验证与合并审查身份可用——核验测试对抗力时可临时改坏被测实现,动作固定为四步:改坏被测实现 → 确认对应用例变红 → 原样还原实现 → 全量用例复跑全绿;探针结束后必须原样还原并确认 git 工作区无残留改动(git status 无改动),探针内容禁止提交,不得保留任何痕迹;残留的代码类未提交改动会被工具层拒绝提交,文档类修正(含新建的 openspec/ 与 .md 文件)照常自动提交;源码文件内的注释修正请自行 commit 后再提交,仅 openspec/ 与 .md 文件由工具自动提交
|
|
37
38
|
- 文档、注释等不影响代码运行的内容:发现不一致/过时问题时直接修正,不为此报 issue;修正情况在提交说明/validation_steps 留痕
|
|
38
39
|
- 工具改进建议仅上报(含规则草案),实施由开发者完成
|
|
39
40
|
|
|
@@ -64,8 +65,8 @@ Info 级别 issue 的 description/suggestion 中禁止出现阶段/时机相关
|
|
|
64
65
|
| 级别 | task 产出验证 | 服务启动验证 | 测试审查 |
|
|
65
66
|
|------|-------------|-------------|---------|
|
|
66
67
|
| Critical | 核心 task 产出文件缺失 | 服务无法启动(无论是否本次变更引起) | 测试依赖无隔离环境导致 CI 必然失败 |
|
|
67
|
-
| High | 关键 task 产出不完整;行为无实现(仅状态标记未实际生效);流程执行正常但关键数据产出缺失/未落库/落错位置(区别于『仅打状态标记未实际生效』:本行=流程与响应正常,产出物缺失或落错位) | 健康检查不通过;关键外部依赖不可用;新增接口无法调通(不论原因) | 核心业务逻辑无测试覆盖;Mock 了被测对象本身;测试仅断言可见状态/响应而 spec 要求的一并操作行为(入队/落库/下游消费等)无断言,将缺陷固化为期望行为;测试仅验证流程跑通/响应成功,未对 spec
|
|
68
|
-
| Medium | task 产出存在但质量不达标;关键数据产出部分缺失、值不完整或与 spec 指定承载位置不一致 | 启动有 WARN 但服务可用 |
|
|
68
|
+
| High | 关键 task 产出不完整;行为无实现(仅状态标记未实际生效);流程执行正常但关键数据产出缺失/未落库/落错位置(区别于『仅打状态标记未实际生效』:本行=流程与响应正常,产出物缺失或落错位) | 健康检查不通过;关键外部依赖不可用;新增接口无法调通(不论原因) | 核心业务逻辑无测试覆盖;Mock 了被测对象本身;测试仅断言可见状态/响应而 spec 要求的一并操作行为(入队/落库/下游消费等)无断言,将缺陷固化为期望行为;测试仅验证流程跑通/响应成功,未对 spec 要求的关键数据产出(落位/完整性)断言;变更涉及接口契约或数据口径,却没有对应接口测试脚本或未执行(不适用场景须有结构化降级理由);高风险场景(并发、幂等、状态机非法迁移、第三方依赖异常与回调超时)无对应用例且无结构化未覆盖理由 |
|
|
69
|
+
| Medium | task 产出存在但质量不达标;关键数据产出部分缺失、值不完整或与 spec 指定承载位置不一致 | 启动有 WARN 但服务可用 | 断言过于宽松;缺少边界值测试或测试设计物缺失(行为面清单/用例矩阵/反向探针记录);关键数据产出部分缺失、值不完整或与 spec 指定承载位置不一致 |
|
|
69
70
|
| Low | 产出文件位置不符合约定 | 启动日志格式不统一 | 测试命名不规范但不影响运行 |
|
|
70
71
|
| Info | 建议补充额外产出物 | 建议增加启动度量指标 | 建议补充边缘用例 |
|
|
71
72
|
|
|
@@ -197,7 +198,7 @@ Info 级别 issue 的 description/suggestion 中禁止出现阶段/时机相关
|
|
|
197
198
|
## 职责边界(确定性检查与任务验证)
|
|
198
199
|
|
|
199
200
|
- **确定性工具检查**:工具维度审查者与 simple 合并审查者被允许运行确定性工具检查(代码格式/架构约束/静态分析/单元测试编译/深度扫描),按已加载质量门 skill 的必做清单执行并将输出映射为统一 issue 结构;检查结果纳入审查结论
|
|
200
|
-
- **任务验证**:任务维度审查者与 simple 合并审查者承担任务验证职责,验证 task 产出完整性、服务健康与 API 测试质量;未通过的 task 经 failed_tasks 上报;验证完成后按清理规范做幂等清理,不留环境残留;数据产出核验——对照 spec
|
|
201
|
+
- **任务验证**:任务维度审查者与 simple 合并审查者承担任务验证职责,验证 task 产出完整性、服务健康与 API 测试质量;未通过的 task 经 failed_tasks 上报;验证完成后按清理规范做幂等清理,不留环境残留;数据产出核验——对照 spec 数据产出要求核验关键数据产出落位/完整性/搬迁承接/基线对比,识别『流程正常但数据缺失/落错位置』类缺陷;测试设计核验——独立产出行为面清单与用例矩阵(从 spec 与既有实现独立推导,不以开发者申报为输入),并自行执行反向探针,识别漏面、漏格、断言放水与探针缺失
|
|
201
202
|
- **维度审查锚点**:以 spec 验收标准、行为组与数据产出要求为主锚做对照审查,skill 检查点清单与 dev 自检申报对照兜底(抽查申报遗漏项)
|
|
202
203
|
- **工具改进**:可工具化的质量 pattern 双报业务 issue 与工具改进 issue(规则草案由审查者提供),实施工作由开发者完成,审查者不直接改动规则/配置文件以外的实现
|
|
203
204
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
name: api-test
|
|
3
3
|
description: Web 应用 API 自动化测试规范——.http 脚本格式、目录结构、SQL 前置数据脚本、认证发现、覆盖标准与执行方式。
|
|
4
4
|
capabilities: ["api-testing"]
|
|
5
|
+
must_do: ["api_script", "api_execution"]
|
|
5
6
|
boundary_hints:
|
|
6
7
|
directories: ["api-tests/"]
|
|
7
8
|
---
|
|
@@ -84,6 +85,16 @@ SQL 脚本按场景准备无法通过接口构造的数据库数据(历史数
|
|
|
84
85
|
|
|
85
86
|
按项目实际 DB 类型选择写法。
|
|
86
87
|
|
|
88
|
+
## 后置校验
|
|
89
|
+
|
|
90
|
+
目的:把断言从响应层升级到数据承载层,在接口调用后校验数据库状态、缓存命中与键值、消息入队与下游消费;仅凭响应状态码与响应体不构成验收。
|
|
91
|
+
|
|
92
|
+
- 只读通道:使用数据库只读查询与缓存只读查询作为后置校验手段,禁止任何写操作(含为校验而临时插数、改状态、删键)
|
|
93
|
+
- 隔离实例:必须查本次变更按「执行顺序」隔离启动的实例,禁止查询宿主既有的数据库与缓存;隔离标识与建议端口同来自编排会话上下文
|
|
94
|
+
- 异步或延迟产出:等待产出完成后断言,须写明超时口径(最长等待时长与轮询间隔),超时仍未产出即判失败,不得以「稍后再查」放行
|
|
95
|
+
- 断言口径:数据库断言落到具体字段值与记录数;缓存断言区分命中/未命中并核对键与值;消息断言核对入队与下游消费结果
|
|
96
|
+
- 客户端命令按项目技术栈确定,本节只定方法与断言口径,不在本文件写死具体客户端与命令
|
|
97
|
+
|
|
87
98
|
## 认证发现
|
|
88
99
|
|
|
89
100
|
API 测试脚本运行前需获取有效认证凭证。常见模式:
|
|
@@ -123,7 +134,7 @@ API 测试脚本必须覆盖所有新增/变更接口:
|
|
|
123
134
|
| 正常路径 | 按 spec 请求结构传入合法值,验证响应状态码(2xx) 和响应结构 |
|
|
124
135
|
| 关键边界 | 缺必填字段 → 4xx;非法值(空串/超长/类型错误) → 4xx;极值 → 正确响应或合理错误 |
|
|
125
136
|
| 业务闭环 | spec 含列表/详情/状态流转/删除接口时,测试须端到端闭环:创建后用返回标识在列表/详情检索成功、状态变更后可见性反映、删除/停用后不可见 |
|
|
126
|
-
| 关键数据产出 | 涉及数据产出/落位/搬迁的变更,测试须断言关键产出字段已持久化到 spec
|
|
137
|
+
| 关键数据产出 | 涉及数据产出/落位/搬迁的变更,测试须断言关键产出字段已持久化到 spec 指定承载位置且值完整非空;产出承载位置包含数据库与缓存/中间件状态;产出为异步/延迟时须等待产出完成后断言;仅凭响应 2xx 与流程跑通不构成验收 |
|
|
127
138
|
|
|
128
139
|
spec 描述为多个行为一并发生的(如状态变更与数据入队),每个行为都须有断言证据;断言范围不限于响应状态码与响应结构,须覆盖变更引发的真实行为(状态落库、消息入队、下游可消费等连带效果)。
|
|
129
140
|
|
|
@@ -151,6 +162,8 @@ http 请求体与 SQL 前置数据均按 spec 业务语义构造:使用真实
|
|
|
151
162
|
- 仅测试代码变更(无生产代码、接口契约、构建与测试配置改动)
|
|
152
163
|
- 纯文档变更
|
|
153
164
|
|
|
165
|
+
按本章判定本次变更未涉及接口契约或数据口径时,必做项 `api_script` 与 `api_execution` 以 completed=false + 结构化 skip_reason 申报:category 填 `not_applicable`,note 写判定依据(如「本次变更未涉及接口契约或数据口径」);判定适用时两项必须实跑后申报,不得以不适用之名跳过。
|
|
166
|
+
|
|
154
167
|
## 执行约束
|
|
155
168
|
|
|
156
169
|
MUST 遵守以下约束,不得以编译通过或单元测试通过替代 API 测试:
|
|
@@ -160,12 +173,16 @@ MUST 遵守以下约束,不得以编译通过或单元测试通过替代 API
|
|
|
160
173
|
|
|
161
174
|
### 脚本准备
|
|
162
175
|
|
|
176
|
+
本节对应必做项 `api_script`。
|
|
177
|
+
|
|
163
178
|
MUST 确保 api-tests/script/ 目录下存在与所有变更接口对应的 .http 测试脚本:
|
|
164
179
|
- 已有对应脚本 → 更新为匹配变更后契约
|
|
165
180
|
- 尚无对应脚本 → 创建新脚本(不得因尚无已有脚本而跳过创建),同步准备 api-tests/data/ 下 SQL 前置数据脚本
|
|
166
181
|
|
|
167
182
|
### 脚本执行
|
|
168
183
|
|
|
184
|
+
本节对应必做项 `api_execution`。
|
|
185
|
+
|
|
169
186
|
API 验证须通过 .http 兼容执行工具运行脚本——不得使用 curl、wget 等通用 HTTP 命令行工具替代。
|
|
170
187
|
|
|
171
188
|
#### 执行工具
|
|
@@ -12,6 +12,7 @@ must_do: ["behavior_surface", "case_matrix", "negative_probe"]
|
|
|
12
12
|
|
|
13
13
|
从 spec 与接口/数据契约推导本次变更牵动的行为面,作为覆盖的完整边界;禁止把验收标准的条目直接当成行为面清单:
|
|
14
14
|
|
|
15
|
+
- 业务规则:除 spec 与契约外,须从存量系统挖掘被本次变更复用或牵动的业务规则——既有实现的分支与校验、数据库约束与索引、配置常量与开关、领域模型不变量、历史缺陷记录;每条业务规则标注来源(spec 条目/代码位置/数据库约束/配置项)。本变更新增或修改的规则以 proposal 的规则边界结论与 spec 的三类场景为输入
|
|
15
16
|
- 状态与流程:新增/变更的状态、状态流转路径、初始态与终止态
|
|
16
17
|
- 数据承载:新增/变更字段、字段的产出与读写位置、可空性、默认值
|
|
17
18
|
- 枚举与取值域:有限取值集合、类别标记、字典项
|
|
@@ -32,14 +33,14 @@ must_do: ["behavior_surface", "case_matrix", "negative_probe"]
|
|
|
32
33
|
| 边界 | 空值/空集合/零、极值、超长、边界长度、类型非法、精度与舍入、日期与时间边界 |
|
|
33
34
|
| 异常 | 非法状态跳转、重复提交与幂等、并发、事务回滚、部分失败、依赖不可用与超时、越权访问 |
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
组合与状态迁移:按「规则 × 状态 × 外部事件」三轴收敛组合——多个可选字段或条件并存时按判定表收敛,并交叉展开规则命中、对象当前状态与外部事件(依赖回调、延迟任务、并发请求等)的组合格;交叉格与状态机的非法迁移成对覆盖,非法迁移须断言被拒绝。
|
|
36
37
|
|
|
37
38
|
## 断言强度
|
|
38
39
|
|
|
39
40
|
- 每条行为至少一条「实现写错就红」的断言:断具体值、状态流转结果、副作用是否发生,而不是「没报错」
|
|
40
41
|
- 仅断言响应成功、字段非空、流程跑通,不构成断言
|
|
41
42
|
- 同一请求内一并发生的多个行为逐个断言,禁止以其中一个行为的断言替代其余
|
|
42
|
-
-
|
|
43
|
+
- 断言范围包括连带效果:状态落库、消息入队、下游可消费、产出字段落位与完整性、缓存命中与失效、缓存与持久化数据的一致性
|
|
43
44
|
|
|
44
45
|
## 用例与实现解耦
|
|
45
46
|
|
|
@@ -53,16 +54,17 @@ must_do: ["behavior_surface", "case_matrix", "negative_probe"]
|
|
|
53
54
|
|
|
54
55
|
1. 把实现改坏(拔掉被测行为、翻转判定条件、让产出落错位置)
|
|
55
56
|
2. 确认该用例失败
|
|
56
|
-
3.
|
|
57
|
+
3. 原样还原实现
|
|
58
|
+
4. 全量用例复跑全绿,并确认工作区无残留改动(git status 干净)
|
|
57
59
|
|
|
58
|
-
|
|
60
|
+
探针内容禁止提交,不得保留任何痕迹。记录探针的改坏点、变红的用例、还原确认结果。反向探针未通过的用例视为无对抗力,须重写。
|
|
59
61
|
|
|
60
62
|
## 证据形态
|
|
61
63
|
|
|
62
64
|
| 检查项 | 交付物 |
|
|
63
65
|
|--------|--------|
|
|
64
|
-
| behavior_surface |
|
|
65
|
-
| case_matrix | 用例矩阵:行为面 × 类别 →
|
|
66
|
+
| behavior_surface | 行为面清单(按上节维度列出,标注对应验收标准,且标注每条规则的来源) |
|
|
67
|
+
| case_matrix | 用例矩阵:行为面 × 类别 → 用例名/脚本名,每格注明断什么;含规则×状态×外部事件交叉格 |
|
|
66
68
|
| negative_probe | 探针记录:改坏点、变红用例、还原确认结果 |
|
|
67
69
|
|
|
68
70
|
## 适用范围
|
|
@@ -43,7 +43,6 @@ phases:
|
|
|
43
43
|
- "执行边界缺省整个 worktree(独立审查无分析环节,不生成边界白名单);issue 的 file 字段指向的文件即修复边界,修复这些文件(含回归引入问题)不算越界;可按 issue 的 suggestion 直接执行修复;reviewer 的 boundary_expansion 已并入边界"
|
|
44
44
|
- "对修复未过次数≥2 的 issue 须先完成 5-Why 根因分析再动手,不得跳过分析直接改代码"
|
|
45
45
|
- "修复完成后先 git commit 再提交;提交前按已加载的质量门类 skill 完成本地构建验证,并按维度规范类 skill(style/architecture/performance/security/maintainability 能力类别)对本次修复文件逐项自检,自检结论与工具检查结果一并汇总进 self_check_results"
|
|
46
|
-
- "有代码、接口契约或数据口径变更时,须按已加载的测试设计规范产出行为面清单、用例矩阵与反向探针记录,随提交在 self_check_results 中逐项申报;反向探针结束后必须先还原实现并复跑全量用例确认全绿再提交,禁止带着改坏的实现提交"
|
|
47
46
|
- "自证/验证需启动服务或基础设施时,一律按已加载技术栈 skill 的隔离启动规范以隔离方式启动,隔离标识与建议端口以视图 Worktree 区块为准;验证完成后停止本次启动的实例,不留残留,残留判定遵循已加载清理规范的归属判别,无法确认归属本变更的进程一律不清;禁止为腾出端口或资源而停止、复用或干扰非本会话启动的进程"
|
|
48
47
|
- "确定性质量工具检查出的问题不得以压制手段(压制注解/注释抑制/规则排除配置/平台状态标注/构建跳过开关等)静默规避:确需压制时,必须在压制处代码注释说明清楚理由,并经 exempt_issue_ids 申请豁免由报源 reviewer 裁定;不得以 fixed_issue_ids 冒充修复"
|
|
49
48
|
- "提交 opx_agent_submit:必传 verdict;修复完成的 issue 经 fixed_issue_ids 上报、不可修 issue 经 exempt_issue_ids 申请豁免"
|
|
@@ -66,12 +65,13 @@ phases:
|
|
|
66
65
|
- "审查锚点按视图「审查范围」:PR 区间形态为 base..head 区间(首轮)或检查点增量(修复轮);全量形态为整个代码库——全量形态下的工具违规无论是否本轮引入一律映射为 issue,不得以不在变更清单为由丢弃"
|
|
67
66
|
- "「有变更」分支先审查上方「本次变更证据」区块的变更文件清单与区间 diff 内容后自行判断:若确认本次变更仅为注释/文档性且无逻辑影响,可自行决定跳过全量工具检查直接提交 passed(validation_steps 以 step=no_change 申报整体豁免必做清单);无法确认或含逻辑/结构变更,必须顺序运行全部确定性工具检查;裁量仅免除全量工具扫描,不免除复核/裁定——本层 blocking issue 仍须处理至终态后提交"
|
|
68
67
|
- "回归验证(独立审查场景,无 spec 验收标准——以既有测试与行为基线为锚):①构建通过;②既有测试全量执行不引入新失败——首轮跑出的存量失败按 issue 上报进入处置通道(审并修模式进修复闭环或豁免,只审模式进报告),修复轮出现「上轮未登记的新失败」即回归、verdict=failed 并报 issue(说明失败用例与归因);③可启动服务时按已加载技术栈 skill 的隔离启动规范隔离启动做冒烟与既有 API 测试脚本执行(存在时),验证后按已加载的清理规范幂等清理(含 verdict=failed 轮次);无法启动服务或无脚本时 ③ 置 completed=false 并以结构化 skip_reason 申报"
|
|
69
|
-
- "
|
|
68
|
+
- "测试设计核验(独立审查无 spec:行为面从变更 diff 与既有实现独立推导):独立产出行为面清单与用例矩阵,核对既有与新增测试对行为面的覆盖、断言强度与实施正确性(检查点按已加载测试设计规范与任务维度判例执行);自行对关键用例执行反向探针——改坏被测实现 → 确认对应用例变红 → 原样还原实现 → 全量用例复跑全绿,把改坏点与还原确认结果编入 validation_steps 的 negative_probe 申报;用例未变红视为该用例无对抗力,须报 issue;无测试对象(无代码、接口契约与数据口径变更)时以 completed=false + 结构化 skip_reason 申报;不得跳过核验直接提交通过"
|
|
69
|
+
- "本 step 能力集含接口测试能力,须按必做项口径申报 api_script 与 api_execution:api_script 对应「变更接口有对应的接口测试脚本」,api_execution 对应「按已加载接口测试规范执行脚本实跑通过」;变更不涉及接口契约或数据口径时,两项均置 completed=false,category 填 not_applicable、adjudication 按实际裁定方式填,note 写判定依据;变更涉及接口契约或数据口径但隔离环境不可用(服务无法启动、隔离实例不可用)时,仅 api_execution 置 completed=false,category 与 adjudication 均填 env_unavailable,note 记录尝试过程,api_script 仍须实查脚本存在性,脚本缺失按「契约变更无对应脚本」判例报 issue,不得以环境不可用为由跳过;两类语义分开,不得混用"
|
|
70
70
|
- "加载匹配维度 skill 做五维度合并审查(style/architecture/performance/security/maintainability),结合领域知识做拓展审查(skill 仅为最低基准);顺带发现的其他缺陷按对应维度严重级别标准报 issue,禁止静默丢弃"
|
|
71
71
|
- "审查新 issue 前先查看视图「待复核」区块中的既有 issue(仅本审查者报源),避免语义重复"
|
|
72
72
|
- "报 issue 时必须显式声明归因维度 dimension(style/architecture/performance/security/maintainability):本 reviewer 不归属任一质量维度,dimension 即该 issue 归属的目标质量维度"
|
|
73
73
|
- "报 issue 前先判断该问题能否通过调整确定性质量扫描工具配置(规则收紧/新增规则/配置增强)统一收敛:可工具化 → 双报业务 issue(file=违规现场)与工具改进 issue(file=规则/配置文件,suggestion 含规则草案+验证命令,末尾标 [tool_eligible],且与业务 issue 同维度),两条 file 不同、方案不同,不构成语义重复,必须双报;不可工具化 → 仅报业务 issue。工具化判断细节与规则草案模板遵循已加载的工具改进类 skill,未加载时仅报业务 issue、跳过工具改进环节;提交时在 validation_steps 中记录工具化判断结论"
|
|
74
|
-
- "
|
|
74
|
+
- "只报不改与文档直改:影响代码运行的逻辑类问题只上报、不直接修改(反向探针核验除外,见约束);文档/注释等不影响代码运行的内容发现不一致/过时问题时直接修正,不为此报 issue,修正情况在提交说明/validation_steps 留痕;工具改进建议仅上报(含规则草案),实施由开发者完成"
|
|
75
75
|
- "复核自己报的已修复待复核(review 态)issue:通过置 done,不通过驳回(附驳回原因,issue 回待修复并累计修复未过次数)"
|
|
76
76
|
- "对视图「待裁定是否可豁免」区块中的豁免申请经 exempt_adjudications 裁定(dismissed/rejected)"
|
|
77
77
|
- "审查区间 diff 与 git log,识别变更中新增的压制手段(压制注解/注释抑制/规则排除配置/平台状态标注/构建跳过开关等):视为豁免申请,核对是否已在压制处代码注释或 commit message 说明理由并申报豁免;未申报或理由不成立 → 报 issue(file=压制位置、line=压制行、rule=被压制规则名,severity 至少 Low);理由成立 → 经 exempt_adjudications 裁定 dismissed"
|
|
@@ -82,6 +82,8 @@ phases:
|
|
|
82
82
|
constraints:
|
|
83
83
|
- "工具调用边界:仅可调用 opx_status、opx_agent_submit、question(需要用户协助处理时);可 bash 运行确定性工具检查、启动服务并执行验证命令"
|
|
84
84
|
- "只报不改与文档直改:禁止 edit/write 影响代码运行的内容(逻辑类问题仅上报);文档/注释等不影响代码运行的内容发现不一致/过时问题时直接修正,不为此报 issue"
|
|
85
|
+
- "反向探针例外:执行反向探针核验时可临时改坏被测实现(仅限探针所需的最小改动),探针结束后必须原样还原并确认 git 工作区无残留改动(git status 无改动)、全量用例复跑全绿;探针内容禁止提交,不得保留任何痕迹;用例未变红视为该用例无对抗力,须报 issue"
|
|
86
|
+
- "提交前工作区不得残留代码类未提交改动(反向探针改动必须还原、新增临时文件必须删除);文档类改动(openspec/ 下文件与 .md 文件,含新建的未跟踪文件)会在提交时自动 commit,不受此限。工具层会拒绝带代码类未提交改动的提交"
|
|
85
87
|
- "即使无 issue 也必须提交 verdict=passed(不通过必须有至少一个 Low+ issue 作为理由)"
|
|
86
88
|
- "Info 级 issue 的 description/suggestion 禁止阶段/时机表述(如'可后续处理')"
|
|
87
89
|
- "压制类 issue 严重级别至少 Low"
|
|
@@ -41,8 +41,7 @@ phases:
|
|
|
41
41
|
- "以视图 Issue 清单驱动修复:Low+ 必办(修复后经 fixed_issue_ids 上报进入待复核),Info 建议修复不阻塞提交;不可修 issue 经 exempt_issue_ids 申请豁免(申请后进入待裁定状态,由报源 reviewer 裁定,被驳回的 issue 回待修复);全部 blocking issue 均已上报修复或申请豁免后才可 passed 提交推进"
|
|
42
42
|
- "执行边界缺省整个 worktree(独立审查无分析环节,不生成边界白名单);issue 的 file 字段指向的文件即修复边界,修复这些文件(含回归引入问题)不算越界;可按 issue 的 suggestion 直接执行修复;reviewer 的 boundary_expansion 已并入边界"
|
|
43
43
|
- "对修复未过次数≥2 的 issue 须先完成 5-Why 根因分析再动手,不得跳过分析直接改代码"
|
|
44
|
-
- "修复完成后先 git commit
|
|
45
|
-
- "有代码、接口契约或数据口径变更时,须按已加载的测试设计规范产出行为面清单、用例矩阵与反向探针记录,随提交在 self_check_results 中逐项申报;反向探针结束后必须先还原实现并复跑全量用例确认全绿再提交,禁止带着改坏的实现提交"
|
|
44
|
+
- "修复完成后先 git commit 再提交(未提交内容会被工具拒绝);提交前按已加载的质量门类 skill 完成本地构建验证,并按维度规范类 skill(style/architecture/performance/security/maintainability 能力类别)对本次修复文件逐项自检,自检结论与工具检查结果一并汇总进 self_check_results"
|
|
46
45
|
- "自证/验证需启动服务或基础设施时,一律按已加载技术栈 skill 的隔离启动规范以隔离方式启动,隔离标识与建议端口以视图 Worktree 区块为准;验证完成后停止本次启动的实例,不留残留,残留判定遵循已加载清理规范的归属判别,无法确认归属本变更的进程一律不清;禁止为腾出端口或资源而停止、复用或干扰非本会话启动的进程"
|
|
47
46
|
- "确定性质量工具检查出的问题不得以压制手段(压制注解/注释抑制/规则排除配置/平台状态标注/构建跳过开关等)静默规避:确需压制时,必须在压制处代码注释说明清楚理由,并经 exempt_issue_ids 申请豁免由报源 reviewer 裁定;不得以 fixed_issue_ids 冒充修复"
|
|
48
47
|
- "提交 opx_agent_submit:必传 verdict;修复完成的 issue 经 fixed_issue_ids 上报、不可修 issue 经 exempt_issue_ids 申请豁免"
|
|
@@ -56,7 +55,7 @@ phases:
|
|
|
56
55
|
- id: verify_tool
|
|
57
56
|
agents:
|
|
58
57
|
- id: openspec-reviewer-tool
|
|
59
|
-
capability_tags: [quality-gate, efficiency
|
|
58
|
+
capability_tags: [quality-gate, efficiency]
|
|
60
59
|
transitions:
|
|
61
60
|
on_pass: verify_task
|
|
62
61
|
on_fail: implement
|
|
@@ -74,6 +73,7 @@ phases:
|
|
|
74
73
|
- "报静态分析类 issue 时,description 必须含:①复现命令(非静默形态,含必要开关);②实测数据(命中类清单/计数);③规则默认阈值与当前项目配置对比(引用可改的配置文件路径)"
|
|
75
74
|
constraints:
|
|
76
75
|
- "工具调用边界:仅可调用 opx_status、opx_agent_submit、question(需要用户协助处理时);可 bash 运行工具;禁止 edit/write 影响代码运行的内容;文档/注释等不影响代码运行的内容发现不一致/过时问题时直接修正,不为此报 issue"
|
|
76
|
+
- "提交前工作区不得残留代码类未提交改动(反向探针改动必须还原、新增临时文件必须删除);文档类改动(openspec/ 下文件与 .md 文件,含新建的未跟踪文件)会在提交时自动 commit,不受此限。工具层会拒绝带代码类未提交改动的提交"
|
|
77
77
|
- "即使无 issue 也必须提交 verdict=passed(不通过必须有至少一个 Low+ issue 作为理由)"
|
|
78
78
|
- "Info 级 issue 的 description/suggestion 禁止阶段/时机表述(如'可后续处理')"
|
|
79
79
|
- "压制类 issue 严重级别至少 Low"
|
|
@@ -86,7 +86,8 @@ phases:
|
|
|
86
86
|
on_fail: implement
|
|
87
87
|
instructions:
|
|
88
88
|
- "回归验证层(独立审查场景,无 spec 验收标准——以既有测试与行为基线为锚)逐项验证:①构建通过;②既有测试全量执行不引入新失败——首轮跑出的存量失败按 issue 上报进入处置通道(审并修模式进修复闭环或豁免,只审模式进报告),修复轮出现「上轮未登记的新失败」即回归、verdict=failed 并报 issue(说明失败用例与归因);③可启动服务时按已加载技术栈 skill 的隔离启动规范隔离启动做冒烟与既有 API 测试脚本执行(存在时),验证后幂等清理"
|
|
89
|
-
- "
|
|
89
|
+
- "测试设计核验(独立审查无 spec:行为面从变更 diff 与既有实现独立推导):独立产出行为面清单与用例矩阵,核对既有与新增测试对行为面的覆盖、断言强度与实施正确性(检查点按已加载测试设计规范与任务维度判例执行);自行对关键用例执行反向探针——改坏被测实现 → 确认对应用例变红 → 原样还原实现 → 全量用例复跑全绿,把改坏点与还原确认结果编入 validation_steps 的 negative_probe 申报;用例未变红视为该用例无对抗力,须报 issue;无测试对象(无代码、接口契约与数据口径变更)时以 completed=false + 结构化 skip_reason 申报;不得跳过核验直接提交通过"
|
|
90
|
+
- "本 step 能力集含接口测试能力,须按必做项口径申报 api_script 与 api_execution:api_script 对应「变更接口有对应的接口测试脚本」,api_execution 对应「按已加载接口测试规范执行脚本实跑通过」;变更不涉及接口契约或数据口径时,两项均置 completed=false,category 填 not_applicable、adjudication 按实际裁定方式填,note 写判定依据;变更涉及接口契约或数据口径但隔离环境不可用(服务无法启动、隔离实例不可用)时,仅 api_execution 置 completed=false,category 与 adjudication 均填 env_unavailable,note 记录尝试过程,api_script 仍须实查脚本存在性,脚本缺失按「契约变更无对应脚本」判例报 issue,不得以环境不可用为由跳过;两类语义分开,不得混用"
|
|
90
91
|
- "验证完成后(含 verdict=failed 轮次)按已加载的清理规范执行幂等清理:停止残留的服务实例、清理本会话的隔离环境、删除残留的隔离扫描项目;无残留即通过,清理结果纳入 validation_steps 申报"
|
|
91
92
|
- "无法启动服务或不存在 API 测试脚本时,③置 completed=false 并以结构化 skip_reason 申报(格式:{\"item\":\"<对应验证项>\",\"category\":\"<降级类别>\",\"adjudication\":\"user_response|unattended_auto|env_unavailable\",\"note\":\"<说明>\"}),注明判定依据"
|
|
92
93
|
- "报 issue 时必须显式声明归因维度 dimension(style/architecture/performance/security/maintainability):本 reviewer 不归属任一质量维度,dimension 即该 issue 归属的目标质量维度"
|
|
@@ -96,6 +97,8 @@ phases:
|
|
|
96
97
|
- "提交 opx_agent_submit,必带 verdict"
|
|
97
98
|
constraints:
|
|
98
99
|
- "工具调用边界:仅可调用 opx_status、opx_agent_submit;可 bash 启动服务/执行检查;禁止 edit/write 影响代码运行的内容;文档/注释等不影响代码运行的内容发现不一致/过时问题时直接修正,不为此报 issue"
|
|
100
|
+
- "反向探针例外:执行反向探针核验时可临时改坏被测实现(仅限探针所需的最小改动),探针结束后必须原样还原并确认 git 工作区无残留改动(git status 无改动)、全量用例复跑全绿;探针内容禁止提交,不得保留任何痕迹;用例未变红视为该用例无对抗力,须报 issue"
|
|
101
|
+
- "提交前工作区不得残留代码类未提交改动(反向探针改动必须还原、新增临时文件必须删除);文档类改动(openspec/ 下文件与 .md 文件,含新建的未跟踪文件)会在提交时自动 commit,不受此限。工具层会拒绝带代码类未提交改动的提交"
|
|
99
102
|
- "即使无 issue 也必须提交 verdict=passed(不通过必须有至少一个 Low+ issue 作为理由)"
|
|
100
103
|
- "禁止为腾出端口或资源而停止、复用或干扰非本会话启动的进程(含用户或其他会话运行的服务实例);验证所需环境一律按隔离标识隔离启动"
|
|
101
104
|
- id: verify_quality
|
|
@@ -123,4 +126,5 @@ phases:
|
|
|
123
126
|
constraints:
|
|
124
127
|
- "禁止运行确定性工具检查(包括但不限于 linter/formatter/静态分析/编译/测试/架构约束检查等);运行权归工具检查层,本约束不排斥基于已加载 skill 做工具化可行性判断并提交工具改进 issue"
|
|
125
128
|
- "工具调用边界:仅可调用 opx_status、opx_agent_submit;禁止 edit/write 影响代码运行的内容,仅输出审查报告;文档/注释等不影响代码运行的内容发现不一致/过时问题时直接修正,不为此报 issue"
|
|
129
|
+
- "提交前工作区不得残留代码类未提交改动(反向探针改动必须还原、新增临时文件必须删除);文档类改动(openspec/ 下文件与 .md 文件,含新建的未跟踪文件)会在提交时自动 commit,不受此限。工具层会拒绝带代码类未提交改动的提交"
|
|
126
130
|
- "即使无 issue 也必须提交 verdict=passed;passed=false 时必须有至少一个归属本维度的 Low+ issue 作为理由"
|
|
@@ -32,9 +32,8 @@ phases:
|
|
|
32
32
|
- "按 Task 项顺序逐个实现,聚焦当前子任务,不超出执行边界"
|
|
33
33
|
- "实施前识别 spec 描述的一并操作行为组(如状态变更与数据入队这类需同时发生的多个行为);实施后逐项自证每个行为真实生效,禁止以单一状态字段的实现替代其他行为"
|
|
34
34
|
- "涉及数据模型或流程产出变更(含字段/实体承载位置搬迁、AI 生成等流程产物)时,自证须包含关键数据产出断言:产出字段已持久化到 spec 指定承载位置且值完整非空、搬迁承接与同一输入产出基线一致(按已加载接口测试规范执行);禁止仅以流程跑通/接口成功替代"
|
|
35
|
-
- "有代码、接口契约或数据口径变更时,须按已加载的测试设计规范产出行为面清单、用例矩阵与反向探针记录,随提交在 self_check_results 中逐项申报;反向探针结束后必须先还原实现并复跑全量用例确认全绿再提交,禁止带着改坏的实现提交"
|
|
36
35
|
- "自证/验证需启动服务或基础设施时,一律按已加载技术栈 skill 的隔离启动规范以隔离方式启动,隔离标识与建议端口以视图 Worktree 区块为准;隔离对象不限于数据库——为本变更启动的 mock 服务、消息队列/缓存等中间件、扫描分析平台等一切环境资源均须按隔离标识隔离,禁止复用既有实例或共享端口;启动前发现隔离环境有残留时,先按已加载的清理规范幂等清空再启动;残留判定遵循已加载清理规范的归属判别,无法确认归属本变更的进程一律不清"
|
|
37
|
-
- "
|
|
36
|
+
- "涉及接口契约或数据口径变更(按已加载的接口测试规范判定)时,实施过程中即时更新/新建与变更接口对应的接口测试脚本(已有脚本更新为匹配变更后契约,尚无脚本则新建,并同步 SQL 前置数据脚本),按已加载接口测试规范的完整执行顺序(含 SQL 前置数据准备、隔离启动基础设施与服务、执行接口测试脚本)执行接口测试,确保变更内容真实有效;执行结果(附执行顺序与覆盖接口清单)经 test_results 参数提交,作为审查者分级复验的事实输入;修复被驳回 issue 后再次提交前同样须执行"
|
|
38
37
|
- "自证完成后停止本次启动的服务/基础设施实例,不留残留"
|
|
39
38
|
- "环境/基础设施问题(schema 缺失、DDL 未执行、依赖未安装)应通过 migration/初始化脚本等代码层面解决;仅生产级凭据、真实第三方资源、人工运维才走 blocker/豁免"
|
|
40
39
|
- "issue 指向文件的目录已并入执行边界,修复这些文件(含回归引入问题)不算越界;issue 的 file 字段指向 openspec/changes/ 下文档时,按该 issue 要求修复对应文档同样不算越界(仅限该 issue 指定文件,不得扩大范围);可按 issue 的 suggestion 直接执行修复;reviewer 的 boundary_expansion 已并入边界"
|
|
@@ -73,8 +72,9 @@ phases:
|
|
|
73
72
|
- "依据 opx_status 提供的变更检测结果分流:无代码/配置变更且本层无待复核/待裁定项 → 直接提交 passed,不运行全量工具检查,并以一条 step 名首段为 no_change 的 validation_steps 声明本轮整体豁免必做清单(completed=false + 结构化 skip_reason 注明判定依据);无变更但有本层待复核/待裁定项 → 仅复核/裁定待处理项后提交,不跑全量工具扫描;有变更 → 加载质量门类 skill 获取工具清单与映射规则,按分级复验执行确定性检查:低成本必做项(env/compile/format/architecture/static_analysis/unit_test/config_check 等分钟级项)全量实跑并逐项申报,将工具输出映射为统一 issue 结构;deep_scan 按核验申报口径处理(见下条)"
|
|
74
73
|
- "「有变更」分支先审查上方「本次变更证据」区块的变更文件清单与区间 diff 内容后自行判断:若确认本次变更仅为注释/文档性且无逻辑影响(非注释掉代码、非结构性/格式化变更、非文件重命名),可自行决定跳过全量工具检查直接提交 passed(validation_steps 以 step=no_change 申报整体豁免必做清单);无法确认或含逻辑/结构变更,必须按分级复验口径执行确定性检查;裁量仅免除全量工具扫描,不免除复核/裁定——本层 blocking issue 仍须处理至终态后提交"
|
|
75
74
|
- "分级复验:低成本必做项(分钟级)全量实跑 + 逐项申报(completed=true 附执行结果);deep_scan 核验上方「开发者自检申报」区块的 dev 申报(证据完整性、命中项与 diff 的合理性)并抽验命中项(如本地复跑单规则静态分析验证命中真实存在),不重扫全量,申报形态为 step 名首段 deep_scan、completed=true、描述注明核验方式与抽验样本;白名单口径:仅高成本必做项(deep_scan)允许核验申报形态,低成本必做项必须实跑后申报;升级回退全量重跑触发条件(统一四处):dev 申报缺证据或不完整 / 抽验失败 / 发现断言放水类 issue / 本轮为修复轮且修复波及接口契约或数据口径——命中任一即本轮高成本项改为实跑全量"
|
|
76
|
-
- "对抗性判断(独立视角,不重走 dev 路径):①task 产出完整性 ②③按 spec 验收标准选样的核心链路抽样重放(1-2 条端到端,覆盖创建-检索-变更-删除闭环中 spec
|
|
77
|
-
- "
|
|
75
|
+
- "对抗性判断(独立视角,不重走 dev 路径):①task 产出完整性 ②③按 spec 验收标准选样的核心链路抽样重放(1-2 条端到端,覆盖创建-检索-变更-删除闭环中 spec 声明的关键行为;服务健康并入抽样重放——能重放即健康)并审查接口测试脚本质量(对照 spec 核断言覆盖/放水/Mock 过度——防作弊主力;含 API 测试自闭环链路:创建后可检索、状态变更后可见、删除后不可见;测试数据贴近业务语义,禁止无意义占位值)④审查测试代码质量(断言放水/Mock 过度/覆盖不足)⑤业务行为真实性:对照 spec 逐项核验每个业务行为的实现与断言证据(含一并操作的行为组、连带效果、下游消费),识别『仅打状态标记未实际生效』类缺陷 ⑥关键数据产出核验:对照 spec 数据产出要求,核验实现与测试是否覆盖产出数据落位(spec 指定承载位置)、完整非空、搬迁承接与同一输入基线一致,识别『流程正常但数据缺失/落错位置』类缺陷;轮次口径:首轮按上述分级复验与对抗判断全项执行;复核轮(二轮起)聚焦被修复 issue 相关面 + 本轮修复 diff 波及面 + dev 再自检申报(「开发者自检申报」区块),首轮已通过且无波及的维度与低成本工具结论不重审,但低成本工具实跑保留(防修复引入回归)"
|
|
76
|
+
- "测试设计核验(独立完成,不以 dev 申报为输入):从 spec 与既有实现独立推导行为面清单与用例矩阵,核对测试对行为面的覆盖、断言强度与实施正确性(检查点按已加载测试设计规范与任务维度判例执行);自行对关键用例执行反向探针——改坏被测实现 → 确认对应用例变红 → 原样还原实现 → 全量用例复跑全绿,把改坏点与还原确认结果编入 validation_steps 的 negative_probe 申报;用例未变红视为该用例无对抗力,须报 issue;无测试对象(无代码、接口契约与数据口径变更)时以 completed=false + 结构化 skip_reason 申报;不得跳过核验直接提交通过"
|
|
77
|
+
- "本 step 能力集含接口测试能力,须按必做项口径申报 api_script 与 api_execution:api_script 对应「变更接口有对应的接口测试脚本」,api_execution 对应「按已加载接口测试规范执行脚本实跑通过」;变更不涉及接口契约或数据口径时,两项均置 completed=false,category 填 not_applicable、adjudication 按实际裁定方式填,note 写判定依据;变更涉及接口契约或数据口径但隔离环境不可用(服务无法启动、隔离实例不可用)时,仅 api_execution 置 completed=false,category 与 adjudication 均填 env_unavailable,note 记录尝试过程,api_script 仍须实查脚本存在性,脚本缺失按「契约变更无对应脚本」判例报 issue,不得以环境不可用为由跳过;两类语义分开,不得混用"
|
|
78
78
|
- "验证完成后(含 verdict=failed 报 issue 回退 implement 的轮次)按已加载的清理规范执行幂等清理:停止残留的服务实例(含抽样重放启动的服务)、清理本 change 的隔离环境、删除残留的隔离扫描项目;无残留即通过,不因重复执行报错;清理结果纳入 validation_steps 申报(未执行须以 completed=false + 结构化 skip_reason 说明)"
|
|
79
79
|
- "本次变更仅含测试代码(判定:视图「变更范围」文件清单全部命中已加载技术栈 skill 定义的测试代码路径,且无生产代码、接口契约、构建与测试配置等非测试文件)时,豁免②③核心链路抽样重放,仅验证①④;豁免时 validation_steps 中②③置 completed=false,并以结构化 skip_reason 申报(格式:{\"item\":\"<对应验证项>\",\"category\":\"<降级类别>\",\"adjudication\":\"user_response|unattended_auto|env_unavailable\",\"note\":\"<说明>\"}),note 注明「本次变更仅含测试代码,豁免抽样重放」及判定依据;⑤⑥的实现核验在纯测试代码变更场景无对象,其断言证据核验并入④测试代码质量"
|
|
80
80
|
- "读取 design.md 的 API 定义(请求/响应结构、数据模型)与 specs/ 下相关 spec.md(需求细节、验收标准与数据产出要求),用于准备测试数据与对照 API 合约、选样核心链路"
|
|
@@ -83,7 +83,7 @@ phases:
|
|
|
83
83
|
- "报 issue 时必须显式声明归因维度 dimension(style/architecture/performance/security/maintainability):本 reviewer 不归属任一质量维度,dimension 即该 issue 归属的目标质量维度"
|
|
84
84
|
- "当发现修复需要调整 openspec/changes/ 下文档时,所报 issue 的 file 字段必须指向目标文档"
|
|
85
85
|
- "报 issue 前先判断该问题能否通过调整确定性质量扫描工具配置(规则收紧/新增规则/配置增强)统一收敛:可工具化 → 双报业务 issue(file=违规现场)与工具改进 issue(file=规则/配置文件,suggestion 含规则草案+验证命令,末尾标 [tool_eligible],且与业务 issue 同维度),两条 file 不同、方案不同,不构成语义重复,必须双报;工具改进 issue 目标文件在执行边界外时附 boundary_expansion(仅 verdict=failed 时可附);不可工具化 → 仅报业务 issue。工具化判断细节与规则草案模板遵循已加载的工具改进类 skill,未加载工具改进类 skill 时仅报业务 issue、跳过工具改进环节;提交时在 validation_steps 中记录工具化判断结论"
|
|
86
|
-
- "
|
|
86
|
+
- "只报不改与文档直改:影响代码运行的逻辑类问题只上报、不直接修改(反向探针核验除外,见约束);文档/注释等不影响代码运行的内容发现不一致/过时问题时直接修正,不为此报 issue,修正情况在提交说明/validation_steps 留痕;工具改进建议仅上报(含规则草案),实施由开发者完成"
|
|
87
87
|
- "复核自己报的已修复待复核(review 态)issue:通过置 done,不通过驳回(附驳回原因,issue 回待修复并累计修复未过次数)"
|
|
88
88
|
- "对视图「待裁定是否可豁免」区块中的豁免申请经 exempt_adjudications 裁定(dismissed/rejected)"
|
|
89
89
|
- "审查变更 diff 与 git log,识别变更中新增的压制手段(压制注解/注释抑制/规则排除配置/平台状态标注/构建跳过开关等):视为豁免申请,核对 dev 是否已在压制处代码注释或 commit message 说明理由并申报豁免;未申报或理由不成立 → 报 issue(file=压制位置、line=压制行、rule=被压制规则名,severity 至少 Low,description/suggestion 写明判定门槛与理由),要求移除压制真实修复或补申报;理由成立 → 经 exempt_adjudications 裁定 dismissed"
|
|
@@ -96,6 +96,8 @@ phases:
|
|
|
96
96
|
constraints:
|
|
97
97
|
- "工具调用边界:仅可调用 opx_status、opx_agent_submit、question(需要用户协助处理时);可 bash 运行确定性工具检查、启动服务并执行验证命令"
|
|
98
98
|
- "只报不改与文档直改:禁止 edit/write 影响代码运行的内容(逻辑类问题仅上报);文档/注释等不影响代码运行的内容发现不一致/过时问题时直接修正,不为此报 issue"
|
|
99
|
+
- "反向探针例外:执行反向探针核验时可临时改坏被测实现(仅限探针所需的最小改动),探针结束后必须原样还原并确认 git 工作区无残留改动(git status 无改动)、全量用例复跑全绿;探针内容禁止提交,不得保留任何痕迹;用例未变红视为该用例无对抗力,须报 issue"
|
|
100
|
+
- "提交前工作区不得残留代码类未提交改动(反向探针改动必须还原、新增临时文件必须删除);文档类改动(openspec/ 下文件与 .md 文件,含新建的未跟踪文件)会在提交时自动 commit,不受此限。工具层会拒绝带代码类未提交改动的提交"
|
|
99
101
|
- "即使无 issue / 无待验证 task 也必须提交 verdict=passed(不通过必须有至少一个 Low+ issue 作为理由)"
|
|
100
102
|
- "Info 级 issue 的 description/suggestion 禁止阶段/时机表述(如'可后续处理')"
|
|
101
103
|
- "禁止以 SonarQube 平台状态修改(SAFE/WONTFIX/False Positive/Confirmed 等)作为豁免手段:平台状态仅作用于当前 SonarQube 项目实例,每次 change 使用独立项目 key,不跨 change 生效;存量问题跨 change 豁免必须经 exempt_issue_ids 申请 → 报源 reviewer 裁定 dismissed 的流程"
|
|
@@ -111,6 +113,7 @@ phases:
|
|
|
111
113
|
instructions:
|
|
112
114
|
- "将基准分支({{base_branch}})最新代码合并进当前工作分支(git merge 基准分支),解决合并冲突;修复范围仅限合并冲突涉及文件与回归失败定位文件(改动仅用于解决合并引入的问题,禁止借机扩大范围)"
|
|
113
115
|
- "合并后回归复验:按已加载的开发实践规范执行构建验证;按已加载的接口测试规范启动服务并执行接口测试(含关键数据产出/搬迁回归断言);全部通过才算通过,不分失败来源(含基准分支新代码引入的失败)"
|
|
116
|
+
- "本 step 能力集含接口测试能力,须按必做项口径申报 api_script 与 api_execution:api_script 对应「变更接口有对应的接口测试脚本」,api_execution 对应「按已加载接口测试规范执行脚本实跑通过」;变更不涉及接口契约或数据口径时,两项均置 completed=false,category 填 not_applicable、adjudication 按实际裁定方式填,note 写判定依据;变更涉及接口契约或数据口径但隔离环境不可用(服务无法启动、隔离实例不可用)时,仅 api_execution 置 completed=false,category 与 adjudication 均填 env_unavailable,note 记录尝试过程,api_script 仍须实查脚本存在性,脚本缺失按「契约变更无对应脚本」判例报 issue,不得以环境不可用为由跳过;两类语义分开,不得混用"
|
|
114
117
|
- "按已加载的清理规范执行幂等清理:停止残留的服务实例、清理本 change 的隔离环境、删除残留的扫描分析项目;无对应 skill 规范时跳过并在申报中注明"
|
|
115
118
|
- "清理后确认工作分支 git status 干净(含未跟踪文件)"
|
|
116
119
|
- "逐项申报 validation_steps(completed + 证据;未执行项以 completed=false + 结构化 skip_reason 申报:{\"item\":\"<必做项>\",\"category\":\"<降级类别>\",\"adjudication\":\"user_response|unattended_auto|env_unavailable\",\"note\":\"<说明>\"})"
|
|
@@ -63,15 +63,14 @@ phases:
|
|
|
63
63
|
- "按 Task 项顺序逐个实现,聚焦当前子任务,不超出执行边界"
|
|
64
64
|
- "实施前识别 spec 描述的一并操作行为组(如状态变更与数据入队这类需同时发生的多个行为);实施后逐项自证每个行为真实生效,禁止以单一状态字段的实现替代其他行为"
|
|
65
65
|
- "涉及数据模型或流程产出变更(含字段/实体承载位置搬迁、AI 生成等流程产物)时,自证须包含关键数据产出断言:产出字段已持久化到 spec 指定承载位置且值完整非空、搬迁承接与同一输入产出基线一致(按已加载接口测试规范执行);禁止仅以流程跑通/接口成功替代"
|
|
66
|
-
- "有代码、接口契约或数据口径变更时,须按已加载的测试设计规范产出行为面清单、用例矩阵与反向探针记录,随提交在 self_check_results 中逐项申报;反向探针结束后必须先还原实现并复跑全量用例确认全绿再提交,禁止带着改坏的实现提交"
|
|
67
66
|
- "自证/验证需启动服务或基础设施时,一律按已加载技术栈 skill 的隔离启动规范以隔离方式启动,隔离标识与建议端口以视图 Worktree 区块为准;隔离对象不限于数据库——为本变更启动的 mock 服务、消息队列/缓存等中间件、扫描分析平台等一切环境资源均须按隔离标识隔离,禁止复用既有实例或共享端口;启动前发现隔离环境有残留时,先按已加载的清理规范幂等清空再启动;残留判定遵循已加载清理规范的归属判别,无法确认归属本变更的进程一律不清"
|
|
68
|
-
- "
|
|
67
|
+
- "涉及接口契约或数据口径变更(按已加载的接口测试规范判定)时,实施过程中即时更新/新建与变更接口对应的接口测试脚本(已有脚本更新为匹配变更后契约,尚无脚本则新建,并同步 SQL 前置数据脚本),按已加载接口测试规范的完整执行顺序(含 SQL 前置数据准备、隔离启动基础设施与服务、执行接口测试脚本)执行接口测试,确保变更内容真实有效;执行结果(附执行顺序与覆盖接口清单)经 test_results 参数提交;修复被驳回 issue 后再次提交前同样须执行"
|
|
69
68
|
- "自证完成后停止本次启动的服务/基础设施实例,不留残留"
|
|
70
69
|
- "环境/基础设施问题(schema 缺失、DDL 未执行、依赖未安装)应通过 migration/初始化脚本等代码层面解决;仅生产级凭据、真实第三方资源、人工运维才走 blocker/豁免"
|
|
71
70
|
- "issue 指向文件的目录已并入执行边界,修复这些文件(含回归引入问题)不算越界;issue 的 file 字段指向 openspec/changes/ 下文档时,按该 issue 要求修复对应文档同样不算越界(仅限该 issue 指定文件,不得扩大范围);可按 issue 的 suggestion 直接执行修复;reviewer 的 boundary_expansion 已并入边界"
|
|
72
71
|
- "按视图 Issue 清单分类实施:Low+ 必办,Info 建议修复不阻塞提交"
|
|
73
72
|
- "对修复未过次数≥2 的 issue 须先完成 5-Why 根因分析再动手,不得跳过分析直接改代码"
|
|
74
|
-
- "全部实现完成后先 git commit
|
|
73
|
+
- "全部实现完成后先 git commit 再提交(未提交内容会被工具拒绝)"
|
|
75
74
|
- "提交前按已加载的质量门类 skill 完成本地构建验证并核对覆盖率门禁达标后再提交"
|
|
76
75
|
- "提交前完成自检并经 self_check_results 汇总"
|
|
77
76
|
- "提交前按已加载的维度规范类 skill(style/architecture/performance/security/maintainability 能力类别)对本次变更文件逐项自检(聚焦本轮变更文件,不扩散全量代码),自检结论与工具检查结果一并汇总进 self_check_results;修复被驳回 issue 后再次提交前同样须按维度规范类 skill 对变更文件自检后再提交"
|
|
@@ -98,7 +97,7 @@ phases:
|
|
|
98
97
|
- id: verify_tool
|
|
99
98
|
agents:
|
|
100
99
|
- id: openspec-reviewer-tool
|
|
101
|
-
capability_tags: [quality-gate, efficiency
|
|
100
|
+
capability_tags: [quality-gate, efficiency]
|
|
102
101
|
transitions:
|
|
103
102
|
on_pass: verify_task
|
|
104
103
|
on_fail: implement
|
|
@@ -116,6 +115,7 @@ phases:
|
|
|
116
115
|
- "报静态分析类 issue 时,description 必须含:①复现命令(非静默形态,含必要开关);②实测数据(命中类清单/计数);③规则默认阈值与当前项目配置对比(引用可改的配置文件路径)"
|
|
117
116
|
constraints:
|
|
118
117
|
- "工具调用边界:仅可调用 opx_status、opx_agent_submit、question(需要用户协助处理时);可 bash 运行工具;禁止 edit/write 影响代码运行的内容;文档/注释等不影响代码运行的内容发现不一致/过时问题时直接修正,不为此报 issue"
|
|
118
|
+
- "提交前工作区不得残留代码类未提交改动(反向探针改动必须还原、新增临时文件必须删除);文档类改动(openspec/ 下文件与 .md 文件,含新建的未跟踪文件)会在提交时自动 commit,不受此限。工具层会拒绝带代码类未提交改动的提交"
|
|
119
119
|
- "即使无 issue 也必须提交 verdict=passed(不通过必须有至少一个 Low+ issue 作为理由)"
|
|
120
120
|
- "Info 级 issue 的 description/suggestion 禁止阶段/时机表述(如'可后续处理')"
|
|
121
121
|
- "禁止以 SonarQube 平台状态修改(SAFE/WONTFIX/False Positive/Confirmed 等)作为豁免手段:平台状态仅作用于当前 SonarQube 项目实例,每次 change 使用独立项目 key,不跨 change 生效;存量问题跨 change 豁免必须经 exempt_issue_ids 申请 → 报源 reviewer 裁定 dismissed 的流程"
|
|
@@ -130,8 +130,9 @@ phases:
|
|
|
130
130
|
on_fail: implement
|
|
131
131
|
instructions:
|
|
132
132
|
- "逐项验证:①task 产出完整性 ②启动服务并检查健康 ③独立执行全量 API 测试并审查质量(含 API 测试自闭环链路:创建后可检索、状态变更后可见、删除后不可见;测试数据贴近业务语义,禁止无意义占位值) ④审查测试代码质量(断言放水/Mock 过度/覆盖不足)⑤业务行为真实性:对照 spec 逐项核验每个业务行为的实现与断言证据(含一并操作的行为组、连带效果、下游消费),识别『仅打状态标记未实际生效』类缺陷 ⑥关键数据产出核验:对照 spec 数据产出要求,核验实现与测试是否覆盖产出数据落位(spec 指定承载位置)、完整非空、搬迁承接与同一输入基线一致,识别『流程正常但数据缺失/落错位置』类缺陷"
|
|
133
|
-
# 全模式 verify_task 视图不渲染开发者自检申报,故证据源取变更 diff 与 worktree
|
|
134
|
-
- "
|
|
133
|
+
# 全模式 verify_task 视图不渲染开发者自检申报,故证据源取变更 diff 与 worktree 测试代码;两种模式均由审查者独立推导,不以开发者申报为输入
|
|
134
|
+
- "测试设计核验(独立完成,不以 dev 申报为输入):从 spec 与既有实现独立推导行为面清单与用例矩阵,核对测试对行为面的覆盖、断言强度与实施正确性;自行对关键用例执行反向探针——改坏被测实现 → 确认对应用例变红 → 原样还原实现 → 全量用例复跑全绿,把改坏点与还原确认结果编入 validation_steps 的 negative_probe 申报;用例未变红视为该用例无对抗力,须报 issue;无测试对象(无代码、接口契约与数据口径变更)时以 completed=false + 结构化 skip_reason 申报;不得跳过核验直接提交通过"
|
|
135
|
+
- "本 step 能力集含接口测试能力,须按必做项口径申报 api_script 与 api_execution:api_script 对应「变更接口有对应的接口测试脚本」,api_execution 对应「按已加载接口测试规范执行脚本实跑通过」;变更不涉及接口契约或数据口径时,两项均置 completed=false,category 填 not_applicable、adjudication 按实际裁定方式填,note 写判定依据;变更涉及接口契约或数据口径但隔离环境不可用(服务无法启动、隔离实例不可用)时,仅 api_execution 置 completed=false,category 与 adjudication 均填 env_unavailable,note 记录尝试过程,api_script 仍须实查脚本存在性,脚本缺失按「契约变更无对应脚本」判例报 issue,不得以环境不可用为由跳过;两类语义分开,不得混用"
|
|
135
136
|
- "验证完成后(含 verdict=failed 轮次)按已加载的清理规范执行幂等清理:停止残留的服务实例、清理本 change 的隔离环境、删除残留的隔离扫描项目;无残留即通过,清理结果纳入 validation_steps 申报"
|
|
136
137
|
- "本次变更仅含测试代码(判定:视图「变更范围」文件清单全部命中已加载技术栈 skill 定义的测试代码路径,且无生产代码、接口契约、构建与测试配置等非测试文件)时,豁免②启动服务并检查健康与③独立执行全量 API 测试,仅验证①④;豁免时 validation_steps 中②③置 completed=false,并以结构化 skip_reason 申报(格式:{\"item\":\"<对应验证项>\",\"category\":\"<降级类别>\",\"adjudication\":\"user_response|unattended_auto|env_unavailable\",\"note\":\"<说明>\"}),note 注明「本次变更仅含测试代码,豁免启动服务与 API 测试」及判定依据;⑤⑥的实现核验在纯测试代码变更场景无对象,其断言证据核验并入④测试代码质量"
|
|
137
138
|
- "读取 design.md 的 API 定义(请求/响应结构、数据模型)与 specs/ 下相关 spec.md(需求细节、验收标准与数据产出要求),用于准备测试数据与对照 API 合约"
|
|
@@ -145,6 +146,8 @@ phases:
|
|
|
145
146
|
- "提交 opx_agent_submit,必带 verdict 与 verified_tasks(覆盖全部待验证 task)"
|
|
146
147
|
constraints:
|
|
147
148
|
- "工具调用边界:仅可调用 opx_status、opx_agent_submit;可 bash 启动服务/执行检查;禁止 edit/write 影响代码运行的内容;文档/注释等不影响代码运行的内容发现不一致/过时问题时直接修正,不为此报 issue"
|
|
149
|
+
- "反向探针例外:执行反向探针核验时可临时改坏被测实现(仅限探针所需的最小改动),探针结束后必须原样还原并确认 git 工作区无残留改动(git status 无改动)、全量用例复跑全绿;探针内容禁止提交,不得保留任何痕迹;用例未变红视为该用例无对抗力,须报 issue"
|
|
150
|
+
- "提交前工作区不得残留代码类未提交改动(反向探针改动必须还原、新增临时文件必须删除);文档类改动(openspec/ 下文件与 .md 文件,含新建的未跟踪文件)会在提交时自动 commit,不受此限。工具层会拒绝带代码类未提交改动的提交"
|
|
148
151
|
- "即使无 issue / 无待验证 task 也必须提交 verdict=passed(不通过必须有至少一个 Low+ issue 作为理由)"
|
|
149
152
|
- id: verify_quality
|
|
150
153
|
agents:
|
|
@@ -172,6 +175,7 @@ phases:
|
|
|
172
175
|
constraints:
|
|
173
176
|
- "禁止运行确定性工具检查(包括但不限于 linter/formatter/静态分析/编译/测试/架构约束检查等);运行权归 tool review 层,本约束不排斥基于已加载 skill 做工具化可行性判断并提交工具改进 issue"
|
|
174
177
|
- "工具调用边界:仅可调用 opx_status、opx_agent_submit;禁止 edit/write 影响代码运行的内容,仅输出审查报告;文档/注释等不影响代码运行的内容发现不一致/过时问题时直接修正,不为此报 issue"
|
|
178
|
+
- "提交前工作区不得残留代码类未提交改动(反向探针改动必须还原、新增临时文件必须删除);文档类改动(openspec/ 下文件与 .md 文件,含新建的未跟踪文件)会在提交时自动 commit,不受此限。工具层会拒绝带代码类未提交改动的提交"
|
|
175
179
|
- "即使无 issue 也必须提交 verdict=passed;passed=false 时必须有至少一个归属本维度的 Low+ issue 作为理由"
|
|
176
180
|
- id: verify_cleanup
|
|
177
181
|
agents:
|
|
@@ -183,6 +187,7 @@ phases:
|
|
|
183
187
|
instructions:
|
|
184
188
|
- "将基准分支({{base_branch}})最新代码合并进当前工作分支(git merge 基准分支),解决合并冲突;修复范围仅限合并冲突涉及文件与回归失败定位文件(改动仅用于解决合并引入的问题,可超出现有执行边界,禁止借机扩大范围)"
|
|
185
189
|
- "合并后回归复验:按已加载的开发实践规范执行构建验证;按已加载的接口测试规范启动服务并执行接口测试(含关键数据产出/搬迁回归断言);全部通过才算通过,不分失败来源(含基准分支新代码引入的失败)"
|
|
190
|
+
- "本 step 能力集含接口测试能力,须按必做项口径申报 api_script 与 api_execution:api_script 对应「变更接口有对应的接口测试脚本」,api_execution 对应「按已加载接口测试规范执行脚本实跑通过」;变更不涉及接口契约或数据口径时,两项均置 completed=false,category 填 not_applicable、adjudication 按实际裁定方式填,note 写判定依据;变更涉及接口契约或数据口径但隔离环境不可用(服务无法启动、隔离实例不可用)时,仅 api_execution 置 completed=false,category 与 adjudication 均填 env_unavailable,note 记录尝试过程,api_script 仍须实查脚本存在性,脚本缺失按「契约变更无对应脚本」判例报 issue,不得以环境不可用为由跳过;两类语义分开,不得混用"
|
|
186
191
|
- "按已加载的清理规范执行幂等清理:停止残留的服务实例、清理本 change 的隔离环境、删除残留的扫描分析项目;无对应 skill 规范时跳过并在申报中注明"
|
|
187
192
|
- "清理后确认工作分支 git status 干净(含未跟踪文件)"
|
|
188
193
|
- "逐项申报 validation_steps(completed + 证据;未执行项以 completed=false + 结构化 skip_reason 申报:{\"item\":\"<必做项>\",\"category\":\"<降级类别>\",\"adjudication\":\"user_response|unattended_auto|env_unavailable\",\"note\":\"<说明>\"})"
|
package/dsh/cordis.patch.yml
CHANGED
|
@@ -158,6 +158,7 @@
|
|
|
158
158
|
适用于全部审查身份(含架构师身份与 full 模式的维度审查):
|
|
159
159
|
|
|
160
160
|
- 影响代码运行的逻辑类问题:只上报 issue,不直接修改相关代码
|
|
161
|
+
- 反向探针例外:仅任务验证与合并审查身份可用——核验测试对抗力时可临时改坏被测实现,动作固定为四步:改坏被测实现 → 确认对应用例变红 → 原样还原实现 → 全量用例复跑全绿;探针结束后必须原样还原并确认 git 工作区无残留改动(git status 无改动),探针内容禁止提交,不得保留任何痕迹;残留的代码类未提交改动会被工具层拒绝提交,文档类修正(含新建的 openspec/ 与 .md 文件)照常自动提交;源码文件内的注释修正请自行 commit 后再提交,仅 openspec/ 与 .md 文件由工具自动提交
|
|
161
162
|
- 文档、注释等不影响代码运行的内容:发现不一致/过时问题时直接修正,不为此报 issue;修正情况在提交说明/validation_steps 留痕
|
|
162
163
|
- 工具改进建议仅上报(含规则草案),实施由开发者完成
|
|
163
164
|
|
|
@@ -188,8 +189,8 @@
|
|
|
188
189
|
| 级别 | task 产出验证 | 服务启动验证 | 测试审查 |
|
|
189
190
|
|------|-------------|-------------|---------|
|
|
190
191
|
| Critical | 核心 task 产出文件缺失 | 服务无法启动(无论是否本次变更引起) | 测试依赖无隔离环境导致 CI 必然失败 |
|
|
191
|
-
| High | 关键 task 产出不完整;行为无实现(仅状态标记未实际生效);流程执行正常但关键数据产出缺失/未落库/落错位置(区别于『仅打状态标记未实际生效』:本行=流程与响应正常,产出物缺失或落错位) | 健康检查不通过;关键外部依赖不可用;新增接口无法调通(不论原因) | 核心业务逻辑无测试覆盖;Mock 了被测对象本身;测试仅断言可见状态/响应而 spec 要求的一并操作行为(入队/落库/下游消费等)无断言,将缺陷固化为期望行为;测试仅验证流程跑通/响应成功,未对 spec
|
|
192
|
-
| Medium | task 产出存在但质量不达标;关键数据产出部分缺失、值不完整或与 spec 指定承载位置不一致 | 启动有 WARN 但服务可用 |
|
|
192
|
+
| High | 关键 task 产出不完整;行为无实现(仅状态标记未实际生效);流程执行正常但关键数据产出缺失/未落库/落错位置(区别于『仅打状态标记未实际生效』:本行=流程与响应正常,产出物缺失或落错位) | 健康检查不通过;关键外部依赖不可用;新增接口无法调通(不论原因) | 核心业务逻辑无测试覆盖;Mock 了被测对象本身;测试仅断言可见状态/响应而 spec 要求的一并操作行为(入队/落库/下游消费等)无断言,将缺陷固化为期望行为;测试仅验证流程跑通/响应成功,未对 spec 要求的关键数据产出(落位/完整性)断言;变更涉及接口契约或数据口径,却没有对应接口测试脚本或未执行(不适用场景须有结构化降级理由);高风险场景(并发、幂等、状态机非法迁移、第三方依赖异常与回调超时)无对应用例且无结构化未覆盖理由 |
|
|
193
|
+
| Medium | task 产出存在但质量不达标;关键数据产出部分缺失、值不完整或与 spec 指定承载位置不一致 | 启动有 WARN 但服务可用 | 断言过于宽松;缺少边界值测试或测试设计物缺失(行为面清单/用例矩阵/反向探针记录);关键数据产出部分缺失、值不完整或与 spec 指定承载位置不一致 |
|
|
193
194
|
| Low | 产出文件位置不符合约定 | 启动日志格式不统一 | 测试命名不规范但不影响运行 |
|
|
194
195
|
| Info | 建议补充额外产出物 | 建议增加启动度量指标 | 建议补充边缘用例 |
|
|
195
196
|
|
|
@@ -321,7 +322,7 @@
|
|
|
321
322
|
## 职责边界(确定性检查与任务验证)
|
|
322
323
|
|
|
323
324
|
- **确定性工具检查**:工具维度审查者与 simple 合并审查者被允许运行确定性工具检查(代码格式/架构约束/静态分析/单元测试编译/深度扫描),按已加载质量门 skill 的必做清单执行并将输出映射为统一 issue 结构;检查结果纳入审查结论
|
|
324
|
-
- **任务验证**:任务维度审查者与 simple 合并审查者承担任务验证职责,验证 task 产出完整性、服务健康与 API 测试质量;未通过的 task 经 failed_tasks 上报;验证完成后按清理规范做幂等清理,不留环境残留;数据产出核验——对照 spec
|
|
325
|
+
- **任务验证**:任务维度审查者与 simple 合并审查者承担任务验证职责,验证 task 产出完整性、服务健康与 API 测试质量;未通过的 task 经 failed_tasks 上报;验证完成后按清理规范做幂等清理,不留环境残留;数据产出核验——对照 spec 数据产出要求核验关键数据产出落位/完整性/搬迁承接/基线对比,识别『流程正常但数据缺失/落错位置』类缺陷;测试设计核验——独立产出行为面清单与用例矩阵(从 spec 与既有实现独立推导,不以开发者申报为输入),并自行执行反向探针,识别漏面、漏格、断言放水与探针缺失
|
|
325
326
|
- **维度审查锚点**:以 spec 验收标准、行为组与数据产出要求为主锚做对照审查,skill 检查点清单与 dev 自检申报对照兜底(抽查申报遗漏项)
|
|
326
327
|
- **工具改进**:可工具化的质量 pattern 双报业务 issue 与工具改进 issue(规则草案由审查者提供),实施工作由开发者完成,审查者不直接改动规则/配置文件以外的实现
|
|
327
328
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ifrankwang/openspec-agents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.151.1",
|
|
4
4
|
"description": "OpenSpec companion plugin for AI coding agents (Claude Code, Codex, ZCode, DeepSeek Harness, OpenCode): a stricter proposal standard plus multi-agent implementation and review, gated to production-grade standards",
|
|
5
5
|
"author": "ifrankwang",
|
|
6
6
|
"license": "MIT",
|
package/src/core/git.ts
CHANGED
|
@@ -219,6 +219,47 @@ export async function isWorktreeClean(worktree: string): Promise<boolean> {
|
|
|
219
219
|
return out.length === 0
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
/**
|
|
223
|
+
* 文档类路径判定:`openspec/` 下文件与 `.md` 后缀文件属文档类(reviewer 直改自动提交的白名单)。
|
|
224
|
+
* 源码、配置、脚本等其余路径一律不算文档类。
|
|
225
|
+
*/
|
|
226
|
+
export function isDocLikePath(p: string): boolean {
|
|
227
|
+
return p.startsWith("openspec/") || /\.md$/i.test(p)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* 列出 worktree 中「非文档类」的未提交改动文件(已跟踪修改/删除 + 未跟踪新文件)。
|
|
232
|
+
*
|
|
233
|
+
* 用途:reviewer 家族提交前的机制层预检——反向探针改坏的实现若未还原,其改动会被自动 commit
|
|
234
|
+
* 静默吞掉。过滤 `openspec/states/`(编排状态文件)与文档类,其余一律视为需要清理的代码类改动;
|
|
235
|
+
* 结果去重并保持 `git status --porcelain` 的出现顺序。
|
|
236
|
+
*
|
|
237
|
+
* status 查询失败时返回空数组,属 fail-open 降级:读不到工作区状态不阻断审查提交
|
|
238
|
+
* (与 runGit 失败返回空串的既有降级口径一致),不要误认为是 bug。
|
|
239
|
+
*/
|
|
240
|
+
export async function listDirtyNonDocFiles(worktree: string): Promise<string[]> {
|
|
241
|
+
// 用 -uall 让未跟踪目录展开成具体文件:默认 porcelain 会把新建目录折叠成 `?? docs/`,
|
|
242
|
+
// 目录路径既不是 openspec/ 前缀也不以 .md 结尾,会让「新目录里只放了 .md」被误判为代码类。
|
|
243
|
+
const res = await runGitChecked(worktree, ["status", "--porcelain", "-uall"])
|
|
244
|
+
if (!res.success) return []
|
|
245
|
+
const seen = new Set<string>()
|
|
246
|
+
const files: string[] = []
|
|
247
|
+
// 用 parseStatusEntries 而非 parsePorcelainPaths:rename 条目(`R old -> new`)必须双向解析。
|
|
248
|
+
// 只取新路径会漏判「代码文件改名为 .md」——旧路径的代码删除被当成文档改动放行,删除会被自动
|
|
249
|
+
// commit 静默吞掉。任一方向路径非文档类即视为需清理的代码类改动。
|
|
250
|
+
for (const entry of parseStatusEntries(res.stdout)) {
|
|
251
|
+
for (const p of entry.paths) {
|
|
252
|
+
if (!p) continue
|
|
253
|
+
if (p.startsWith("openspec/states/")) continue
|
|
254
|
+
if (isDocLikePath(p)) continue
|
|
255
|
+
if (seen.has(p)) continue
|
|
256
|
+
seen.add(p)
|
|
257
|
+
files.push(p)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return files
|
|
261
|
+
}
|
|
262
|
+
|
|
222
263
|
export async function markTaskGroupCheckboxesComplete(
|
|
223
264
|
worktree: string,
|
|
224
265
|
changeId: string,
|
|
@@ -771,53 +812,83 @@ export async function reconcileMainPollution(
|
|
|
771
812
|
|
|
772
813
|
/** worktree 自动提交(reviewer 家族文档直改兜底)结果。 */
|
|
773
814
|
export interface AutoCommitResult {
|
|
774
|
-
/** skipped:过滤 openspec/states/
|
|
775
|
-
* failed:git 调用失败(降级返回,不抛异常)。 */
|
|
815
|
+
/** skipped:过滤 openspec/states/ 后既无已跟踪文件变更、也无未跟踪文档类(未触发提交);
|
|
816
|
+
* committed:add -u + 未跟踪文档类的 git add + commit 完成;failed:git 调用失败(降级返回,不抛异常)。 */
|
|
776
817
|
status: "skipped" | "committed" | "failed"
|
|
777
|
-
/**
|
|
818
|
+
/** 本次实际提交的文件路径(已跟踪变更 + 未跟踪文档类)。rename 条目只保留新路径,
|
|
819
|
+
* 避免同一重命名被计成两个文件;旧路径不丢信息——重命名前路径的删除已由提交前预检拦截。 */
|
|
778
820
|
files: string[]
|
|
779
821
|
/** status=failed 时的失败 stderr。 */
|
|
780
822
|
stderr?: string
|
|
781
|
-
/**
|
|
823
|
+
/** 未纳入自动提交的未跟踪文件(?? 条目)路径清单:非文档类,提示编排者/开发者另行处理。
|
|
824
|
+
* 未跟踪的文档类文件已纳入自动提交,不出现在此清单。 */
|
|
782
825
|
untrackedFiles: string[]
|
|
783
826
|
}
|
|
784
827
|
|
|
785
828
|
/**
|
|
786
|
-
* 自动提交 worktree
|
|
829
|
+
* 自动提交 worktree 内的文档类改动(reviewer 家族文档直改兜底)。
|
|
787
830
|
*
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
831
|
+
* 触发场景:审查者/架构师按文档直改义务修正文档后,由 opx_agent_submit 在 verdict 校验通过后
|
|
832
|
+
* 调用——未提交修正会被下一轮工具检查点增量检测误判为新变更,也会在 reconcileMainPollution 的
|
|
833
|
+
* 干净树预检处死锁,提交时自动 commit 兜底。
|
|
791
834
|
*
|
|
792
835
|
* 语义:
|
|
793
|
-
* 1. `status --porcelain`
|
|
794
|
-
*
|
|
795
|
-
*
|
|
836
|
+
* 1. `status --porcelain` 解析条目(rename 双向解析),过滤 `openspec/states/` 编排状态目录;
|
|
837
|
+
* 文档类(openspec/ 与 .md)中的已跟踪改动与未跟踪新建文件都纳入提交,其余未跟踪文件
|
|
838
|
+
* (非文档类)不纳入、经 untrackedFiles 提示调用方处理。
|
|
839
|
+
* 2. 过滤后既无已跟踪变更、也无未跟踪文档类 → skipped(不产生空 commit)。
|
|
840
|
+
* 3. 有可提交内容 → `add -u`(已跟踪修改/删除,排除 openspec/states/)+ `add -- <未跟踪文档>`
|
|
796
841
|
* → `commit`(消息风格对齐既有 `docs(tasks):` / `docs(openspec):` 惯例)。
|
|
797
|
-
*
|
|
842
|
+
* 4. 任一 git 调用失败 → failed(含 stderr),不抛异常——调用方据此在返回体提示,不阻塞 verdict 写入。
|
|
798
843
|
*/
|
|
799
844
|
export async function autoCommitWorktreeChanges(
|
|
800
845
|
wtPath: string,
|
|
801
846
|
agent: string,
|
|
802
847
|
stepId: string,
|
|
803
848
|
): Promise<AutoCommitResult> {
|
|
804
|
-
|
|
849
|
+
// -uall:未跟踪目录展开为具体文件,避免新建目录里的文档类文件被折叠成目录路径而判成非文档类。
|
|
850
|
+
const statusRes = await runGitChecked(wtPath, ["status", "--porcelain", "-uall"])
|
|
805
851
|
if (!statusRes.success) {
|
|
806
852
|
return { status: "failed", files: [], untrackedFiles: [], stderr: statusRes.stderr }
|
|
807
853
|
}
|
|
808
|
-
const
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
if (
|
|
814
|
-
|
|
854
|
+
const trackedFiles: string[] = []
|
|
855
|
+
const untrackedDocFiles: string[] = []
|
|
856
|
+
const untrackedOtherFiles: string[] = []
|
|
857
|
+
const seen = new Set<string>()
|
|
858
|
+
const pushUnique = (list: string[], path: string): void => {
|
|
859
|
+
if (seen.has(path)) return
|
|
860
|
+
seen.add(path)
|
|
861
|
+
list.push(path)
|
|
862
|
+
}
|
|
863
|
+
for (const entry of parseStatusEntries(statusRes.stdout)) {
|
|
864
|
+
const isUntracked = entry.x === "?" && entry.y === "?"
|
|
865
|
+
if (isUntracked) {
|
|
866
|
+
for (const path of entry.paths) {
|
|
867
|
+
if (!path || path.startsWith("openspec/states/")) continue
|
|
868
|
+
pushUnique(isDocLikePath(path) ? untrackedDocFiles : untrackedOtherFiles, path)
|
|
869
|
+
}
|
|
870
|
+
continue
|
|
871
|
+
}
|
|
872
|
+
// 已跟踪条目:rename(`R old -> new`)只记新路径,避免同一重命名在提示里计成两个文件。
|
|
873
|
+
// 旧路径不丢信息:重命名前路径的删除已由提交前预检拦截(走不到这里的自动提交阶段)。
|
|
874
|
+
const newPath = entry.paths[entry.paths.length - 1]
|
|
875
|
+
if (!newPath || newPath.startsWith("openspec/states/")) continue
|
|
876
|
+
pushUnique(trackedFiles, newPath)
|
|
877
|
+
}
|
|
878
|
+
if (trackedFiles.length === 0 && untrackedDocFiles.length === 0) {
|
|
879
|
+
return { status: "skipped", files: [], untrackedFiles: untrackedOtherFiles }
|
|
815
880
|
}
|
|
816
|
-
if (files.length === 0) return { status: "skipped", files: [], untrackedFiles }
|
|
817
881
|
|
|
882
|
+
const committedFiles = [...trackedFiles, ...untrackedDocFiles]
|
|
818
883
|
const addResult = await runGitChecked(wtPath, ["add", "-u", "--", ".", ":(exclude)openspec/states"])
|
|
819
884
|
if (!addResult.success) {
|
|
820
|
-
return { status: "failed", files, untrackedFiles, stderr: addResult.stderr }
|
|
885
|
+
return { status: "failed", files: committedFiles, untrackedFiles: untrackedOtherFiles, stderr: addResult.stderr }
|
|
886
|
+
}
|
|
887
|
+
if (untrackedDocFiles.length > 0) {
|
|
888
|
+
const addDocResult = await runGitChecked(wtPath, ["add", "--", ...untrackedDocFiles])
|
|
889
|
+
if (!addDocResult.success) {
|
|
890
|
+
return { status: "failed", files: committedFiles, untrackedFiles: untrackedOtherFiles, stderr: addDocResult.stderr }
|
|
891
|
+
}
|
|
821
892
|
}
|
|
822
893
|
const commitResult = await runGitChecked(wtPath, [
|
|
823
894
|
"commit",
|
|
@@ -825,7 +896,7 @@ export async function autoCommitWorktreeChanges(
|
|
|
825
896
|
`docs(opx): direct fixes by ${agent} (${stepId})`,
|
|
826
897
|
])
|
|
827
898
|
if (!commitResult.success) {
|
|
828
|
-
return { status: "failed", files, untrackedFiles, stderr: commitResult.stderr }
|
|
899
|
+
return { status: "failed", files: committedFiles, untrackedFiles: untrackedOtherFiles, stderr: commitResult.stderr }
|
|
829
900
|
}
|
|
830
|
-
return { status: "committed", files, untrackedFiles }
|
|
901
|
+
return { status: "committed", files: committedFiles, untrackedFiles: untrackedOtherFiles }
|
|
831
902
|
}
|
package/src/core/tools/submit.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { agentToReviewDimension, agentToReviewLayer, isReviewerFamily, readIssue
|
|
|
15
15
|
import { REVIEW_DIMENSIONS } from "../types.ts"
|
|
16
16
|
import type { Dimension } from "../types.ts"
|
|
17
17
|
import { taskChildrenOf, taskChildById, normalizeTaskChildIds, taskListOf, issueChildrenOf } from "../task-children.ts"
|
|
18
|
-
import { autoCommitWorktreeChanges, reconcileMainPollution, getCurrentHead, isWorktreeClean } from "../git.ts"
|
|
18
|
+
import { autoCommitWorktreeChanges, listDirtyNonDocFiles, reconcileMainPollution, getCurrentHead, isWorktreeClean } from "../git.ts"
|
|
19
19
|
import { assertIssueFilesWithin } from "../paths.ts"
|
|
20
20
|
import {
|
|
21
21
|
readStateByWorktree, writeState, getLockPath, acquireLock, releaseLock,
|
|
@@ -792,6 +792,28 @@ export async function agentSubmitExecute(params: AgentSubmitParams, ctx: ToolCon
|
|
|
792
792
|
const newChildren = (params.new_children ?? []).map((nc) => buildIssueChild(nc, ctx.agent))
|
|
793
793
|
|
|
794
794
|
if (stepPhase === "review") {
|
|
795
|
+
// 机制层预检:工作区不得残留「非文档类」未提交改动。reviewer 家族提交后会对 wtPath 自动
|
|
796
|
+
// commit 工作区改动(见下方 isReviewerFamily 分支),反向探针改坏的实现若未还原会被静默
|
|
797
|
+
// 提交掉;这里在一切参数处理与状态写入之前拦截,拒绝时零状态变更(与 simple implement
|
|
798
|
+
// 检查同守则)。failed 提交同样拦截——failed 也会触发自动 commit,不拦会把探针残留提交掉。
|
|
799
|
+
// 文档类(openspec/ 与 .md)放行,由自动 commit 兜底;verify_cleanup 是 developer 身份,
|
|
800
|
+
// 非 reviewer 家族,不经此预检。
|
|
801
|
+
if (isReviewerFamily(ctx.agent)) {
|
|
802
|
+
const dirtyNonDoc = await listDirtyNonDocFiles(wtPath)
|
|
803
|
+
if (dirtyNonDoc.length > 0) {
|
|
804
|
+
const fileLines = dirtyNonDoc.map((f) => ` - ${f}`).join("\n")
|
|
805
|
+
throw new Error(
|
|
806
|
+
`提交被拒绝:工作区存在未提交的代码类改动。\n` +
|
|
807
|
+
`可能是反向探针改坏的实现未还原,或违反了「只报不改」直接修改了代码。\n` +
|
|
808
|
+
`涉及文件:\n${fileLines}\n` +
|
|
809
|
+
`请先清理后再提交:\n` +
|
|
810
|
+
` - 已跟踪文件的改动:git checkout -- <文件> 还原\n` +
|
|
811
|
+
` - 新增的临时文件:直接删除\n` +
|
|
812
|
+
`文档类改动(openspec/ 下文件与 .md 文件)不受此限,提交时会自动 commit;\n` +
|
|
813
|
+
`确属必要的代码修正,请自行 commit 后再提交。`,
|
|
814
|
+
)
|
|
815
|
+
}
|
|
816
|
+
}
|
|
795
817
|
// 必做清单覆盖度门禁取「当前提交 agent 的能力集」:各 agent 只按自身 capability_tags 解析 skill。
|
|
796
818
|
// verify_quality 多 agent 各自解析不出声明 must_do 的 skill(自动跳过),verify_tool 按 quality-gate
|
|
797
819
|
// 能力集、verify_task 按 test-design 能力集分别校验各自的必做清单。
|
|
@@ -810,15 +832,21 @@ export async function agentSubmitExecute(params: AgentSubmitParams, ctx: ToolCon
|
|
|
810
832
|
} else if (stepPhase === "todo") {
|
|
811
833
|
handleAnalyzeParams(item, params)
|
|
812
834
|
} else if (stepPhase === "in_progress") {
|
|
813
|
-
//
|
|
814
|
-
//
|
|
815
|
-
//
|
|
816
|
-
//
|
|
817
|
-
|
|
835
|
+
// implement 提交工作区干净校验:工作区必须无未提交内容,不干净直接拒绝并提示先 commit。
|
|
836
|
+
// 两种模式口径不同:
|
|
837
|
+
// - simple:无 analyze 环节、执行边界默认整个 worktree,dev 提交即收尾合并的载体,未提交内容
|
|
838
|
+
// 会让收尾裸合并携带脏状态——无条件检查(含 verdict=failed)。
|
|
839
|
+
// - full:仅 verdict=passed 时检查。failed 提交多为 blocker/需求缺口上报,此时半成品脏工作区
|
|
840
|
+
// 属正常,强查会把 dev 报 blocker 的路径堵死。
|
|
841
|
+
// 检查在一切参数处理与状态写入之前,拒绝时零状态变更(含内存态);收尾门禁
|
|
842
|
+
// (completeTaskGroupExecute 的 worktree 干净检查)仍原样保留作最终兜底。
|
|
843
|
+
if (params.step_id === "implement" && (state.mode === "simple" || params.verdict === "passed")) {
|
|
818
844
|
const clean = await isWorktreeClean(wtPath)
|
|
819
845
|
if (!clean) {
|
|
820
846
|
throw new Error(
|
|
821
|
-
|
|
847
|
+
state.mode === "simple"
|
|
848
|
+
? "simple 模式 implement 提交被拒绝:工作区存在未提交内容。\n请先 git commit 提交全部变更后再提交 implement。"
|
|
849
|
+
: "full 模式 implement 提交被拒绝:工作区存在未提交内容。\n请先 git commit 后再提交 implement。"
|
|
822
850
|
)
|
|
823
851
|
}
|
|
824
852
|
}
|
|
@@ -875,12 +903,12 @@ export async function agentSubmitExecute(params: AgentSubmitParams, ctx: ToolCon
|
|
|
875
903
|
)
|
|
876
904
|
} else if (ac.status === "failed") {
|
|
877
905
|
autoCommitLines.push(
|
|
878
|
-
`- ⚠️ worktree 直改修正自动提交失败:${ac.stderr || "未知错误"}
|
|
906
|
+
`- ⚠️ worktree 直改修正自动提交失败:${ac.stderr || "未知错误"}。请编排者安排补提交(在 worktree 内手工 add 并 commit:已跟踪改动用 git add -u,未跟踪文档用 git add -- <文件>)`,
|
|
879
907
|
)
|
|
880
908
|
}
|
|
881
909
|
if (ac.untrackedFiles.length > 0) {
|
|
882
910
|
autoCommitLines.push(
|
|
883
|
-
`- ⚠️
|
|
911
|
+
`- ⚠️ 存在未跟踪的非文档类文件未纳入自动提交(文档类已自动提交;这些须由编排者/开发者自行处理):${ac.untrackedFiles.join("、")}`,
|
|
884
912
|
)
|
|
885
913
|
}
|
|
886
914
|
}
|