@ifrankwang/openspec-agents 0.151.0 → 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 +1 -1
- package/assets/workflows/review-simple.yaml +1 -0
- package/assets/workflows/review.yaml +4 -1
- package/assets/workflows/task-simple.yaml +1 -0
- package/assets/workflows/task.yaml +4 -1
- package/dsh/cordis.patch.yml +1 -1
- 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.151.
|
|
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,7 +34,7 @@ permission:
|
|
|
34
34
|
适用于全部审查身份(含架构师身份与 full 模式的维度审查):
|
|
35
35
|
|
|
36
36
|
- 影响代码运行的逻辑类问题:只上报 issue,不直接修改相关代码
|
|
37
|
-
- 反向探针例外:仅任务验证与合并审查身份可用——核验测试对抗力时可临时改坏被测实现,动作固定为四步:改坏被测实现 → 确认对应用例变红 → 原样还原实现 → 全量用例复跑全绿;探针结束后必须原样还原并确认 git 工作区无残留改动(git status
|
|
37
|
+
- 反向探针例外:仅任务验证与合并审查身份可用——核验测试对抗力时可临时改坏被测实现,动作固定为四步:改坏被测实现 → 确认对应用例变红 → 原样还原实现 → 全量用例复跑全绿;探针结束后必须原样还原并确认 git 工作区无残留改动(git status 无改动),探针内容禁止提交,不得保留任何痕迹;残留的代码类未提交改动会被工具层拒绝提交,文档类修正(含新建的 openspec/ 与 .md 文件)照常自动提交;源码文件内的注释修正请自行 commit 后再提交,仅 openspec/ 与 .md 文件由工具自动提交
|
|
38
38
|
- 文档、注释等不影响代码运行的内容:发现不一致/过时问题时直接修正,不为此报 issue;修正情况在提交说明/validation_steps 留痕
|
|
39
39
|
- 工具改进建议仅上报(含规则草案),实施由开发者完成
|
|
40
40
|
|
|
@@ -83,6 +83,7 @@ phases:
|
|
|
83
83
|
- "工具调用边界:仅可调用 opx_status、opx_agent_submit、question(需要用户协助处理时);可 bash 运行确定性工具检查、启动服务并执行验证命令"
|
|
84
84
|
- "只报不改与文档直改:禁止 edit/write 影响代码运行的内容(逻辑类问题仅上报);文档/注释等不影响代码运行的内容发现不一致/过时问题时直接修正,不为此报 issue"
|
|
85
85
|
- "反向探针例外:执行反向探针核验时可临时改坏被测实现(仅限探针所需的最小改动),探针结束后必须原样还原并确认 git 工作区无残留改动(git status 无改动)、全量用例复跑全绿;探针内容禁止提交,不得保留任何痕迹;用例未变红视为该用例无对抗力,须报 issue"
|
|
86
|
+
- "提交前工作区不得残留代码类未提交改动(反向探针改动必须还原、新增临时文件必须删除);文档类改动(openspec/ 下文件与 .md 文件,含新建的未跟踪文件)会在提交时自动 commit,不受此限。工具层会拒绝带代码类未提交改动的提交"
|
|
86
87
|
- "即使无 issue 也必须提交 verdict=passed(不通过必须有至少一个 Low+ issue 作为理由)"
|
|
87
88
|
- "Info 级 issue 的 description/suggestion 禁止阶段/时机表述(如'可后续处理')"
|
|
88
89
|
- "压制类 issue 严重级别至少 Low"
|
|
@@ -41,7 +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
|
|
44
|
+
- "修复完成后先 git commit 再提交(未提交内容会被工具拒绝);提交前按已加载的质量门类 skill 完成本地构建验证,并按维度规范类 skill(style/architecture/performance/security/maintainability 能力类别)对本次修复文件逐项自检,自检结论与工具检查结果一并汇总进 self_check_results"
|
|
45
45
|
- "自证/验证需启动服务或基础设施时,一律按已加载技术栈 skill 的隔离启动规范以隔离方式启动,隔离标识与建议端口以视图 Worktree 区块为准;验证完成后停止本次启动的实例,不留残留,残留判定遵循已加载清理规范的归属判别,无法确认归属本变更的进程一律不清;禁止为腾出端口或资源而停止、复用或干扰非本会话启动的进程"
|
|
46
46
|
- "确定性质量工具检查出的问题不得以压制手段(压制注解/注释抑制/规则排除配置/平台状态标注/构建跳过开关等)静默规避:确需压制时,必须在压制处代码注释说明清楚理由,并经 exempt_issue_ids 申请豁免由报源 reviewer 裁定;不得以 fixed_issue_ids 冒充修复"
|
|
47
47
|
- "提交 opx_agent_submit:必传 verdict;修复完成的 issue 经 fixed_issue_ids 上报、不可修 issue 经 exempt_issue_ids 申请豁免"
|
|
@@ -73,6 +73,7 @@ phases:
|
|
|
73
73
|
- "报静态分析类 issue 时,description 必须含:①复现命令(非静默形态,含必要开关);②实测数据(命中类清单/计数);③规则默认阈值与当前项目配置对比(引用可改的配置文件路径)"
|
|
74
74
|
constraints:
|
|
75
75
|
- "工具调用边界:仅可调用 opx_status、opx_agent_submit、question(需要用户协助处理时);可 bash 运行工具;禁止 edit/write 影响代码运行的内容;文档/注释等不影响代码运行的内容发现不一致/过时问题时直接修正,不为此报 issue"
|
|
76
|
+
- "提交前工作区不得残留代码类未提交改动(反向探针改动必须还原、新增临时文件必须删除);文档类改动(openspec/ 下文件与 .md 文件,含新建的未跟踪文件)会在提交时自动 commit,不受此限。工具层会拒绝带代码类未提交改动的提交"
|
|
76
77
|
- "即使无 issue 也必须提交 verdict=passed(不通过必须有至少一个 Low+ issue 作为理由)"
|
|
77
78
|
- "Info 级 issue 的 description/suggestion 禁止阶段/时机表述(如'可后续处理')"
|
|
78
79
|
- "压制类 issue 严重级别至少 Low"
|
|
@@ -97,6 +98,7 @@ phases:
|
|
|
97
98
|
constraints:
|
|
98
99
|
- "工具调用边界:仅可调用 opx_status、opx_agent_submit;可 bash 启动服务/执行检查;禁止 edit/write 影响代码运行的内容;文档/注释等不影响代码运行的内容发现不一致/过时问题时直接修正,不为此报 issue"
|
|
99
100
|
- "反向探针例外:执行反向探针核验时可临时改坏被测实现(仅限探针所需的最小改动),探针结束后必须原样还原并确认 git 工作区无残留改动(git status 无改动)、全量用例复跑全绿;探针内容禁止提交,不得保留任何痕迹;用例未变红视为该用例无对抗力,须报 issue"
|
|
101
|
+
- "提交前工作区不得残留代码类未提交改动(反向探针改动必须还原、新增临时文件必须删除);文档类改动(openspec/ 下文件与 .md 文件,含新建的未跟踪文件)会在提交时自动 commit,不受此限。工具层会拒绝带代码类未提交改动的提交"
|
|
100
102
|
- "即使无 issue 也必须提交 verdict=passed(不通过必须有至少一个 Low+ issue 作为理由)"
|
|
101
103
|
- "禁止为腾出端口或资源而停止、复用或干扰非本会话启动的进程(含用户或其他会话运行的服务实例);验证所需环境一律按隔离标识隔离启动"
|
|
102
104
|
- id: verify_quality
|
|
@@ -124,4 +126,5 @@ phases:
|
|
|
124
126
|
constraints:
|
|
125
127
|
- "禁止运行确定性工具检查(包括但不限于 linter/formatter/静态分析/编译/测试/架构约束检查等);运行权归工具检查层,本约束不排斥基于已加载 skill 做工具化可行性判断并提交工具改进 issue"
|
|
126
128
|
- "工具调用边界:仅可调用 opx_status、opx_agent_submit;禁止 edit/write 影响代码运行的内容,仅输出审查报告;文档/注释等不影响代码运行的内容发现不一致/过时问题时直接修正,不为此报 issue"
|
|
129
|
+
- "提交前工作区不得残留代码类未提交改动(反向探针改动必须还原、新增临时文件必须删除);文档类改动(openspec/ 下文件与 .md 文件,含新建的未跟踪文件)会在提交时自动 commit,不受此限。工具层会拒绝带代码类未提交改动的提交"
|
|
127
130
|
- "即使无 issue 也必须提交 verdict=passed;passed=false 时必须有至少一个归属本维度的 Low+ issue 作为理由"
|
|
@@ -97,6 +97,7 @@ phases:
|
|
|
97
97
|
- "工具调用边界:仅可调用 opx_status、opx_agent_submit、question(需要用户协助处理时);可 bash 运行确定性工具检查、启动服务并执行验证命令"
|
|
98
98
|
- "只报不改与文档直改:禁止 edit/write 影响代码运行的内容(逻辑类问题仅上报);文档/注释等不影响代码运行的内容发现不一致/过时问题时直接修正,不为此报 issue"
|
|
99
99
|
- "反向探针例外:执行反向探针核验时可临时改坏被测实现(仅限探针所需的最小改动),探针结束后必须原样还原并确认 git 工作区无残留改动(git status 无改动)、全量用例复跑全绿;探针内容禁止提交,不得保留任何痕迹;用例未变红视为该用例无对抗力,须报 issue"
|
|
100
|
+
- "提交前工作区不得残留代码类未提交改动(反向探针改动必须还原、新增临时文件必须删除);文档类改动(openspec/ 下文件与 .md 文件,含新建的未跟踪文件)会在提交时自动 commit,不受此限。工具层会拒绝带代码类未提交改动的提交"
|
|
100
101
|
- "即使无 issue / 无待验证 task 也必须提交 verdict=passed(不通过必须有至少一个 Low+ issue 作为理由)"
|
|
101
102
|
- "Info 级 issue 的 description/suggestion 禁止阶段/时机表述(如'可后续处理')"
|
|
102
103
|
- "禁止以 SonarQube 平台状态修改(SAFE/WONTFIX/False Positive/Confirmed 等)作为豁免手段:平台状态仅作用于当前 SonarQube 项目实例,每次 change 使用独立项目 key,不跨 change 生效;存量问题跨 change 豁免必须经 exempt_issue_ids 申请 → 报源 reviewer 裁定 dismissed 的流程"
|
|
@@ -70,7 +70,7 @@ phases:
|
|
|
70
70
|
- "issue 指向文件的目录已并入执行边界,修复这些文件(含回归引入问题)不算越界;issue 的 file 字段指向 openspec/changes/ 下文档时,按该 issue 要求修复对应文档同样不算越界(仅限该 issue 指定文件,不得扩大范围);可按 issue 的 suggestion 直接执行修复;reviewer 的 boundary_expansion 已并入边界"
|
|
71
71
|
- "按视图 Issue 清单分类实施:Low+ 必办,Info 建议修复不阻塞提交"
|
|
72
72
|
- "对修复未过次数≥2 的 issue 须先完成 5-Why 根因分析再动手,不得跳过分析直接改代码"
|
|
73
|
-
- "全部实现完成后先 git commit
|
|
73
|
+
- "全部实现完成后先 git commit 再提交(未提交内容会被工具拒绝)"
|
|
74
74
|
- "提交前按已加载的质量门类 skill 完成本地构建验证并核对覆盖率门禁达标后再提交"
|
|
75
75
|
- "提交前完成自检并经 self_check_results 汇总"
|
|
76
76
|
- "提交前按已加载的维度规范类 skill(style/architecture/performance/security/maintainability 能力类别)对本次变更文件逐项自检(聚焦本轮变更文件,不扩散全量代码),自检结论与工具检查结果一并汇总进 self_check_results;修复被驳回 issue 后再次提交前同样须按维度规范类 skill 对变更文件自检后再提交"
|
|
@@ -115,6 +115,7 @@ phases:
|
|
|
115
115
|
- "报静态分析类 issue 时,description 必须含:①复现命令(非静默形态,含必要开关);②实测数据(命中类清单/计数);③规则默认阈值与当前项目配置对比(引用可改的配置文件路径)"
|
|
116
116
|
constraints:
|
|
117
117
|
- "工具调用边界:仅可调用 opx_status、opx_agent_submit、question(需要用户协助处理时);可 bash 运行工具;禁止 edit/write 影响代码运行的内容;文档/注释等不影响代码运行的内容发现不一致/过时问题时直接修正,不为此报 issue"
|
|
118
|
+
- "提交前工作区不得残留代码类未提交改动(反向探针改动必须还原、新增临时文件必须删除);文档类改动(openspec/ 下文件与 .md 文件,含新建的未跟踪文件)会在提交时自动 commit,不受此限。工具层会拒绝带代码类未提交改动的提交"
|
|
118
119
|
- "即使无 issue 也必须提交 verdict=passed(不通过必须有至少一个 Low+ issue 作为理由)"
|
|
119
120
|
- "Info 级 issue 的 description/suggestion 禁止阶段/时机表述(如'可后续处理')"
|
|
120
121
|
- "禁止以 SonarQube 平台状态修改(SAFE/WONTFIX/False Positive/Confirmed 等)作为豁免手段:平台状态仅作用于当前 SonarQube 项目实例,每次 change 使用独立项目 key,不跨 change 生效;存量问题跨 change 豁免必须经 exempt_issue_ids 申请 → 报源 reviewer 裁定 dismissed 的流程"
|
|
@@ -146,6 +147,7 @@ phases:
|
|
|
146
147
|
constraints:
|
|
147
148
|
- "工具调用边界:仅可调用 opx_status、opx_agent_submit;可 bash 启动服务/执行检查;禁止 edit/write 影响代码运行的内容;文档/注释等不影响代码运行的内容发现不一致/过时问题时直接修正,不为此报 issue"
|
|
148
149
|
- "反向探针例外:执行反向探针核验时可临时改坏被测实现(仅限探针所需的最小改动),探针结束后必须原样还原并确认 git 工作区无残留改动(git status 无改动)、全量用例复跑全绿;探针内容禁止提交,不得保留任何痕迹;用例未变红视为该用例无对抗力,须报 issue"
|
|
150
|
+
- "提交前工作区不得残留代码类未提交改动(反向探针改动必须还原、新增临时文件必须删除);文档类改动(openspec/ 下文件与 .md 文件,含新建的未跟踪文件)会在提交时自动 commit,不受此限。工具层会拒绝带代码类未提交改动的提交"
|
|
149
151
|
- "即使无 issue / 无待验证 task 也必须提交 verdict=passed(不通过必须有至少一个 Low+ issue 作为理由)"
|
|
150
152
|
- id: verify_quality
|
|
151
153
|
agents:
|
|
@@ -173,6 +175,7 @@ phases:
|
|
|
173
175
|
constraints:
|
|
174
176
|
- "禁止运行确定性工具检查(包括但不限于 linter/formatter/静态分析/编译/测试/架构约束检查等);运行权归 tool review 层,本约束不排斥基于已加载 skill 做工具化可行性判断并提交工具改进 issue"
|
|
175
177
|
- "工具调用边界:仅可调用 opx_status、opx_agent_submit;禁止 edit/write 影响代码运行的内容,仅输出审查报告;文档/注释等不影响代码运行的内容发现不一致/过时问题时直接修正,不为此报 issue"
|
|
178
|
+
- "提交前工作区不得残留代码类未提交改动(反向探针改动必须还原、新增临时文件必须删除);文档类改动(openspec/ 下文件与 .md 文件,含新建的未跟踪文件)会在提交时自动 commit,不受此限。工具层会拒绝带代码类未提交改动的提交"
|
|
176
179
|
- "即使无 issue 也必须提交 verdict=passed;passed=false 时必须有至少一个归属本维度的 Low+ issue 作为理由"
|
|
177
180
|
- id: verify_cleanup
|
|
178
181
|
agents:
|
package/dsh/cordis.patch.yml
CHANGED
|
@@ -158,7 +158,7 @@
|
|
|
158
158
|
适用于全部审查身份(含架构师身份与 full 模式的维度审查):
|
|
159
159
|
|
|
160
160
|
- 影响代码运行的逻辑类问题:只上报 issue,不直接修改相关代码
|
|
161
|
-
- 反向探针例外:仅任务验证与合并审查身份可用——核验测试对抗力时可临时改坏被测实现,动作固定为四步:改坏被测实现 → 确认对应用例变红 → 原样还原实现 → 全量用例复跑全绿;探针结束后必须原样还原并确认 git 工作区无残留改动(git status
|
|
161
|
+
- 反向探针例外:仅任务验证与合并审查身份可用——核验测试对抗力时可临时改坏被测实现,动作固定为四步:改坏被测实现 → 确认对应用例变红 → 原样还原实现 → 全量用例复跑全绿;探针结束后必须原样还原并确认 git 工作区无残留改动(git status 无改动),探针内容禁止提交,不得保留任何痕迹;残留的代码类未提交改动会被工具层拒绝提交,文档类修正(含新建的 openspec/ 与 .md 文件)照常自动提交;源码文件内的注释修正请自行 commit 后再提交,仅 openspec/ 与 .md 文件由工具自动提交
|
|
162
162
|
- 文档、注释等不影响代码运行的内容:发现不一致/过时问题时直接修正,不为此报 issue;修正情况在提交说明/validation_steps 留痕
|
|
163
163
|
- 工具改进建议仅上报(含规则草案),实施由开发者完成
|
|
164
164
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ifrankwang/openspec-agents",
|
|
3
|
-
"version": "0.151.
|
|
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
|
}
|