@ifrankwang/openspec-agents 0.131.0 → 0.132.0
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/.mcp-server/cli.mjs +130 -59
- package/README.md +2 -2
- package/assets/workflows/task-simple.yaml +3 -1
- package/assets/workflows/task.yaml +3 -1
- package/package.json +1 -1
- package/src/core/tools/lifecycle.ts +44 -13
- package/src/core/tools/schemas.ts +6 -5
- package/src/core/tools/submit.ts +118 -19
- package/src/core/tools/types.ts +6 -4
- package/src/core/types.ts +5 -1
- package/src/core/workflow/engine.ts +14 -0
- package/src/core/workflow/status.ts +30 -0
package/.mcp-server/cli.mjs
CHANGED
|
@@ -29694,6 +29694,7 @@ var REVIEW_DIMENSIONS = [...CODE_DIMENSIONS];
|
|
|
29694
29694
|
var BUILD_PHASE_TARGETS = ["task_analysis", "dev_impl", "review"];
|
|
29695
29695
|
var REVIEW_LAYERS = ["tool", "task", "quality"];
|
|
29696
29696
|
var REVIEW_VERIFY_STEPS = ["verify_tool", "verify_task", "verify_quality"];
|
|
29697
|
+
var SIMPLE_REVIEW_STEPS = ["quality_review"];
|
|
29697
29698
|
|
|
29698
29699
|
// src/core/constants.ts
|
|
29699
29700
|
var STATE_DIR_NAME = "openspec";
|
|
@@ -30119,6 +30120,48 @@ function stepAgentIds(step) {
|
|
|
30119
30120
|
return step.agents.map((a) => a.id);
|
|
30120
30121
|
}
|
|
30121
30122
|
|
|
30123
|
+
// src/core/task-children.ts
|
|
30124
|
+
function taskChildrenOf(item) {
|
|
30125
|
+
return (item.children ?? []).filter((c) => c.type === "task");
|
|
30126
|
+
}
|
|
30127
|
+
function issueChildrenOf(item) {
|
|
30128
|
+
return (item.children ?? []).filter((c) => c.type === "issue");
|
|
30129
|
+
}
|
|
30130
|
+
function taskChildById(item, id) {
|
|
30131
|
+
return item.children.find((c) => c.type === "task" && (c.id === id || c.externalId === id)) ?? null;
|
|
30132
|
+
}
|
|
30133
|
+
function normalizeTaskChildIds(rawIds, item) {
|
|
30134
|
+
const byNumber = new Map;
|
|
30135
|
+
for (const c of taskChildrenOf(item)) {
|
|
30136
|
+
if (c.externalId !== undefined)
|
|
30137
|
+
byNumber.set(c.externalId, c.id);
|
|
30138
|
+
}
|
|
30139
|
+
return rawIds.map((id) => byNumber.get(id) ?? id);
|
|
30140
|
+
}
|
|
30141
|
+
function phaseToTaskStatus(phase, hasRejectReason) {
|
|
30142
|
+
switch (phase) {
|
|
30143
|
+
case "review":
|
|
30144
|
+
return "submitted";
|
|
30145
|
+
case "done":
|
|
30146
|
+
case "cancelled":
|
|
30147
|
+
return "verified";
|
|
30148
|
+
case "todo":
|
|
30149
|
+
return hasRejectReason ? "rejected" : "open";
|
|
30150
|
+
default:
|
|
30151
|
+
return "open";
|
|
30152
|
+
}
|
|
30153
|
+
}
|
|
30154
|
+
function taskListOf(item) {
|
|
30155
|
+
return taskChildrenOf(item).map((c) => ({
|
|
30156
|
+
id: c.id,
|
|
30157
|
+
specTrace: typeof c.metadata["specTrace"] === "string" ? c.metadata["specTrace"] : "",
|
|
30158
|
+
title: c.title,
|
|
30159
|
+
status: phaseToTaskStatus(c.phase, c.metadata["reject_reason"] !== undefined),
|
|
30160
|
+
taskNumber: typeof c.externalId === "string" ? c.externalId : "",
|
|
30161
|
+
rejectReason: typeof c.metadata["reject_reason"] === "string" ? c.metadata["reject_reason"] : null
|
|
30162
|
+
}));
|
|
30163
|
+
}
|
|
30164
|
+
|
|
30122
30165
|
// src/core/workflow/engine.ts
|
|
30123
30166
|
var REVIEW_STEP_TO_LAYER = {
|
|
30124
30167
|
verify_tool: "tool",
|
|
@@ -30501,6 +30544,14 @@ function recommendForItem(item, workflow) {
|
|
|
30501
30544
|
}
|
|
30502
30545
|
const gateBlock = forwardAdvanceBlockReason(item, workflow, current.step);
|
|
30503
30546
|
if (gateBlock) {
|
|
30547
|
+
if (current.phaseName === "review" && taskChildrenOf(item).some((c) => c.phase === "review")) {
|
|
30548
|
+
return {
|
|
30549
|
+
status: "blocked",
|
|
30550
|
+
stepId: current.step.id,
|
|
30551
|
+
agents: stepAgentIds(current.step),
|
|
30552
|
+
blockedReason: `${gateBlock}需该 step 审查者经 opx_agent_submit 补交 verified_tasks(覆盖全部待验证任务)后推进。`
|
|
30553
|
+
};
|
|
30554
|
+
}
|
|
30504
30555
|
return {
|
|
30505
30556
|
status: "blocked",
|
|
30506
30557
|
stepId: current.step.id,
|
|
@@ -30523,48 +30574,6 @@ function recommendForItem(item, workflow) {
|
|
|
30523
30574
|
};
|
|
30524
30575
|
}
|
|
30525
30576
|
|
|
30526
|
-
// src/core/task-children.ts
|
|
30527
|
-
function taskChildrenOf(item) {
|
|
30528
|
-
return (item.children ?? []).filter((c) => c.type === "task");
|
|
30529
|
-
}
|
|
30530
|
-
function issueChildrenOf(item) {
|
|
30531
|
-
return (item.children ?? []).filter((c) => c.type === "issue");
|
|
30532
|
-
}
|
|
30533
|
-
function taskChildById(item, id) {
|
|
30534
|
-
return item.children.find((c) => c.type === "task" && (c.id === id || c.externalId === id)) ?? null;
|
|
30535
|
-
}
|
|
30536
|
-
function normalizeTaskChildIds(rawIds, item) {
|
|
30537
|
-
const byNumber = new Map;
|
|
30538
|
-
for (const c of taskChildrenOf(item)) {
|
|
30539
|
-
if (c.externalId !== undefined)
|
|
30540
|
-
byNumber.set(c.externalId, c.id);
|
|
30541
|
-
}
|
|
30542
|
-
return rawIds.map((id) => byNumber.get(id) ?? id);
|
|
30543
|
-
}
|
|
30544
|
-
function phaseToTaskStatus(phase, hasRejectReason) {
|
|
30545
|
-
switch (phase) {
|
|
30546
|
-
case "review":
|
|
30547
|
-
return "submitted";
|
|
30548
|
-
case "done":
|
|
30549
|
-
case "cancelled":
|
|
30550
|
-
return "verified";
|
|
30551
|
-
case "todo":
|
|
30552
|
-
return hasRejectReason ? "rejected" : "open";
|
|
30553
|
-
default:
|
|
30554
|
-
return "open";
|
|
30555
|
-
}
|
|
30556
|
-
}
|
|
30557
|
-
function taskListOf(item) {
|
|
30558
|
-
return taskChildrenOf(item).map((c) => ({
|
|
30559
|
-
id: c.id,
|
|
30560
|
-
specTrace: typeof c.metadata["specTrace"] === "string" ? c.metadata["specTrace"] : "",
|
|
30561
|
-
title: c.title,
|
|
30562
|
-
status: phaseToTaskStatus(c.phase, c.metadata["reject_reason"] !== undefined),
|
|
30563
|
-
taskNumber: typeof c.externalId === "string" ? c.externalId : "",
|
|
30564
|
-
rejectReason: typeof c.metadata["reject_reason"] === "string" ? c.metadata["reject_reason"] : null
|
|
30565
|
-
}));
|
|
30566
|
-
}
|
|
30567
|
-
|
|
30568
30577
|
// src/core/workflow/submit.ts
|
|
30569
30578
|
function resolveReviewStepForSource(workflow, source) {
|
|
30570
30579
|
for (const phase of workflow.phases) {
|
|
@@ -35704,6 +35713,7 @@ function renderAnalyzeBlockers(item) {
|
|
|
35704
35713
|
}
|
|
35705
35714
|
function renderDeveloperChildren(item, ctxAgent, exemptionCtx) {
|
|
35706
35715
|
const lines = [];
|
|
35716
|
+
lines.push(...renderBlockerTrail(item, BLOCKER_TRAIL_TITLE_DEV));
|
|
35707
35717
|
const toFix = issueChildrenOf(item).filter((c) => isAgentOwnedIssue(c, ctxAgent));
|
|
35708
35718
|
if (toFix.length === 0)
|
|
35709
35719
|
return lines;
|
|
@@ -35844,6 +35854,21 @@ function renderToolAdjudicateOnly(item, rec, ctxAgent, exemptedHits, exemptionCt
|
|
|
35844
35854
|
return lines.join(`
|
|
35845
35855
|
`);
|
|
35846
35856
|
}
|
|
35857
|
+
var BLOCKER_TRAIL_TITLE_REVIEW = "## Blocker 留痕(人工/不可验证任务的申报依据)";
|
|
35858
|
+
var BLOCKER_TRAIL_TITLE_DEV = "## Blocker 留痕(已处理 blocker 的用户确认,凭记录申报完成)";
|
|
35859
|
+
function renderBlockerTrail(item, title) {
|
|
35860
|
+
const resolved = readBlockers(item).filter((b) => b.status === "resolved" && b.userResponse);
|
|
35861
|
+
if (resolved.length === 0)
|
|
35862
|
+
return [];
|
|
35863
|
+
const lines = [title, ""];
|
|
35864
|
+
for (const b of resolved) {
|
|
35865
|
+
lines.push(`- Blocker #${b.id}${b.taskId ? ` | Task #${b.taskId}` : ""} | ${b.category}`);
|
|
35866
|
+
lines.push(` - 描述:${b.description}`);
|
|
35867
|
+
lines.push(` - 用户确认:${b.userResponse}`);
|
|
35868
|
+
}
|
|
35869
|
+
lines.push("");
|
|
35870
|
+
return lines;
|
|
35871
|
+
}
|
|
35847
35872
|
function renderTaskChildren(item, ctxAgent, exemptionCtx) {
|
|
35848
35873
|
const lines = [];
|
|
35849
35874
|
const pendingTasks = readTasks(item).filter((t) => t.status === "submitted");
|
|
@@ -35853,6 +35878,7 @@ function renderTaskChildren(item, ctxAgent, exemptionCtx) {
|
|
|
35853
35878
|
lines.push(renderTaskItem(t));
|
|
35854
35879
|
lines.push("");
|
|
35855
35880
|
}
|
|
35881
|
+
lines.push(...renderBlockerTrail(item, BLOCKER_TRAIL_TITLE_REVIEW));
|
|
35856
35882
|
const issues = issueChildrenOf(item);
|
|
35857
35883
|
const own = issues.filter((c) => isAgentOwnedIssue(c, ctxAgent));
|
|
35858
35884
|
lines.push(...renderChildrenSection("Issue (待复核)", own, exemptionCtx));
|
|
@@ -35881,6 +35907,7 @@ function renderMergedChildren(item, ctxAgent, exemptionCtx) {
|
|
|
35881
35907
|
lines.push(renderTaskItem(t));
|
|
35882
35908
|
lines.push("");
|
|
35883
35909
|
}
|
|
35910
|
+
lines.push(...renderBlockerTrail(item, BLOCKER_TRAIL_TITLE_REVIEW));
|
|
35884
35911
|
const issues = issueChildrenOf(item);
|
|
35885
35912
|
const own = issues.filter((c) => isAgentOwnedIssue(c, ctxAgent));
|
|
35886
35913
|
lines.push(...renderChildrenSection("Issue (待复核)", own, exemptionCtx));
|
|
@@ -36077,6 +36104,9 @@ function applyRecoveryState(item, recovery, parsedTasks, mode) {
|
|
|
36077
36104
|
delete item.tags[key];
|
|
36078
36105
|
}
|
|
36079
36106
|
}
|
|
36107
|
+
if ((recovery?.reset_steps ?? []).includes("quality_review")) {
|
|
36108
|
+
clearStepTags(item, "quality_review");
|
|
36109
|
+
}
|
|
36080
36110
|
item.currentStep = "quality_review";
|
|
36081
36111
|
syncTaskChildren(item, parsedTasks, { defaultStatus: "done" });
|
|
36082
36112
|
return;
|
|
@@ -36146,12 +36176,17 @@ function assertValidRecovery(recovery) {
|
|
|
36146
36176
|
throw new Error("reset_steps 与 review_layer 互斥,不可同时使用。");
|
|
36147
36177
|
}
|
|
36148
36178
|
if (recovery.reset_steps.length === 0) {
|
|
36149
|
-
throw new Error("reset_steps
|
|
36179
|
+
throw new Error("reset_steps 不能为空数组,请至少指定一个审查 step。");
|
|
36150
36180
|
}
|
|
36151
|
-
|
|
36152
|
-
|
|
36153
|
-
|
|
36154
|
-
|
|
36181
|
+
}
|
|
36182
|
+
}
|
|
36183
|
+
function assertValidResetStepValues(recovery, mode) {
|
|
36184
|
+
if (!recovery?.reset_steps?.length)
|
|
36185
|
+
return;
|
|
36186
|
+
const valid = mode === "simple" ? SIMPLE_REVIEW_STEPS : REVIEW_VERIFY_STEPS;
|
|
36187
|
+
for (const stepId of recovery.reset_steps) {
|
|
36188
|
+
if (!valid.includes(stepId)) {
|
|
36189
|
+
throw new Error(`reset_steps 中的 step "${stepId}" 不属于当前模式(${mode})的审查 step,` + `合法值:${valid.join("、")}。传入值:"${stepId}"。`);
|
|
36155
36190
|
}
|
|
36156
36191
|
}
|
|
36157
36192
|
}
|
|
@@ -36234,6 +36269,7 @@ async function initExecute(params, ctx) {
|
|
|
36234
36269
|
state.baseBranch = state.baseBranch || baseBranch;
|
|
36235
36270
|
state.isolationNamespace = state.isolationNamespace || generateIsolationNamespace(state.changeId);
|
|
36236
36271
|
}
|
|
36272
|
+
assertValidResetStepValues(args.recovery, state.mode ?? "full");
|
|
36237
36273
|
for (const group of parsedGroups) {
|
|
36238
36274
|
const isCurrent = group.id === args.task_group_id;
|
|
36239
36275
|
const groupTasks = tasksByGroup.get(group.id) ?? [];
|
|
@@ -36324,6 +36360,11 @@ async function initExecute(params, ctx) {
|
|
|
36324
36360
|
parts.push(`已恢复到 ${args.recovery.phase} 阶段。`);
|
|
36325
36361
|
if (modeSwitchNote)
|
|
36326
36362
|
parts.push(modeSwitchNote);
|
|
36363
|
+
if (args.recovery?.review_layer && (state.mode ?? "full") === "simple") {
|
|
36364
|
+
parts.push(`
|
|
36365
|
+
|
|
36366
|
+
⚠️ simple 模式无 review 子层(仅 quality_review 单层审查),recovery.review_layer 参数未生效。`);
|
|
36367
|
+
}
|
|
36327
36368
|
return parts.join("");
|
|
36328
36369
|
}
|
|
36329
36370
|
async function bindWorktreeRefs(item, worktreePath, branch, baseBranch, opts = {}) {
|
|
@@ -36593,6 +36634,14 @@ function isSupplementOnly(params) {
|
|
|
36593
36634
|
return false;
|
|
36594
36635
|
return (params.fixed_issue_ids?.length ?? 0) === 0 && (params.exempt_issue_ids?.length ?? 0) === 0 && (params.new_children?.length ?? 0) === 0 && (params.verified_tasks?.length ?? 0) === 0 && (params.failed_tasks?.length ?? 0) === 0 && !params.boundary_expansion && (params.validation_steps?.length ?? 0) === 0 && params.test_results === undefined;
|
|
36595
36636
|
}
|
|
36637
|
+
function isTaskVerificationSupplement(params) {
|
|
36638
|
+
if ((params.verified_tasks?.length ?? 0) === 0)
|
|
36639
|
+
return false;
|
|
36640
|
+
return (params.failed_tasks?.length ?? 0) === 0 && (params.new_children?.length ?? 0) === 0 && (params.fixed_issue_ids?.length ?? 0) === 0 && (params.exempt_issue_ids?.length ?? 0) === 0 && (params.exempt_adjudications?.length ?? 0) === 0 && (params.recheck_adjudications?.length ?? 0) === 0 && !params.boundary_expansion && (params.validation_steps?.length ?? 0) === 0 && params.test_results === undefined;
|
|
36641
|
+
}
|
|
36642
|
+
function isTaskVerifySupplementAllowed(item, params, stepId, agent) {
|
|
36643
|
+
return isTaskVerificationSupplement(params) && getStepVerdict(item, stepId, agent) === "passed" && taskChildrenOf(item).some((c) => c.phase === "review");
|
|
36644
|
+
}
|
|
36596
36645
|
function assertRejectedAdjudicationNotPassed(params) {
|
|
36597
36646
|
if (params.verdict !== "passed")
|
|
36598
36647
|
return;
|
|
@@ -36708,6 +36757,8 @@ function clearReviewVerificationTags(item) {
|
|
|
36708
36757
|
}
|
|
36709
36758
|
function resetTasksForBlocker(item) {
|
|
36710
36759
|
for (const child of taskChildrenOf(item)) {
|
|
36760
|
+
if (child.phase === "done")
|
|
36761
|
+
continue;
|
|
36711
36762
|
child.phase = "todo";
|
|
36712
36763
|
delete child.metadata["reject_reason"];
|
|
36713
36764
|
}
|
|
@@ -36739,6 +36790,10 @@ function handleAnalyzeParams(item, params) {
|
|
|
36739
36790
|
}
|
|
36740
36791
|
function handleImplementParams(item, params) {
|
|
36741
36792
|
if (params.blocker) {
|
|
36793
|
+
if (params.blocker_updates?.length) {
|
|
36794
|
+
throw new Error(`不可在同一轮提交中同时上报新 blocker(blocker 参数)与更新旧 blocker(blocker_updates)。
|
|
36795
|
+
` + "请分两轮处理:先以 verdict=failed + blocker 上报新阻塞;用户确认后,下一轮再携带 blocker_updates 记录答复并重新提交。");
|
|
36796
|
+
}
|
|
36742
36797
|
if (params.verdict !== "failed") {
|
|
36743
36798
|
throw new Error("blocker 参数仅支持 verdict=failed 提交(on_fail 回退 analyze)。");
|
|
36744
36799
|
}
|
|
@@ -36746,6 +36801,20 @@ function handleImplementParams(item, params) {
|
|
|
36746
36801
|
resetTasksForBlocker(item);
|
|
36747
36802
|
return;
|
|
36748
36803
|
}
|
|
36804
|
+
if (params.blocker_updates?.length) {
|
|
36805
|
+
for (const u of params.blocker_updates) {
|
|
36806
|
+
const blocker = itemBlockers(item).find((b) => b.id === u.blocker_id);
|
|
36807
|
+
if (!blocker)
|
|
36808
|
+
throw new Error(`blocker "${u.blocker_id}" 不存在于 metadata.blockers 中。`);
|
|
36809
|
+
if (blocker.status !== "awaiting_user")
|
|
36810
|
+
throw new Error(`blocker "${u.blocker_id}" 状态不是 awaiting_user,无法更新。`);
|
|
36811
|
+
blocker.userResponse = u.user_response;
|
|
36812
|
+
blocker.status = "resolved";
|
|
36813
|
+
}
|
|
36814
|
+
}
|
|
36815
|
+
if (params.verdict === "passed" && itemBlockers(item).some((b) => b.status !== "resolved")) {
|
|
36816
|
+
throw new Error("存在未解决的 blocker,无法以 passed 提交 implement step。请先通过 blocker_updates 处理全部 blocker。");
|
|
36817
|
+
}
|
|
36749
36818
|
const tasks = taskListOf(item);
|
|
36750
36819
|
const completed = normalizeTaskChildIds(params.completed_task_ids ?? [], item);
|
|
36751
36820
|
const validIds = new Set(tasks.map((t) => t.id));
|
|
@@ -36767,7 +36836,7 @@ function handleImplementParams(item, params) {
|
|
|
36767
36836
|
throw new Error(`以下 task 处于 open/rejected 状态且未在 completed_task_ids 中:
|
|
36768
36837
|
` + remaining.map((t) => `- #${t.id}(${t.status}) ${t.title}`).join(`
|
|
36769
36838
|
`) + `
|
|
36770
|
-
|
|
36839
|
+
completed_task_ids 仅申报已完成可供验证的任务;无法完成或人工执行的任务不得虚报,` + `应以 verdict=failed + blocker 上报(task_id 指向该任务),经用户确认后凭 blocker 留痕重新申报。`);
|
|
36771
36840
|
}
|
|
36772
36841
|
if (params.self_check_results) {
|
|
36773
36842
|
item.metadata["self_check_results"] = params.self_check_results;
|
|
@@ -36805,7 +36874,7 @@ function assertFailedHasReason(item, params, newChildren, stepId, agent) {
|
|
|
36805
36874
|
hasReason = hasNewBlocking || existingBlocking.length > 0;
|
|
36806
36875
|
} else if (stepId === "quality_review") {
|
|
36807
36876
|
layerName = "AI 审查层";
|
|
36808
|
-
hasReason = hasNewBlocking || existingBlocking.some((c) => resolveChildIssueFields(c).sourcePhase === "quality");
|
|
36877
|
+
hasReason = (params.failed_tasks?.length ?? 0) > 0 || hasNewBlocking || existingBlocking.some((c) => resolveChildIssueFields(c).sourcePhase === "quality");
|
|
36809
36878
|
} else {
|
|
36810
36879
|
const dimension = agentToReviewDimension(agent);
|
|
36811
36880
|
layerName = dimension ? `AI 审查层(${dimension})` : "AI 审查层";
|
|
@@ -36839,7 +36908,8 @@ function handleReviewParams(item, params, newChildren, stepId, opts) {
|
|
|
36839
36908
|
` + `请按已加载质量门类 skill 的必做清单逐项执行并逐项申报结果(completed=true);` + `确实无法执行的必做项须以结构化 skip_reason 申报降级理由` + `(格式:${SKIP_REASON_FORMAT})。` + `低成本必做项必须实跑后申报;核验申报仅限 workflow 指令白名单限定的高成本必做项。`);
|
|
36840
36909
|
}
|
|
36841
36910
|
}
|
|
36842
|
-
|
|
36911
|
+
const isTaskVerifyStep = stepId === "verify_task" || stepId === "quality_review";
|
|
36912
|
+
if (params.verified_tasks?.length || params.failed_tasks?.length || params.verdict === "passed" && isTaskVerifyStep) {
|
|
36843
36913
|
const tasks = taskListOf(item);
|
|
36844
36914
|
const validIds = new Set(tasks.map((t) => t.id));
|
|
36845
36915
|
const verified = normalizeTaskChildIds(params.verified_tasks ?? [], item);
|
|
@@ -36862,7 +36932,8 @@ function handleReviewParams(item, params, newChildren, stepId, opts) {
|
|
|
36862
36932
|
if (uncovered.length > 0) {
|
|
36863
36933
|
throw new Error(`以下 submitted task 未被 verified_tasks 或 failed_tasks 覆盖:
|
|
36864
36934
|
` + uncovered.map((t) => `- #${t.id} ${t.title}`).join(`
|
|
36865
|
-
`)
|
|
36935
|
+
`) + `
|
|
36936
|
+
verified_tasks 用于逐项确认已完成的任务;验证未通过的任务须以 failed_tasks 逐项驳回并设 verdict=failed 回退;` + `人工执行或环境不可验证的任务不得虚报确认,应以 verdict=failed + failed_tasks 驳回,` + `由开发者经 verdict=failed + blocker(task_id 指向该任务)上报、用户确认留痕后凭记录重新申报完成。`);
|
|
36866
36937
|
}
|
|
36867
36938
|
for (const id of verified) {
|
|
36868
36939
|
const child = taskChildById(item, id);
|
|
@@ -36939,7 +37010,7 @@ async function agentSubmitExecute(params, ctx) {
|
|
|
36939
37010
|
}
|
|
36940
37011
|
}
|
|
36941
37012
|
const guardStepPhase = workflow.stepMap.get(params.step_id)?.phase.name;
|
|
36942
|
-
if (guardStepPhase === "review" && getStepVerdict(item, params.step_id, ctx.agent) === "passed" && !isSupplementOnly(params)) {
|
|
37013
|
+
if (guardStepPhase === "review" && getStepVerdict(item, params.step_id, ctx.agent) === "passed" && !isSupplementOnly(params) && !isTaskVerifySupplementAllowed(item, params, params.step_id, ctx.agent)) {
|
|
36943
37014
|
throw new Error(`重复提交守卫:agent "${ctx.agent}" 已在 step "${params.step_id}" 以 passed 通过,不允许重复提交。`);
|
|
36944
37015
|
}
|
|
36945
37016
|
assertRejectedAdjudicationNotPassed(params);
|
|
@@ -37009,7 +37080,7 @@ async function agentSubmitExecute(params, ctx) {
|
|
|
37009
37080
|
const agentCfg = stepCfg.agents.find((a) => a.id === ctx.agent);
|
|
37010
37081
|
handleReviewParams(item, params, newChildren, params.step_id, {
|
|
37011
37082
|
capabilityTags: agentCfg?.capability_tags ?? [],
|
|
37012
|
-
skipMustDoGate: isSupplementOnly(params)
|
|
37083
|
+
skipMustDoGate: isSupplementOnly(params) || isTaskVerifySupplementAllowed(item, params, params.step_id, ctx.agent)
|
|
37013
37084
|
});
|
|
37014
37085
|
} else if (stepPhase === "todo") {
|
|
37015
37086
|
handleAnalyzeParams(item, params);
|
|
@@ -37334,8 +37405,8 @@ var recoverySchema = {
|
|
|
37334
37405
|
},
|
|
37335
37406
|
reset_steps: {
|
|
37336
37407
|
type: "array",
|
|
37337
|
-
description: "
|
|
37338
|
-
items: { type: "string", enum: ["verify_tool", "verify_task", "verify_quality"] }
|
|
37408
|
+
description: "重置指定审查 step 的通过标记为 pending(仅 phase=review 时有效,与 review_layer 互斥)。用于已 passed 但被本层遗漏复核/裁定/任务验证阻塞的 review step 强制重新审查;恢复后 currentStep 落在第一个未全部通过的审查 step,可能早于被重置的 step。值按模式生效:full 模式合法值为 verify_tool/verify_task/verify_quality,simple 模式合法值为 quality_review;传入不属于当前模式的值会在运行时报错。",
|
|
37409
|
+
items: { type: "string", enum: ["verify_tool", "verify_task", "verify_quality", "quality_review"] }
|
|
37339
37410
|
}
|
|
37340
37411
|
},
|
|
37341
37412
|
required: ["phase"],
|
|
@@ -37447,7 +37518,7 @@ var agentSubmitSchema = {
|
|
|
37447
37518
|
blockers: { type: "array", description: "analyze step:新增 blocker 列表", items: blockerItem },
|
|
37448
37519
|
blocker_updates: {
|
|
37449
37520
|
type: "array",
|
|
37450
|
-
description: "analyze step:按 blocker_id 置 resolved
|
|
37521
|
+
description: "analyze / implement step:按 blocker_id 置 resolved 并记录用户答复(implement 下人工执行任务凭留痕重新申报的依据)",
|
|
37451
37522
|
items: {
|
|
37452
37523
|
type: "object",
|
|
37453
37524
|
properties: {
|
|
@@ -37483,12 +37554,12 @@ var agentSubmitSchema = {
|
|
|
37483
37554
|
},
|
|
37484
37555
|
verified_tasks: {
|
|
37485
37556
|
type: "array",
|
|
37486
|
-
description: "verify_task step
|
|
37557
|
+
description: "verify_task / quality_review step(任务验证归属层):逐项确认验证通过的 task id;passed 提交时 submitted task 必须被 verified_tasks/failed_tasks 全覆盖(工具强制门禁),漏带会被拒绝",
|
|
37487
37558
|
items: { type: "string" }
|
|
37488
37559
|
},
|
|
37489
37560
|
failed_tasks: {
|
|
37490
37561
|
type: "array",
|
|
37491
|
-
description: "verify_task step:验证失败的 task 列表(含原因)",
|
|
37562
|
+
description: "verify_task / quality_review step:验证失败的 task 列表(含原因)",
|
|
37492
37563
|
items: taskVerifyItem
|
|
37493
37564
|
}
|
|
37494
37565
|
},
|
|
@@ -37613,7 +37684,7 @@ async function ensureDefaultUnattended(args, ctx) {
|
|
|
37613
37684
|
}
|
|
37614
37685
|
} catch {}
|
|
37615
37686
|
}
|
|
37616
|
-
var PKG_VERSION = "0.
|
|
37687
|
+
var PKG_VERSION = "0.132.0";
|
|
37617
37688
|
function buildMcpServer(worktree, opts = {}) {
|
|
37618
37689
|
const mcp = new McpServer({ name: "openspec-agents", version: PKG_VERSION });
|
|
37619
37690
|
for (const [name, spec] of Object.entries(TOOL_SPECS)) {
|
package/README.md
CHANGED
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
- **多工具支持**:Claude Code、Codex、ZCode、DeepSeek Harness(DSH)、OpenCode 均可原生接入。
|
|
13
13
|
- **规范驱动**:直接消费 OpenSpec 变更规范(proposal / design / tasks / specs),流程与规范一一对应。
|
|
14
14
|
- **智能体团队**:不再是单个 AI 从头写到尾,而是「实现者 + 审查者 + 质量把关者」分工协作,结果更稳。
|
|
15
|
-
-
|
|
15
|
+
- **质量门禁**:实现之后经过工具检查、任务验证与多维度质量审查;发现的问题自动回退修复,也可按规则豁免并留痕。任务验证层(任务审查 / 合并审查)以通过(passed)提交时必须逐项确认全部已申报任务(verified_tasks 全覆盖为工具强制门禁);已通过但漏带确认导致阻塞时,审查者可补交确认结果解除阻塞。simple 模式下验证按性质分流——开发者执行确定性检查并申报,审查者分级复验(低成本项实跑、高成本项核验申报加抽样重放)并专注对抗性判断。
|
|
16
16
|
- **隔离执行**:每个变更在独立的环境中实施,互不干扰;变更完成后合并并清理。
|
|
17
|
-
-
|
|
17
|
+
- **过程可审计**:进度、结论、豁免与恢复点全程记录,随时可以接着上次的进度继续。人工执行或环境不可验证的任务经 blocker 上报、用户确认留痕(blocker_updates)后凭记录申报完成;恢复参数 reset_steps 按流程模式(full / simple)校验各自的审查步骤值域后生效。
|
|
18
18
|
|
|
19
19
|
## 快速开始
|
|
20
20
|
|
|
@@ -46,7 +46,8 @@ phases:
|
|
|
46
46
|
- "修复完成的 issue 经 fixed_issue_ids 上报后进入待复核(review)状态,终态由报源 reviewer 复核裁定;不可修 issue 经 exempt_issue_ids 申请豁免(申请后进入待裁定(review)状态,由报源 reviewer 在后续审查轮次裁定,被驳回的 issue 回待修复);Info 级 issue 不阻塞提交,无需申请豁免;全部 blocking issue 均已上报修复或申请豁免后才可 passed 提交推进"
|
|
47
47
|
- "确定性质量工具检查出的问题不得以压制手段(压制注解/注释抑制/规则排除配置/平台状态标注/构建跳过开关等)静默规避:确需压制时,必须在压制处代码注释说明清楚理由(commit message 可补充),并经 exempt_issue_ids 申请豁免由报源 reviewer 裁定;不得以 fixed_issue_ids 冒充修复;被驳回后须移除压制并真实修复"
|
|
48
48
|
- "遇外部依赖、凭证、真实输入或必须 stub/降级/跳过验收才能继续时,以 verdict=failed 提交 blocker(含 source_role、task_id、category、description、evidence、attempted_actions、options)"
|
|
49
|
-
- "
|
|
49
|
+
- "blocker 上报后经 question 向用户确认处理方式,确认结果经 blocker_updates(blocker_id + user_response)上报置 resolved;人工执行任务凭用户确认留痕列入 completed_task_ids 重新提交(申报依据为 blocker 记录,不得凭空申报)"
|
|
50
|
+
- "提交 opx_agent_submit:passed 提交前须将全部已完成(含被驳回后修复的)子任务列入 completed_task_ids(数字 id 如 1、2,或任务编号如 1.1)申报,全部子任务已验证时可省略;无法完成或人工执行的任务不得虚报,应经 verdict=failed + blocker 上报后凭留痕申报;不可修 issue 申请豁免(exempt_issue_ids)"
|
|
50
51
|
constraints:
|
|
51
52
|
- "禁用 edit/write 修改 `{{worktree_path}}/openspec/changes/` 下任何文档(spec/design/tasks/clarify);唯一例外:所提交 fixed_issue_ids 中某 issue 的 file 字段指向该目录下文档时,仅允许按该 issue 的要求修改对应文档,不得扩大范围;本约束为提示词级约束,工具层不校验实际改动的文件"
|
|
52
53
|
- "执行边界默认整个 worktree(无 analyze 环节,不生成边界白名单)"
|
|
@@ -83,6 +84,7 @@ phases:
|
|
|
83
84
|
- "对视图「待裁定是否可豁免」区块中的豁免申请经 exempt_adjudications 裁定(dismissed/rejected)"
|
|
84
85
|
- "审查变更 diff 与 git log,识别变更中新增的压制手段(压制注解/注释抑制/规则排除配置/平台状态标注/构建跳过开关等):视为豁免申请,核对 dev 是否已在压制处代码注释或 commit message 说明理由并申报豁免;未申报或理由不成立 → 报 issue(file=压制位置、line=压制行、rule=被压制规则名,severity 至少 Low,description/suggestion 写明判定门槛与理由),要求移除压制真实修复或补申报;理由成立 → 经 exempt_adjudications 裁定 dismissed"
|
|
85
86
|
- "有 task 未通过时必须 verdict=failed 并经 failed_tasks 上报(task_id + reason);passed 时不允许提供 failed_tasks"
|
|
87
|
+
- "对标注人工执行/环境不可验证的任务,核验视图 Blocker 留痕区块(resolved 且 user_response 记录用户确认)后经 verified_tasks 确认并在 validation_steps 留痕;无留痕且无法自行验证 → verdict=failed 经 failed_tasks 驳回并说明理由"
|
|
86
88
|
- "报静态分析类 issue 时,rule 字段必填官方规则全名(命名格式遵循已加载的质量门类 skill 的规则导航索引),漏填将导致项目级豁免清单无法生效"
|
|
87
89
|
- "报静态分析类 issue 时,description 必须含:①复现命令(非静默形态,含必要开关);②实测数据(命中类清单/计数);③规则默认阈值与当前项目配置对比(引用可改的配置文件路径)"
|
|
88
90
|
- "按 opx_status 操作指引完成全部验证项,提交前将每项验证结果与证据填入 validation_steps 参数(未执行的项须填 skip_reason 注明原因),不得省略任何一项"
|
|
@@ -77,7 +77,8 @@ phases:
|
|
|
77
77
|
- "修复完成的 issue 经 fixed_issue_ids 上报后进入待复核(review)状态,终态由报源 reviewer 复核裁定;不可修 issue 经 exempt_issue_ids 申请豁免(申请后进入待裁定(review)状态,由报源 reviewer 在后续审查轮次裁定,被驳回的 issue 回待修复);Info 级 issue 不阻塞提交,无需申请豁免;全部 blocking issue 均已上报修复或申请豁免后才可 passed 提交推进"
|
|
78
78
|
- "确定性质量工具检查出的问题不得以压制手段(压制注解/注释抑制/规则排除配置/平台状态标注/构建跳过开关等)静默规避:确需压制时,必须在压制处代码注释说明清楚理由(commit message 可补充),并经 exempt_issue_ids 申请豁免由报源 reviewer 裁定;不得以 fixed_issue_ids 冒充修复;被驳回后须移除压制并真实修复"
|
|
79
79
|
- "遇外部依赖、凭证、真实输入或必须 stub/降级/跳过验收才能继续时,以 verdict=failed 提交 blocker(含 source_role、task_id、category、description、evidence、attempted_actions、options)"
|
|
80
|
-
- "
|
|
80
|
+
- "blocker 上报后经 question 向用户确认处理方式,确认结果经 blocker_updates(blocker_id + user_response)上报置 resolved;人工执行任务凭用户确认留痕列入 completed_task_ids 重新提交(申报依据为 blocker 记录,不得凭空申报)"
|
|
81
|
+
- "提交 opx_agent_submit:passed 提交前须将全部已完成(含被驳回后修复的)子任务列入 completed_task_ids(数字 id 如 1、2,或任务编号如 1.1)申报,全部子任务已验证时可省略;无法完成或人工执行的任务不得虚报,应经 verdict=failed + blocker 上报后凭留痕申报;不可修 issue 申请豁免(exempt_issue_ids)"
|
|
81
82
|
constraints:
|
|
82
83
|
- "禁用 edit/write 修改 `{{worktree_path}}/openspec/changes/` 下任何文档(spec/design/tasks/clarify);唯一例外:所提交 fixed_issue_ids 中某 issue 的 file 字段指向该目录下文档时,仅允许按该 issue 的要求修改对应文档,不得扩大范围;本约束为提示词级约束,工具层不校验实际改动的文件"
|
|
83
84
|
- "允许变更目录范围:{{allowed_directories}}"
|
|
@@ -130,6 +131,7 @@ phases:
|
|
|
130
131
|
- "本次变更仅含测试代码(判定:视图「变更范围」文件清单全部命中已加载技术栈 skill 定义的测试代码路径,且无生产代码、接口契约、构建与测试配置等非测试文件)时,豁免②启动服务并检查健康与③独立执行全量 API 测试,仅验证①④;豁免时 validation_steps 中②③置 completed=false,并以结构化 skip_reason 申报(格式:{\"item\":\"<对应验证项>\",\"category\":\"<降级类别>\",\"adjudication\":\"user_response|unattended_auto|env_unavailable\",\"note\":\"<说明>\"}),note 注明「本次变更仅含测试代码,豁免启动服务与 API 测试」及判定依据;⑤⑥的实现核验在纯测试代码变更场景无对象,其断言证据核验并入④测试代码质量"
|
|
131
132
|
- "读取 design.md 的 API 定义(请求/响应结构、数据模型)与 specs/ 下相关 spec.md(需求细节、验收标准与数据产出要求),用于准备测试数据与对照 API 合约"
|
|
132
133
|
- "有 task 未通过时必须 verdict=failed 并经 failed_tasks 上报(task_id + reason);passed 时不允许提供 failed_tasks"
|
|
134
|
+
- "对标注人工执行/环境不可验证的任务,核验视图 Blocker 留痕区块(resolved 且 user_response 记录用户确认)后经 verified_tasks 确认并在 validation_steps 留痕;无留痕且无法自行验证 → verdict=failed 经 failed_tasks 驳回并说明理由"
|
|
133
135
|
- "当发现修复需要调整 openspec/changes/ 下文档时,所报 issue 的 file 字段必须指向目标文档"
|
|
134
136
|
- "报 issue 时必须显式声明归因维度 dimension(style/architecture/performance/security/maintainability):本 reviewer 不归属任一质量维度,dimension 即该 issue 归属的目标质量维度"
|
|
135
137
|
- "复核本层已修复待复核(review 态)issue:通过置 done,不通过驳回(附驳回原因,issue 回待修复并累计修复未过次数)"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ifrankwang/openspec-agents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.132.0",
|
|
4
4
|
"description": "OpenSpec change orchestration plugin for AI coding agents (Claude Code, Codex, ZCode, DeepSeek Harness, OpenCode)",
|
|
5
5
|
"author": "ifrankwang",
|
|
6
6
|
"license": "MIT",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "path"
|
|
2
2
|
import { rmdir } from "node:fs/promises"
|
|
3
3
|
import type { OrchestrateState, TaskItem, TaskStatus, WorkflowMode } from "../types.ts"
|
|
4
|
-
import { BUILD_PHASE_TARGETS, REVIEW_LAYERS, REVIEW_VERIFY_STEPS } from "../types.ts"
|
|
4
|
+
import { BUILD_PHASE_TARGETS, REVIEW_LAYERS, REVIEW_VERIFY_STEPS, SIMPLE_REVIEW_STEPS } from "../types.ts"
|
|
5
5
|
import { agentToReviewLayer } from "../constants.ts"
|
|
6
6
|
import { runGit, runGitChecked, getCurrentBranch, getMergeBase, isWorktreeClean, markTaskGroupCheckboxesComplete, mergeBranchToTarget, discoverDiskWorktrees, detectMainRepoPollution, detectChanges, removeTaskGroupWorktree, type DetectChangesResult } from "../git.ts"
|
|
7
7
|
import { readStateByWorktree, readStateByChangeId, writeState, writeContextToWorktree } from "../state.ts"
|
|
@@ -135,8 +135,9 @@ function firstUnpassedReviewStep(item: WorkItem, workflow: LoadedWorkflow): stri
|
|
|
135
135
|
* simple 分支(mode === "simple",3.2):无 analyze / verify_* step,task_analysis / dev_impl 均落
|
|
136
136
|
* in_progress/implement(task_analysis 重置 task children 全 todo,dev_impl 保留既有进度);review 落
|
|
137
137
|
* review/quality_review——implement 置 passed、quality_review 的 failed tag 删除回 pending(passed 保留)、
|
|
138
|
-
* task children 缺省 done;reset_steps
|
|
139
|
-
*
|
|
138
|
+
* task children 缺省 done;reset_steps 按模式生效(simple 仅接受 quality_review,含该值时清空
|
|
139
|
+
* quality_review 全部 tags——passed 也清,强制重审正是该参数的目的,清空后引擎重派该 step 审查者);
|
|
140
|
+
* review_layer 在 simple 下无对应子层(仅 quality_review 单层),接受但不生效,init 返回体输出警告。
|
|
140
141
|
*/
|
|
141
142
|
function applyRecoveryState(
|
|
142
143
|
item: WorkItem,
|
|
@@ -176,7 +177,12 @@ function applyRecoveryState(
|
|
|
176
177
|
if (item.tags[key] !== "passed") delete item.tags[key]
|
|
177
178
|
}
|
|
178
179
|
}
|
|
179
|
-
// reset_steps
|
|
180
|
+
// reset_steps(simple 模式仅 quality_review):含该值时清空该 step 全部 tags(passed 也清——
|
|
181
|
+
// 强制重审正是该参数的目的,含「已通过但任务验证/裁定遗漏」场景),tag 清空后引擎重派该 step 审查者
|
|
182
|
+
if (((recovery?.reset_steps ?? []) as string[]).includes("quality_review")) {
|
|
183
|
+
clearStepTags(item, "quality_review")
|
|
184
|
+
}
|
|
185
|
+
// review_layer 在 simple 下无对应子层,接受但不生效(init 返回体对该组合输出警告)
|
|
180
186
|
item.currentStep = "quality_review"
|
|
181
187
|
syncTaskChildren(item, parsedTasks, { defaultStatus: "done" })
|
|
182
188
|
return
|
|
@@ -241,8 +247,10 @@ function applyRecoveryState(
|
|
|
241
247
|
* - phase 必须为合法恢复阶段(task_analysis/dev_impl/review),缺失/非法即抛错并列出合法值;
|
|
242
248
|
* - review_layer 必须为合法子层(tool/task/quality),非法即抛错;
|
|
243
249
|
* - review_layer 仅当 phase=review 时允许存在,其余 phase 组合复用既有组合错误消息;
|
|
244
|
-
* - reset_steps
|
|
245
|
-
*
|
|
250
|
+
* - reset_steps 必须为非空数组,仅当 phase=review 时允许存在,且与 review_layer 互斥
|
|
251
|
+
* (二者都操纵哪些 review step 通过)。reset_steps 的 step 值域按模式生效(full 仅
|
|
252
|
+
* verify_tool/verify_task/verify_quality,simple 仅 quality_review),由
|
|
253
|
+
* assertValidResetStepValues 在 state 读取后做模式感知校验——本函数仅做与模式无关的组合校验。
|
|
246
254
|
*/
|
|
247
255
|
function assertValidRecovery(recovery: InitParams["recovery"]): void {
|
|
248
256
|
if (recovery === undefined) return
|
|
@@ -267,14 +275,28 @@ function assertValidRecovery(recovery: InitParams["recovery"]): void {
|
|
|
267
275
|
throw new Error("reset_steps 与 review_layer 互斥,不可同时使用。")
|
|
268
276
|
}
|
|
269
277
|
if (recovery.reset_steps.length === 0) {
|
|
270
|
-
throw new Error("reset_steps
|
|
278
|
+
throw new Error("reset_steps 不能为空数组,请至少指定一个审查 step。")
|
|
271
279
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* reset_steps 值域的模式感知校验(state 读取/固化后调用——有效模式 = state.mode ?? "full"):
|
|
285
|
+
* - full 模式仅接受 verify_tool/verify_task/verify_quality;
|
|
286
|
+
* - simple 模式仅接受 quality_review;
|
|
287
|
+
* 跨模式值抛错并列出当前模式的合法值。phase/review_layer/组合校验在 assertValidRecovery 前置
|
|
288
|
+
* (错误早于任何状态变更且不落盘),值域校验依赖 state.mode 故后置到 state 读取之后——
|
|
289
|
+
* state 读取不是状态变更,该原则不破坏。
|
|
290
|
+
*/
|
|
291
|
+
function assertValidResetStepValues(recovery: InitParams["recovery"], mode: WorkflowMode): void {
|
|
292
|
+
if (!recovery?.reset_steps?.length) return
|
|
293
|
+
const valid = mode === "simple" ? SIMPLE_REVIEW_STEPS : REVIEW_VERIFY_STEPS
|
|
294
|
+
for (const stepId of recovery.reset_steps) {
|
|
295
|
+
if (!(valid as readonly string[]).includes(stepId)) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
`reset_steps 中的 step "${stepId}" 不属于当前模式(${mode})的审查 step,` +
|
|
298
|
+
`合法值:${valid.join("、")}。传入值:"${stepId}"。`
|
|
299
|
+
)
|
|
278
300
|
}
|
|
279
301
|
}
|
|
280
302
|
}
|
|
@@ -389,6 +411,10 @@ export async function initExecute(params: InitParams, ctx: ToolContext): Promise
|
|
|
389
411
|
state.isolationNamespace = state.isolationNamespace || generateIsolationNamespace(state.changeId)
|
|
390
412
|
}
|
|
391
413
|
|
|
414
|
+
// reset_steps 值域的模式感知校验:位于 state 读取/固化之后(有效模式 = state.mode ?? "full",
|
|
415
|
+
// 与 applyRecoveryState 消费的生效模式一致)、一切状态变更与落盘之前,跨模式值报错不落盘
|
|
416
|
+
assertValidResetStepValues(args.recovery, state.mode ?? "full")
|
|
417
|
+
|
|
392
418
|
// 按 tasks.md 构造全部任务组的 task WorkItem(单轨:workItems 为唯一事实源)
|
|
393
419
|
for (const group of parsedGroups) {
|
|
394
420
|
const isCurrent = group.id === args.task_group_id
|
|
@@ -491,6 +517,11 @@ export async function initExecute(params: InitParams, ctx: ToolContext): Promise
|
|
|
491
517
|
const parts = ["编排会话已初始化。"]
|
|
492
518
|
if (args.recovery) parts.push(`已恢复到 ${args.recovery.phase} 阶段。`)
|
|
493
519
|
if (modeSwitchNote) parts.push(modeSwitchNote)
|
|
520
|
+
// review_layer 在 simple 模式下无对应子层(仅 quality_review 单层审查),该参数接受但不生效:
|
|
521
|
+
// 返回体显式警告,避免调用方误以为已按子层恢复
|
|
522
|
+
if (args.recovery?.review_layer && (state.mode ?? "full") === "simple") {
|
|
523
|
+
parts.push("\n\n⚠️ simple 模式无 review 子层(仅 quality_review 单层审查),recovery.review_layer 参数未生效。")
|
|
524
|
+
}
|
|
494
525
|
return parts.join("")
|
|
495
526
|
}
|
|
496
527
|
|
|
@@ -191,8 +191,8 @@ export const recoverySchema: JSONSchema = {
|
|
|
191
191
|
reset_steps: {
|
|
192
192
|
type: "array",
|
|
193
193
|
description:
|
|
194
|
-
"
|
|
195
|
-
items: { type: "string", enum: ["verify_tool", "verify_task", "verify_quality"] },
|
|
194
|
+
"重置指定审查 step 的通过标记为 pending(仅 phase=review 时有效,与 review_layer 互斥)。用于已 passed 但被本层遗漏复核/裁定/任务验证阻塞的 review step 强制重新审查;恢复后 currentStep 落在第一个未全部通过的审查 step,可能早于被重置的 step。值按模式生效:full 模式合法值为 verify_tool/verify_task/verify_quality,simple 模式合法值为 quality_review;传入不属于当前模式的值会在运行时报错。",
|
|
195
|
+
items: { type: "string", enum: ["verify_tool", "verify_task", "verify_quality", "quality_review"] },
|
|
196
196
|
},
|
|
197
197
|
},
|
|
198
198
|
required: ["phase"],
|
|
@@ -312,7 +312,7 @@ export const agentSubmitSchema: JSONSchema = {
|
|
|
312
312
|
blockers: { type: "array", description: "analyze step:新增 blocker 列表", items: blockerItem },
|
|
313
313
|
blocker_updates: {
|
|
314
314
|
type: "array",
|
|
315
|
-
description: "analyze step:按 blocker_id 置 resolved
|
|
315
|
+
description: "analyze / implement step:按 blocker_id 置 resolved 并记录用户答复(implement 下人工执行任务凭留痕重新申报的依据)",
|
|
316
316
|
items: {
|
|
317
317
|
type: "object",
|
|
318
318
|
properties: {
|
|
@@ -349,12 +349,13 @@ export const agentSubmitSchema: JSONSchema = {
|
|
|
349
349
|
},
|
|
350
350
|
verified_tasks: {
|
|
351
351
|
type: "array",
|
|
352
|
-
description:
|
|
352
|
+
description:
|
|
353
|
+
"verify_task / quality_review step(任务验证归属层):逐项确认验证通过的 task id;passed 提交时 submitted task 必须被 verified_tasks/failed_tasks 全覆盖(工具强制门禁),漏带会被拒绝",
|
|
353
354
|
items: { type: "string" },
|
|
354
355
|
},
|
|
355
356
|
failed_tasks: {
|
|
356
357
|
type: "array",
|
|
357
|
-
description: "verify_task step:验证失败的 task 列表(含原因)",
|
|
358
|
+
description: "verify_task / quality_review step:验证失败的 task 列表(含原因)",
|
|
358
359
|
items: taskVerifyItem,
|
|
359
360
|
},
|
|
360
361
|
},
|
package/src/core/tools/submit.ts
CHANGED
|
@@ -57,6 +57,49 @@ function isSupplementOnly(params: AgentSubmitParams): boolean {
|
|
|
57
57
|
)
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* 判定本次提交是否「仅携带任务验证补交」(verified_tasks 非空,其余实质参数全空)。
|
|
62
|
+
* 供重复提交守卫的补交豁免:任务验证归属 step(verify_task/quality_review)上次 passed 提交
|
|
63
|
+
* 漏带 verified_tasks 导致 task children 卡 review 态(done 收口被拦截,见 engine
|
|
64
|
+
* recommendForItem 的 gateBlock 补交推导)时,允许仅补带 verified_tasks 重提解除阻塞;
|
|
65
|
+
* 空提交或携带其他实质参数的重复提交仍被守卫拒绝。放行须叠加「存在 review 态 task child」
|
|
66
|
+
* 条件(与引擎推导条件同源),防止放行面过宽。
|
|
67
|
+
*/
|
|
68
|
+
function isTaskVerificationSupplement(params: AgentSubmitParams): boolean {
|
|
69
|
+
if ((params.verified_tasks?.length ?? 0) === 0) return false
|
|
70
|
+
return (
|
|
71
|
+
(params.failed_tasks?.length ?? 0) === 0 &&
|
|
72
|
+
(params.new_children?.length ?? 0) === 0 &&
|
|
73
|
+
(params.fixed_issue_ids?.length ?? 0) === 0 &&
|
|
74
|
+
(params.exempt_issue_ids?.length ?? 0) === 0 &&
|
|
75
|
+
(params.exempt_adjudications?.length ?? 0) === 0 &&
|
|
76
|
+
(params.recheck_adjudications?.length ?? 0) === 0 &&
|
|
77
|
+
!params.boundary_expansion &&
|
|
78
|
+
(params.validation_steps?.length ?? 0) === 0 &&
|
|
79
|
+
params.test_results === undefined
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 任务验证补交放行判定(重复提交守卫的豁免与必做清单门禁的跳过共用同源条件):仅携带
|
|
85
|
+
* verified_tasks 的严格补交形态,且该 agent 在该 step 已 passed、item 存在 review 态
|
|
86
|
+
* (submitted)task child——与守卫放宽、引擎 gateBlock 补交推导条件同源。
|
|
87
|
+
* 必做门禁的跳过绑定本条件(而非仅形态判定):首次提交(step 尚未 passed)不得借补交形态
|
|
88
|
+
* 跳过必做清单覆盖度校验——补交跳过的理由是完整审查在首次 passed 时已做过。
|
|
89
|
+
*/
|
|
90
|
+
function isTaskVerifySupplementAllowed(
|
|
91
|
+
item: WorkItem,
|
|
92
|
+
params: AgentSubmitParams,
|
|
93
|
+
stepId: string,
|
|
94
|
+
agent: string,
|
|
95
|
+
): boolean {
|
|
96
|
+
return (
|
|
97
|
+
isTaskVerificationSupplement(params) &&
|
|
98
|
+
getStepVerdict(item, stepId, agent) === "passed" &&
|
|
99
|
+
taskChildrenOf(item).some((c) => c.phase === "review")
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
60
103
|
/**
|
|
61
104
|
* 统一裁定一致性守卫:任一裁定参数含「驳回(rejected)」时,提交级 verdict 必须为 failed。
|
|
62
105
|
* - exempt_adjudications 含 action==="rejected"(issue 回 todo 继续修复)→ verdict 必须为 failed
|
|
@@ -232,9 +275,11 @@ function clearReviewVerificationTags(item: WorkItem): void {
|
|
|
232
275
|
clearStepTags(item, "verify_quality")
|
|
233
276
|
}
|
|
234
277
|
|
|
235
|
-
/** blocker 提交后的 reset 助手:task children
|
|
278
|
+
/** blocker 提交后的 reset 助手:task children 仅 todo/review 态回 todo(done 已验证不得降级,
|
|
279
|
+
* 对齐 handleImplementParams 提交时对 done 态的跳过语义)+ 清 review 层验证标记(对齐旧 resetForBlocker)。 */
|
|
236
280
|
function resetTasksForBlocker(item: WorkItem): void {
|
|
237
281
|
for (const child of taskChildrenOf(item)) {
|
|
282
|
+
if (child.phase === "done") continue
|
|
238
283
|
child.phase = "todo"
|
|
239
284
|
delete child.metadata["reject_reason"]
|
|
240
285
|
}
|
|
@@ -264,10 +309,19 @@ function handleAnalyzeParams(item: WorkItem, params: AgentSubmitParams): void {
|
|
|
264
309
|
}
|
|
265
310
|
}
|
|
266
311
|
|
|
267
|
-
/** implement step 参数处理:blocker(on_fail)、
|
|
268
|
-
* test_results 申报存档。 */
|
|
312
|
+
/** implement step 参数处理:blocker(on_fail)、blocker_updates 消费闭环 + 无未解决 blocker 门禁、
|
|
313
|
+
* completed_task_ids + 覆盖门禁、self_check_results / test_results 申报存档。 */
|
|
269
314
|
function handleImplementParams(item: WorkItem, params: AgentSubmitParams): void {
|
|
270
315
|
if (params.blocker) {
|
|
316
|
+
// 同轮组合显式拒绝(置于分支最前,先于一切写入):blocker 分支在下方 return,同轮携带的
|
|
317
|
+
// blocker_updates 会被静默丢弃(丢失申报)。校验放行面只有分两轮:先 failed+blocker 上报新
|
|
318
|
+
// 阻塞,用户确认后下一轮再 blocker_updates + 重新提交。
|
|
319
|
+
if (params.blocker_updates?.length) {
|
|
320
|
+
throw new Error(
|
|
321
|
+
"不可在同一轮提交中同时上报新 blocker(blocker 参数)与更新旧 blocker(blocker_updates)。\n" +
|
|
322
|
+
"请分两轮处理:先以 verdict=failed + blocker 上报新阻塞;用户确认后,下一轮再携带 blocker_updates 记录答复并重新提交。"
|
|
323
|
+
)
|
|
324
|
+
}
|
|
271
325
|
if (params.verdict !== "failed") {
|
|
272
326
|
throw new Error("blocker 参数仅支持 verdict=failed 提交(on_fail 回退 analyze)。")
|
|
273
327
|
}
|
|
@@ -276,6 +330,23 @@ function handleImplementParams(item: WorkItem, params: AgentSubmitParams): void
|
|
|
276
330
|
return
|
|
277
331
|
}
|
|
278
332
|
|
|
333
|
+
// blocker_updates 消费闭环(校验语义与 handleAnalyzeParams 一致):按 blocker_id 置 resolved 并记录
|
|
334
|
+
// 用户答复。task 指向型 blocker(人工执行/环境不可验证任务)经用户确认留痕后,开发者凭该记录将对应
|
|
335
|
+
// 任务列入 completed_task_ids 重新申报,审查者核验留痕后方可在任务验证层确认。
|
|
336
|
+
// 位于 blocker 分支之外:blocker 是本轮上报参数(上方分支 return),blocker_updates 是后续轮次参数。
|
|
337
|
+
if (params.blocker_updates?.length) {
|
|
338
|
+
for (const u of params.blocker_updates) {
|
|
339
|
+
const blocker = itemBlockers(item).find((b) => b.id === u.blocker_id)
|
|
340
|
+
if (!blocker) throw new Error(`blocker "${u.blocker_id}" 不存在于 metadata.blockers 中。`)
|
|
341
|
+
if (blocker.status !== "awaiting_user") throw new Error(`blocker "${u.blocker_id}" 状态不是 awaiting_user,无法更新。`)
|
|
342
|
+
blocker.userResponse = u.user_response
|
|
343
|
+
blocker.status = "resolved"
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (params.verdict === "passed" && itemBlockers(item).some((b) => b.status !== "resolved")) {
|
|
347
|
+
throw new Error("存在未解决的 blocker,无法以 passed 提交 implement step。请先通过 blocker_updates 处理全部 blocker。")
|
|
348
|
+
}
|
|
349
|
+
|
|
279
350
|
const tasks = taskListOf(item)
|
|
280
351
|
const completed = normalizeTaskChildIds(params.completed_task_ids ?? [], item)
|
|
281
352
|
const validIds = new Set(tasks.map((t) => t.id))
|
|
@@ -305,7 +376,8 @@ function handleImplementParams(item: WorkItem, params: AgentSubmitParams): void
|
|
|
305
376
|
throw new Error(
|
|
306
377
|
`以下 task 处于 open/rejected 状态且未在 completed_task_ids 中:\n` +
|
|
307
378
|
remaining.map((t) => `- #${t.id}(${t.status}) ${t.title}`).join("\n") +
|
|
308
|
-
`\
|
|
379
|
+
`\ncompleted_task_ids 仅申报已完成可供验证的任务;无法完成或人工执行的任务不得虚报,` +
|
|
380
|
+
`应以 verdict=failed + blocker 上报(task_id 指向该任务),经用户确认后凭 blocker 留痕重新申报。`
|
|
309
381
|
)
|
|
310
382
|
}
|
|
311
383
|
|
|
@@ -342,9 +414,10 @@ function mergeBoundaryInto(item: WorkItem, expansion: NonNullable<AgentSubmitPar
|
|
|
342
414
|
* - verify_tool:本次 new_children 含 Low+,或存在未终态的 Low+ tool 报源层阻塞 child
|
|
343
415
|
* - verify_quality:本次 new_children 含 Low+ 且维度属于当前提交 agent,或存在未终态的 Low+ quality 报源层
|
|
344
416
|
* 阻塞 child 且 dimension 属于当前提交 agent 维度(新报与遗留理由均按维度过滤,F3;报源层由 source 反推)
|
|
345
|
-
* - quality_review(simple 合并审查):本次 new_children 含 Low+,或存在未终态的
|
|
346
|
-
* child(不按维度过滤——openspec-reviewer 无固定维度,对全部质量层负责;按
|
|
347
|
-
*
|
|
417
|
+
* - quality_review(simple 合并审查):本次 failed_tasks 非空,或 new_children 含 Low+,或存在未终态的
|
|
418
|
+
* Low+ quality 报源层阻塞 child(不按维度过滤——openspec-reviewer 无固定维度,对全部质量层负责;按
|
|
419
|
+
* agentToReviewDimension 过滤会对该身份恒为 undefined 导致 failed 提交死锁。simple 审查者合并承担任务
|
|
420
|
+
* 验证语义,failed_tasks 驳回任务与 Low+ issue 同为合法 failed 理由,与 verify_task 分支对齐)
|
|
348
421
|
* - verify_cleanup:本次 new_children 含 Low+,或存在未终态的 Low+ 阻塞 child(不按维度/报源层过滤——
|
|
349
422
|
* 收尾层须对全部残留阻塞负责,任何层的遗留阻塞都构成不通过理由)
|
|
350
423
|
* 理由判定在 dedupeNewChildren 之后调用(F4):传入的 newChildren 为已去重的 accepted,重复新报不构成理由。
|
|
@@ -376,11 +449,14 @@ function assertFailedHasReason(
|
|
|
376
449
|
} else if (stepId === "quality_review") {
|
|
377
450
|
// simple 合并审查 step:审查者(openspec-reviewer)不归属固定质量维度,理由判定不按
|
|
378
451
|
// agentToReviewDimension 过滤(该值对 openspec-reviewer 为 undefined,按维度过滤会导致任何
|
|
379
|
-
// failed 提交都报「不存在未解决的阻塞 issue
|
|
380
|
-
//
|
|
381
|
-
//
|
|
452
|
+
// failed 提交都报「不存在未解决的阻塞 issue」死锁)。simple 审查者承担任务验证(verify_task
|
|
453
|
+
// 语义),failed_tasks 与 Low+ issue 同为合法 failed 理由:本次 failed_tasks 非空(驳回任务回退
|
|
454
|
+
// implement),或本次新报含 Low+ issue,或存在 quality 报源(sourcePhase === "quality")未终态
|
|
455
|
+
// 阻塞 issue,均构成不通过理由——simple 审查者对全部质量层负责,与 full 模式 verify_quality
|
|
456
|
+
// 各维度分支互不影响。
|
|
382
457
|
layerName = "AI 审查层"
|
|
383
458
|
hasReason =
|
|
459
|
+
(params.failed_tasks?.length ?? 0) > 0 ||
|
|
384
460
|
hasNewBlocking ||
|
|
385
461
|
existingBlocking.some((c) => resolveChildIssueFields(c).sourcePhase === "quality")
|
|
386
462
|
} else {
|
|
@@ -453,7 +529,17 @@ function handleReviewParams(
|
|
|
453
529
|
}
|
|
454
530
|
}
|
|
455
531
|
|
|
456
|
-
|
|
532
|
+
// 任务验证覆盖门禁(兜底):verify_task / quality_review(任务验证归属 step)以 passed 提交时,
|
|
533
|
+
// 「submitted task 必须被 verified/failed 全覆盖」为无条件必查——reviewer 不得 passed 而不对任何
|
|
534
|
+
// 已申报任务表态(漏带 verified_tasks 会让 task children 卡 review 态,done 收口被拦截形成死锁)。
|
|
535
|
+
// 其余 review step 维持既有条件式(携带任一数组才进入校验);verdict=failed 不做此强制
|
|
536
|
+
// (报 issue 回退时无须给全部任务表态)。
|
|
537
|
+
const isTaskVerifyStep = stepId === "verify_task" || stepId === "quality_review"
|
|
538
|
+
if (
|
|
539
|
+
params.verified_tasks?.length ||
|
|
540
|
+
params.failed_tasks?.length ||
|
|
541
|
+
(params.verdict === "passed" && isTaskVerifyStep)
|
|
542
|
+
) {
|
|
457
543
|
const tasks = taskListOf(item)
|
|
458
544
|
const validIds = new Set(tasks.map((t) => t.id))
|
|
459
545
|
const verified = normalizeTaskChildIds(params.verified_tasks ?? [], item)
|
|
@@ -476,7 +562,10 @@ function handleReviewParams(
|
|
|
476
562
|
if (uncovered.length > 0) {
|
|
477
563
|
throw new Error(
|
|
478
564
|
`以下 submitted task 未被 verified_tasks 或 failed_tasks 覆盖:\n` +
|
|
479
|
-
uncovered.map((t) => `- #${t.id} ${t.title}`).join("\n")
|
|
565
|
+
uncovered.map((t) => `- #${t.id} ${t.title}`).join("\n") +
|
|
566
|
+
`\nverified_tasks 用于逐项确认已完成的任务;验证未通过的任务须以 failed_tasks 逐项驳回并设 verdict=failed 回退;` +
|
|
567
|
+
`人工执行或环境不可验证的任务不得虚报确认,应以 verdict=failed + failed_tasks 驳回,` +
|
|
568
|
+
`由开发者经 verdict=failed + blocker(task_id 指向该任务)上报、用户确认留痕后凭记录重新申报完成。`
|
|
480
569
|
)
|
|
481
570
|
}
|
|
482
571
|
for (const id of verified) {
|
|
@@ -572,17 +661,22 @@ export async function agentSubmitExecute(params: AgentSubmitParams, ctx: ToolCon
|
|
|
572
661
|
|
|
573
662
|
// 重复提交守卫(仅 review step):同 step 同 agent 已以 passed 通过后不允许重复提交;failed 允许重提
|
|
574
663
|
// (回退重审期 failed tag 未被归因清空时须可重提,如 verify_task 仅 failed_tasks 驳回)。
|
|
575
|
-
//
|
|
576
|
-
//
|
|
577
|
-
//
|
|
578
|
-
//
|
|
664
|
+
// 补交豁免(两条对称通道,均要求严格形态——仅携带对应补交参数,无其他实质提交内容):
|
|
665
|
+
// - 裁定补交:仅携带 recheck_adjudications / exempt_adjudications——上次提交漏带复核/豁免裁定导致
|
|
666
|
+
// 本层 review 态 issue 阻塞门禁时(engine.blockedSupplementAgents 已推导应补交的报源 reviewer),
|
|
667
|
+
// reviewer 可补带裁定重提解除阻塞;
|
|
668
|
+
// - 任务验证补交:仅携带 verified_tasks 且存在 review 态 task child(与引擎 gateBlock 补交推导
|
|
669
|
+
// 条件同源)——任务验证归属 step 上次 passed 漏带 verified_tasks 导致 task children 卡 review
|
|
670
|
+
// 态、done 收口被拦截时,审查者可补交 verified_tasks 覆盖待验证任务后推进。
|
|
671
|
+
// 空提交或携带其他实质参数(含 fixed_issue_ids / exempt_issue_ids / new_children)的重复提交仍被拒绝。
|
|
579
672
|
// 守卫在一切裁定(豁免/复核)之前执行,保证守卫拒绝时零副作用。step 归属按 stepMap 直接判定
|
|
580
673
|
// (不依赖 assertSubmitRouting,保持越权 step 提交的裁定拦截语义不变)。
|
|
581
674
|
const guardStepPhase = workflow.stepMap.get(params.step_id)?.phase.name
|
|
582
675
|
if (
|
|
583
676
|
guardStepPhase === "review" &&
|
|
584
677
|
getStepVerdict(item, params.step_id, ctx.agent) === "passed" &&
|
|
585
|
-
!isSupplementOnly(params)
|
|
678
|
+
!isSupplementOnly(params) &&
|
|
679
|
+
!isTaskVerifySupplementAllowed(item, params, params.step_id, ctx.agent)
|
|
586
680
|
) {
|
|
587
681
|
throw new Error(`重复提交守卫:agent "${ctx.agent}" 已在 step "${params.step_id}" 以 passed 通过,不允许重复提交。`)
|
|
588
682
|
}
|
|
@@ -693,8 +787,13 @@ export async function agentSubmitExecute(params: AgentSubmitParams, ctx: ToolCon
|
|
|
693
787
|
const agentCfg = stepCfg.agents.find((a) => a.id === ctx.agent)
|
|
694
788
|
handleReviewParams(item, params, newChildren, params.step_id, {
|
|
695
789
|
capabilityTags: agentCfg?.capability_tags ?? [],
|
|
696
|
-
//
|
|
697
|
-
|
|
790
|
+
// 补交豁免(仅裁定参数 / 仅 verified_tasks 的任务验证补交,均无实质审查内容)不重复校验
|
|
791
|
+
// 必做清单覆盖度——完整审查在首次 passed 时已做过。任务验证补交的跳过与守卫放宽同源
|
|
792
|
+
//(isTaskVerifySupplementAllowed:已 passed 且存在 review 态 task child),首次提交
|
|
793
|
+
//(step 尚未 passed)不借补交形态跳过必做门禁。
|
|
794
|
+
skipMustDoGate:
|
|
795
|
+
isSupplementOnly(params) ||
|
|
796
|
+
isTaskVerifySupplementAllowed(item, params, params.step_id, ctx.agent),
|
|
698
797
|
})
|
|
699
798
|
} else if (stepPhase === "todo") {
|
|
700
799
|
handleAnalyzeParams(item, params)
|
package/src/core/tools/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BuildPhaseTarget, ReviewLayer, ReviewVerifyStep, WorkflowMode } from "../types.ts"
|
|
1
|
+
import type { BuildPhaseTarget, ReviewLayer, ReviewVerifyStep, SimpleReviewStep, WorkflowMode } from "../types.ts"
|
|
2
2
|
|
|
3
3
|
export interface ToolContext {
|
|
4
4
|
worktree: string
|
|
@@ -24,9 +24,11 @@ export interface InitParams {
|
|
|
24
24
|
phase: BuildPhaseTarget
|
|
25
25
|
review_layer?: ReviewLayer
|
|
26
26
|
reopenIssues?: boolean
|
|
27
|
-
/**
|
|
28
|
-
* 恢复后 currentStep
|
|
29
|
-
|
|
27
|
+
/** 重置指定审查 step 的审查 tags 为 pending(仅 phase=review 时有效,与 review_layer 互斥),
|
|
28
|
+
* 恢复后 currentStep 落在第一个未全部通过的审查 step,可能早于被重置的 step。
|
|
29
|
+
* 值按模式生效:full 模式为 verify_tool/verify_task/verify_quality,simple 模式为 quality_review
|
|
30
|
+
* (跨模式值在运行时报错)。 */
|
|
31
|
+
reset_steps?: ReviewVerifyStep[] | SimpleReviewStep[]
|
|
30
32
|
}
|
|
31
33
|
/** 流程模式(full/simple):首次新建编排状态时固化;已开始的变更仅在切组(其他任务组均已完成
|
|
32
34
|
* 或从未激活)或 recovery.phase=task_analysis 重制当前组(其他任务组同样须已完成或从未激活)
|
package/src/core/types.ts
CHANGED
|
@@ -20,9 +20,13 @@ export type BuildPhaseTarget = typeof BUILD_PHASE_TARGETS[number]
|
|
|
20
20
|
export const REVIEW_LAYERS = ["tool", "task", "quality"] as const
|
|
21
21
|
export type ReviewLayer = typeof REVIEW_LAYERS[number]
|
|
22
22
|
|
|
23
|
-
/** review 三个验证 step 合法值(recovery.reset_steps
|
|
23
|
+
/** review 三个验证 step 合法值(recovery.reset_steps 在 full 模式的值域)。 */
|
|
24
24
|
export const REVIEW_VERIFY_STEPS = ["verify_tool", "verify_task", "verify_quality"] as const
|
|
25
25
|
export type ReviewVerifyStep = typeof REVIEW_VERIFY_STEPS[number]
|
|
26
|
+
|
|
27
|
+
/** simple 模式 review 合并审查 step 合法值(recovery.reset_steps 在 simple 模式的值域)。 */
|
|
28
|
+
export const SIMPLE_REVIEW_STEPS = ["quality_review"] as const
|
|
29
|
+
export type SimpleReviewStep = typeof SIMPLE_REVIEW_STEPS[number]
|
|
26
30
|
export type OrchestrateStatus = "not_started" | "in_progress" | "completed"
|
|
27
31
|
export type DimensionVerdict = "pending" | "passed" | "failed"
|
|
28
32
|
export type QualityLayerProgress = Record<ReviewDimension, DimensionVerdict>
|
|
@@ -9,6 +9,7 @@ import { WORK_ITEM_PHASES, BLOCKING_SEVERITIES, stepAgentIds, EXEMPT_REQUEST_KEY
|
|
|
9
9
|
import type { LoadedWorkflow } from "./loader.ts"
|
|
10
10
|
import { tagKey } from "./types.ts"
|
|
11
11
|
import { reviewLayerFromMetadata, readIssueSource } from "../constants.ts"
|
|
12
|
+
import { taskChildrenOf } from "../task-children.ts"
|
|
12
13
|
|
|
13
14
|
const TERMINAL_PHASES: WorkItemPhase[] = ["done", "cancelled"]
|
|
14
15
|
|
|
@@ -555,6 +556,19 @@ export function recommendForItem(item: WorkItem, workflow: LoadedWorkflow): Engi
|
|
|
555
556
|
// 避免"step 已通过"误导造成无人再触发迁移的静默死锁。
|
|
556
557
|
const gateBlock = forwardAdvanceBlockReason(item, workflow, current.step)
|
|
557
558
|
if (gateBlock) {
|
|
559
|
+
// 任务验证补交推导:review 阶段 step 全 passed 但门禁被 review 态 task child 拦截(任务验证
|
|
560
|
+
// 归属 step——verify_task/quality_review——passed 提交漏带 verified_tasks,或 full 模式下游
|
|
561
|
+
// 收尾 step 推进到 done 时暴露)时,返回该 step 全部 agent 由其经 opx_agent_submit 补交
|
|
562
|
+
// verified_tasks 覆盖全部待验证任务后推进(与 submit 重复提交守卫的补交放行条件同源:
|
|
563
|
+
// 仅携带 verified_tasks + 存在 review 态 task child),不再恒返回 agents=[] 形成静默死锁。
|
|
564
|
+
if (current.phaseName === "review" && taskChildrenOf(item).some((c) => c.phase === "review")) {
|
|
565
|
+
return {
|
|
566
|
+
status: "blocked",
|
|
567
|
+
stepId: current.step.id,
|
|
568
|
+
agents: stepAgentIds(current.step),
|
|
569
|
+
blockedReason: `${gateBlock}需该 step 审查者经 opx_agent_submit 补交 verified_tasks(覆盖全部待验证任务)后推进。`,
|
|
570
|
+
}
|
|
571
|
+
}
|
|
558
572
|
return {
|
|
559
573
|
status: "blocked",
|
|
560
574
|
stepId: current.step.id,
|
|
@@ -717,6 +717,11 @@ function renderAnalyzeBlockers(item: WorkItem): string[] {
|
|
|
717
717
|
/** implement step:developer 待修复 children(仅 todo 态:review 态由对应 reviewer 复核、豁免申请走待裁定区块)。 */
|
|
718
718
|
function renderDeveloperChildren(item: WorkItem, ctxAgent: string, exemptionCtx?: ExemptionHintCtx): string[] {
|
|
719
719
|
const lines: string[] = []
|
|
720
|
+
// 已处理 blocker 的用户确认留痕:dev 凭该记录将人工执行/环境不可验证任务重新列入
|
|
721
|
+
// completed_task_ids 申报(full 模式下 architect 代为 resolve 后,dev 在 implement 视图
|
|
722
|
+
// 读取用户确认内容——凭留痕申报的依据链在此闭合)。置于待修复清单早退之前,无待修复
|
|
723
|
+
// issue 时留痕仍渲染。
|
|
724
|
+
lines.push(...renderBlockerTrail(item, BLOCKER_TRAIL_TITLE_DEV))
|
|
720
725
|
// 仅 issue child 进入修复清单(task child 不得混入 issue 渲染)
|
|
721
726
|
const toFix = issueChildrenOf(item).filter((c) => isAgentOwnedIssue(c, ctxAgent))
|
|
722
727
|
if (toFix.length === 0) return lines
|
|
@@ -928,6 +933,29 @@ function renderToolAdjudicateOnly(item: WorkItem, rec: EngineRecommendation, ctx
|
|
|
928
933
|
return lines.join("\n")
|
|
929
934
|
}
|
|
930
935
|
|
|
936
|
+
/** blocker 留痕区块标题(审查者视角):任务验证视图的申报核验依据口径。 */
|
|
937
|
+
const BLOCKER_TRAIL_TITLE_REVIEW = "## Blocker 留痕(人工/不可验证任务的申报依据)"
|
|
938
|
+
/** blocker 留痕区块标题(开发者视角):已处理 blocker 的用户确认记录,dev 凭记录重新申报完成。 */
|
|
939
|
+
const BLOCKER_TRAIL_TITLE_DEV = "## Blocker 留痕(已处理 blocker 的用户确认,凭记录申报完成)"
|
|
940
|
+
|
|
941
|
+
/** blocker 留痕区块(任务验证视图 verify_task / quality_review 与 developer implement 视图共用同一实现):
|
|
942
|
+
* 渲染已 resolved 且带用户答复留痕的 blocker 摘要(task_id / 描述 / 用户确认)——「人工执行/
|
|
943
|
+
* 环境不可验证」任务经 blocker 上报、用户确认留痕后由开发者凭记录重新申报,审查者核验该留痕
|
|
944
|
+
* 后方可经 verified_tasks 确认(人工任务协议的申报依据)。复用 readBlockers 读取,无留痕不渲染;
|
|
945
|
+
* 经 title 参数区分视角措辞(审查者核验申报 / 开发者凭记录申报)。 */
|
|
946
|
+
function renderBlockerTrail(item: WorkItem, title: string): string[] {
|
|
947
|
+
const resolved = readBlockers(item).filter((b) => b.status === "resolved" && b.userResponse)
|
|
948
|
+
if (resolved.length === 0) return []
|
|
949
|
+
const lines = [title, ""]
|
|
950
|
+
for (const b of resolved) {
|
|
951
|
+
lines.push(`- Blocker #${b.id}${b.taskId ? ` | Task #${b.taskId}` : ""} | ${b.category}`)
|
|
952
|
+
lines.push(` - 描述:${b.description}`)
|
|
953
|
+
lines.push(` - 用户确认:${b.userResponse}`)
|
|
954
|
+
}
|
|
955
|
+
lines.push("")
|
|
956
|
+
return lines
|
|
957
|
+
}
|
|
958
|
+
|
|
931
959
|
/** verify_task step:task children 待验证列表 + task 层 issue 主区块 + 调用者可裁定的豁免申请(待裁定)。 */
|
|
932
960
|
function renderTaskChildren(item: WorkItem, ctxAgent: string, exemptionCtx?: ExemptionHintCtx): string[] {
|
|
933
961
|
const lines: string[] = []
|
|
@@ -937,6 +965,7 @@ function renderTaskChildren(item: WorkItem, ctxAgent: string, exemptionCtx?: Exe
|
|
|
937
965
|
for (const t of pendingTasks) lines.push(renderTaskItem(t))
|
|
938
966
|
lines.push("")
|
|
939
967
|
}
|
|
968
|
+
lines.push(...renderBlockerTrail(item, BLOCKER_TRAIL_TITLE_REVIEW))
|
|
940
969
|
const issues = issueChildrenOf(item)
|
|
941
970
|
const own = issues.filter((c) => isAgentOwnedIssue(c, ctxAgent))
|
|
942
971
|
lines.push(...renderChildrenSection("Issue (待复核)", own, exemptionCtx))
|
|
@@ -971,6 +1000,7 @@ function renderMergedChildren(item: WorkItem, ctxAgent: string, exemptionCtx?: E
|
|
|
971
1000
|
for (const t of pendingTasks) lines.push(renderTaskItem(t))
|
|
972
1001
|
lines.push("")
|
|
973
1002
|
}
|
|
1003
|
+
lines.push(...renderBlockerTrail(item, BLOCKER_TRAIL_TITLE_REVIEW))
|
|
974
1004
|
const issues = issueChildrenOf(item)
|
|
975
1005
|
const own = issues.filter((c) => isAgentOwnedIssue(c, ctxAgent))
|
|
976
1006
|
lines.push(...renderChildrenSection("Issue (待复核)", own, exemptionCtx))
|