@peterxiaoyang/superspec 0.1.45 → 0.1.47
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/dist/cli.js +2 -1
- package/dist/code_review.d.ts +11 -1
- package/dist/code_review.js +40 -0
- package/dist/explore_round.d.ts +23 -0
- package/dist/explore_round.js +94 -0
- package/dist/format.d.ts +67 -2
- package/dist/format.js +273 -22
- package/dist/openspec.d.ts +13 -0
- package/dist/openspec.js +53 -4
- package/dist/phase_confirmation.js +71 -2
- package/dist/phase_plan.d.ts +6 -1
- package/dist/phase_plan.js +180 -32
- package/dist/record.js +191 -57
- package/dist/review.js +2 -0
- package/dist/task_evidence.js +5 -3
- package/dist/transition.d.ts +1 -0
- package/dist/transition.js +222 -28
- package/dist/types.d.ts +42 -0
- package/package.json +1 -1
- package/templates/workflow/AGENTS.md +15 -5
- package/templates/workflow/agents/architect.toml +1 -1
- package/templates/workflow/agents/code-reviewer.toml +1 -1
- package/templates/workflow/agents/critic.toml +1 -1
- package/templates/workflow/agents/executor.toml +1 -1
- package/templates/workflow/agents/explore.toml +1 -1
- package/templates/workflow/agents/test-engineer.toml +1 -1
- package/templates/workflow/agents/test-runner.toml +1 -1
- package/templates/workflow/agents/verifier.toml +1 -1
- package/templates/workflow/prompts/architect.md +25 -33
- package/templates/workflow/prompts/code-reviewer.md +19 -67
- package/templates/workflow/prompts/critic.md +36 -86
- package/templates/workflow/prompts/executor.md +17 -19
- package/templates/workflow/prompts/explore.md +12 -46
- package/templates/workflow/prompts/test-engineer.md +22 -34
- package/templates/workflow/prompts/test-runner.md +11 -21
- package/templates/workflow/prompts/verifier.md +13 -37
- package/templates/workflow/skills/superspec-apply/SKILL.md +26 -26
- package/templates/workflow/skills/superspec-explore/SKILL.md +69 -60
- package/templates/workflow/skills/superspec-propose/SKILL.md +85 -133
- package/templates/workflow/skills/superspec-review/SKILL.md +14 -44
package/dist/record.js
CHANGED
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
// SuperSpec 流程引擎 — record:工作项结果登记
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { ensureChangeLayout, readEvents, appendEvent, makeEvent, sha256File, sha256Text, withLock, appendRawRecord, } from "./store.js";
|
|
5
5
|
import { rebuildSnapshot } from "./sync.js";
|
|
6
6
|
import { changeRoot as openspecChangeRoot } from "./openspec.js";
|
|
7
7
|
import { isPhaseConfirmationScope, phaseActionForAnswer, phaseConfirmationForCurrentState, workflowRiskForPhaseConfirmation, } from "./phase_confirmation.js";
|
|
8
|
-
import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_DECISION_SCOPE_PREFIX, codeReviewDecisionAnswerLabel, normalizeCodeReviewDecisionAnswer, } from "./code_review.js";
|
|
8
|
+
import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_DECISION_SCOPE_PREFIX, codeReviewJobStaleReason, codeReviewDecisionAnswerLabel, currentCodeReviewWorkingPaths, latestCodeReviewFailedStatus, normalizeCodeReviewDecisionAnswer, parseCodeReviewDecisionScope, } from "./code_review.js";
|
|
9
9
|
import { invalidReasonForSubmittedReport } from "./job_validity.js";
|
|
10
10
|
import { jobSubmitArgv } from "./job_action.js";
|
|
11
11
|
import { REVIEW_DOC_PATHS, REVIEW_REJECTION_OVERRIDE_SCOPE_PREFIX, REVIEW_REJECTION_OVERRIDE_ANSWER, parseReviewRejectionOverrideScope, reviewGateRoleResolution, reviewRejectionOverrideScope, } from "./review.js";
|
|
12
12
|
import { EXPLORE_DISCOVERY_REVIEW_GATE, PROPOSE_FINAL_REVIEW_GATE } from "./review_job_gates.js";
|
|
13
13
|
import { RecordInputDecodingError, readRecordInputFile } from "./record_input.js";
|
|
14
|
+
import { currentExploreRoundId } from "./explore_round.js";
|
|
15
|
+
import { discoveryOpenQuestionDisplayText, discoveryQuestionContextFingerprint, discoveryOpenQuestionScope, EXPLORE_OPEN_QUESTION_SCOPE_PREFIX, parseDiscoveryOpenQuestions, } from "./format.js";
|
|
14
16
|
const REVIEW_REPORT_REQUIRED_FIELDS = ["role", "verdict", "findings"];
|
|
15
17
|
const REVIEW_REPORT_OPTIONAL_FIELDS = ["summary", "evidence_refs", "risks", "open_questions"];
|
|
16
18
|
const REVIEWER_KINDS = new Set(["codex-subagent", "human", "external-agent"]);
|
|
17
19
|
const CODE_REVIEW_FINDING_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
|
|
20
|
+
function isReviewRole(role) {
|
|
21
|
+
return role === "critic" || role === "architect" || role === "test-engineer" || role === "code-reviewer" || role === "verifier";
|
|
22
|
+
}
|
|
18
23
|
function requiresReviewer(role) {
|
|
19
24
|
return role === "critic" || role === "architect" || role === "test-engineer" || role === "code-reviewer";
|
|
20
25
|
}
|
|
21
26
|
/** All review reports with bound material acknowledge full file coverage. */
|
|
22
27
|
function requiresReviewScope(job) {
|
|
23
|
-
return job.boundFiles.length > 0 && job.role
|
|
28
|
+
return job.boundFiles.length > 0 && isReviewRole(job.role);
|
|
24
29
|
}
|
|
25
30
|
function projectLocalInvalidReportMustTerminate(job) {
|
|
26
31
|
return job.role === "code-reviewer" || job.packet_context?.code_state_check !== undefined;
|
|
@@ -62,14 +67,13 @@ function previousRejectionInstruction(job) {
|
|
|
62
67
|
if (!previous)
|
|
63
68
|
return "";
|
|
64
69
|
const reason = `上一次同角色审查没有形成可推进结论,原因:${previous.reason}。`;
|
|
65
|
-
if (!previous.findings || previous.findings.length === 0)
|
|
66
|
-
return `${reason}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return "问题列表中的每个新 finding 必须分配稳定 ID,后续同一问题沿用该 ID,";
|
|
70
|
+
if (!previous.findings || previous.findings.length === 0) {
|
|
71
|
+
return `${reason}上一轮没有可复核的历史 finding;请按当前 gate 的完整范围独立审查,不要把拒绝原因当作需求或验收标准,`;
|
|
72
|
+
}
|
|
73
|
+
const identityRule = job.role === "code-reviewer"
|
|
74
|
+
? "同一问题仍存在时复用原 finding ID;legacy finding 没有 ID 时沿用原始语义并补一个稳定 ID;"
|
|
75
|
+
: "已解决或已由等价证据闭环的问题不要重复报告,不得通过更换标题或措辞重复同一问题;";
|
|
76
|
+
return `${reason}本轮是修复复核:逐项判断本工作项附带的上一次同角色 finding 是否仍成立。Finding 中的 recommendation 只是非绑定建议,不是需求或验收标准;先独立核对 underlying problem、直接证据和本次验收,不得因原建议指定了某种架构就要求照做。${identityRule}默认只复核历史 finding;新 blocker 仅允许是本次修正直接引入的回归,并必须说明“修正动作 → 新问题”的因果链,不得展开无关的故障模型、消费者或架构议题。`;
|
|
73
77
|
}
|
|
74
78
|
function reviewScopeForJob(job) {
|
|
75
79
|
if (job.review_targets !== undefined || job.read_only_refs !== undefined) {
|
|
@@ -94,10 +98,15 @@ function reviewScopeInstruction(job, reviewTargets, readOnlyRefs) {
|
|
|
94
98
|
: "";
|
|
95
99
|
return targets + refs;
|
|
96
100
|
}
|
|
97
|
-
function
|
|
101
|
+
function genericReviewCoverageInstruction(job) {
|
|
98
102
|
if (!requiresReviewScope(job))
|
|
99
103
|
return "";
|
|
100
|
-
return
|
|
104
|
+
return "完整审查全部 boundFiles,不因发现第一个 blocker 停止;read_only_refs 只在核对本次问题与上下游一致性时读取。review_scope.checked_paths 必须回执已浏览的全部 boundFiles,但该回执不能代替语义审查,也不扩大可报告问题的范围。";
|
|
105
|
+
}
|
|
106
|
+
function proposalIncrementalReviewInstruction(job) {
|
|
107
|
+
if (!PROPOSE_FINAL_REVIEW_GATE.isJobForGate(job))
|
|
108
|
+
return "";
|
|
109
|
+
return "若 proposal.md 有“需求变化”,以其中记录的受影响能力、直接修改章节和保持不变范围作为增量审查锚点;新 finding 必须由该变化、其直接修改或由此造成的跨文档矛盾引起,并说明因果链。此前已通过且明确保持不变的设计不得重新打开为 blocker;Recommendation 只描述需要补足的结果、契约或证据,不得把未经 proposal、design 或用户决定选定的新基础设施写成 required fix。";
|
|
101
110
|
}
|
|
102
111
|
function migrationEvidenceInstruction(job) {
|
|
103
112
|
const isProposeReview = PROPOSE_FINAL_REVIEW_GATE.isJobForGate(job);
|
|
@@ -117,6 +126,66 @@ function asObject(value) {
|
|
|
117
126
|
function nonEmptyString(value) {
|
|
118
127
|
return typeof value === "string" && value.trim() !== "";
|
|
119
128
|
}
|
|
129
|
+
function currentDiscoveryOpenQuestion(projectRoot, change) {
|
|
130
|
+
const discoveryPath = join(openspecChangeRoot(projectRoot, change), ".superspec", "artifacts", "discovery.md");
|
|
131
|
+
if (!existsSync(discoveryPath))
|
|
132
|
+
return null;
|
|
133
|
+
return parseDiscoveryOpenQuestions(readFileSync(discoveryPath, "utf8"))[0] ?? null;
|
|
134
|
+
}
|
|
135
|
+
function latestAcceptedExploreOpenQuestionDecision(events, scope) {
|
|
136
|
+
return [...events].reverse().find(event => {
|
|
137
|
+
if (event.event_type !== "user_decision_recorded")
|
|
138
|
+
return false;
|
|
139
|
+
const payload = event.payload;
|
|
140
|
+
return payload.accepted !== false && payload.scope === scope;
|
|
141
|
+
}) ?? null;
|
|
142
|
+
}
|
|
143
|
+
function isExploreOpenQuestionScope(scope) {
|
|
144
|
+
return scope.startsWith(EXPLORE_OPEN_QUESTION_SCOPE_PREFIX);
|
|
145
|
+
}
|
|
146
|
+
function isWellFormedExploreOpenQuestionScope(scope) {
|
|
147
|
+
return /^explore_open_question:sha256:[a-f0-9]{64}:(?:Q-[A-Za-z0-9][A-Za-z0-9_-]*|item-[1-9]\d*)$/.test(scope);
|
|
148
|
+
}
|
|
149
|
+
function invalidExploreOpenQuestionResult(projectRoot, change, inputDigest, existing, decision, reason) {
|
|
150
|
+
const existingPayload = existing?.payload;
|
|
151
|
+
if (existingPayload?.accepted === false && existingPayload.reason === reason) {
|
|
152
|
+
return {
|
|
153
|
+
event_type: "user_decision_recorded",
|
|
154
|
+
accepted: false,
|
|
155
|
+
message: "幂等返回:同一无效待确认问题答复已登记",
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
appendEvent(projectRoot, change, makeEvent(change, "user_decision_recorded", {
|
|
159
|
+
accepted: false,
|
|
160
|
+
scope: decision.scope,
|
|
161
|
+
answer: decision.answer,
|
|
162
|
+
reason,
|
|
163
|
+
input_digest: inputDigest,
|
|
164
|
+
}));
|
|
165
|
+
return {
|
|
166
|
+
event_type: "user_decision_recorded",
|
|
167
|
+
accepted: false,
|
|
168
|
+
message: reason === "invalid_explore_open_question_scope"
|
|
169
|
+
? "确认事项的内部标识无效,请重新执行 next 获取当前事项"
|
|
170
|
+
: reason === "explore_open_question_already_recorded"
|
|
171
|
+
? "这件事已有已登记答复,请先回写 discovery.md 后重新执行 next"
|
|
172
|
+
: "需要确认的事项已变化或已完成,请重新执行 next 获取当前事项",
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function invalidCodeReviewDecisionResult(projectRoot, change, inputDigest, existing, decision, reason, message) {
|
|
176
|
+
const existingPayload = existing?.payload;
|
|
177
|
+
if (existingPayload?.accepted === false && existingPayload.reason === reason) {
|
|
178
|
+
return { event_type: "user_decision_recorded", accepted: false, message: "幂等返回:同一无效代码审查决策已登记" };
|
|
179
|
+
}
|
|
180
|
+
appendEvent(projectRoot, change, makeEvent(change, "user_decision_recorded", {
|
|
181
|
+
accepted: false,
|
|
182
|
+
scope: decision.scope,
|
|
183
|
+
answer: decision.answer,
|
|
184
|
+
reason,
|
|
185
|
+
input_digest: inputDigest,
|
|
186
|
+
}));
|
|
187
|
+
return { event_type: "user_decision_recorded", accepted: false, message };
|
|
188
|
+
}
|
|
120
189
|
function stringArray(value) {
|
|
121
190
|
return Array.isArray(value) && value.every(item => typeof item === "string");
|
|
122
191
|
}
|
|
@@ -421,8 +490,8 @@ function recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, even
|
|
|
421
490
|
if (!Array.isArray(obj.findings)) {
|
|
422
491
|
checks.push("报告问题列表 findings 必须是数组");
|
|
423
492
|
}
|
|
424
|
-
else if (isOrdinaryReviewer(job.role) && obj.verdict === "fail" && obj.findings.length === 0) {
|
|
425
|
-
checks.push("
|
|
493
|
+
else if ((isOrdinaryReviewer(job.role) || job.role === "verifier") && obj.verdict === "fail" && obj.findings.length === 0) {
|
|
494
|
+
checks.push("审查报告结论为 fail 时 findings 至少包含一个问题");
|
|
426
495
|
}
|
|
427
496
|
if (requiresReviewer(job.role)) {
|
|
428
497
|
validateReviewer(obj, checks);
|
|
@@ -633,6 +702,40 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
|
|
|
633
702
|
}));
|
|
634
703
|
return { event_type: "user_decision_recorded", accepted: false, message: "决策文件缺少决策范围(scope)或答复内容(answer)" };
|
|
635
704
|
}
|
|
705
|
+
// Explore 的用户答复只能绑定当前文档顺序中的第一项。这里不判断答案是否
|
|
706
|
+
// “正确”,只机械校验当前项、Discovery 决策上下文和 Explore 轮次仍与 next
|
|
707
|
+
// 返回时一致,防止旧问题在材料改写或重新探索后被错误登记为新问题的决定。
|
|
708
|
+
let exploreOpenQuestion = null;
|
|
709
|
+
let exploreOpenQuestionRoundId = null;
|
|
710
|
+
let exploreOpenQuestionContextFingerprint = null;
|
|
711
|
+
if (isExploreOpenQuestionScope(decision.scope)) {
|
|
712
|
+
if (!isWellFormedExploreOpenQuestionScope(decision.scope)) {
|
|
713
|
+
return invalidExploreOpenQuestionResult(projectRoot, change, inputDigest, existing, { scope: decision.scope, answer: decision.answer }, "invalid_explore_open_question_scope");
|
|
714
|
+
}
|
|
715
|
+
const current = currentDiscoveryOpenQuestion(projectRoot, change);
|
|
716
|
+
const expectedScope = current ? discoveryOpenQuestionScope(current, currentExploreRoundId(events)) : null;
|
|
717
|
+
if (!expectedScope || decision.scope !== expectedScope) {
|
|
718
|
+
return invalidExploreOpenQuestionResult(projectRoot, change, inputDigest, existing, { scope: decision.scope, answer: decision.answer }, "stale_explore_open_question_scope");
|
|
719
|
+
}
|
|
720
|
+
const acceptedForCurrentScope = latestAcceptedExploreOpenQuestionDecision(events, decision.scope);
|
|
721
|
+
if (acceptedForCurrentScope) {
|
|
722
|
+
const previousAnswer = acceptedForCurrentScope.payload.answer;
|
|
723
|
+
if (previousAnswer === decision.answer) {
|
|
724
|
+
return {
|
|
725
|
+
event_type: "user_decision_recorded",
|
|
726
|
+
accepted: true,
|
|
727
|
+
message: "幂等返回:同一用户决策已登记",
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
return invalidExploreOpenQuestionResult(projectRoot, change, inputDigest, existing, { scope: decision.scope, answer: decision.answer }, "explore_open_question_already_recorded");
|
|
731
|
+
}
|
|
732
|
+
exploreOpenQuestion = current;
|
|
733
|
+
exploreOpenQuestionRoundId = currentExploreRoundId(events);
|
|
734
|
+
const discoveryPath = join(openspecChangeRoot(projectRoot, change), ".superspec", "artifacts", "discovery.md");
|
|
735
|
+
exploreOpenQuestionContextFingerprint = current
|
|
736
|
+
? discoveryQuestionContextFingerprint(readFileSync(discoveryPath, "utf8"), current)
|
|
737
|
+
: null;
|
|
738
|
+
}
|
|
636
739
|
let phaseConfirmation = null;
|
|
637
740
|
let phaseAction = null;
|
|
638
741
|
let phaseReviewRisk = null;
|
|
@@ -699,7 +802,10 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
|
|
|
699
802
|
phaseAction = action;
|
|
700
803
|
phaseReviewRisk = decisionRisk;
|
|
701
804
|
}
|
|
702
|
-
|
|
805
|
+
// Explore scope 已在上面按当前问题和同 scope 的已接受答复完成校验。此前同一
|
|
806
|
+
// input 曾因 stale 被拒绝、但材料后来回到完全相同的当前项时,不能让旧拒绝
|
|
807
|
+
// 记录永久吞掉一次现在有效的登记。
|
|
808
|
+
if (existing && !phaseAction && !exploreOpenQuestion) {
|
|
703
809
|
const accepted = existing.payload.accepted !== false;
|
|
704
810
|
return {
|
|
705
811
|
event_type: "user_decision_recorded",
|
|
@@ -760,36 +866,45 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
|
|
|
760
866
|
};
|
|
761
867
|
reviewRejectionDecisionSource = decision.decision_source;
|
|
762
868
|
}
|
|
869
|
+
let codeReviewDecisionReference = null;
|
|
763
870
|
if (decision.scope.startsWith(CODE_REVIEW_DECISION_SCOPE_PREFIX)) {
|
|
764
871
|
const normalizedAnswer = normalizeCodeReviewDecisionAnswer(decision.answer);
|
|
765
872
|
if (!normalizedAnswer) {
|
|
766
|
-
|
|
767
|
-
accepted: false,
|
|
768
|
-
scope: decision.scope,
|
|
769
|
-
answer: decision.answer,
|
|
770
|
-
reason: "invalid_code_review_decision_answer",
|
|
771
|
-
input_digest: inputDigest,
|
|
772
|
-
}));
|
|
773
|
-
return {
|
|
774
|
-
event_type: "user_decision_recorded",
|
|
775
|
-
accepted: false,
|
|
776
|
-
message: `代码审查决策必须是 ${CODE_REVIEW_DECISION_ANSWER_LABELS.reopen_propose}、${CODE_REVIEW_DECISION_ANSWER_LABELS.reopen_apply} 或 ${CODE_REVIEW_DECISION_ANSWER_LABELS.dismiss}`,
|
|
777
|
-
};
|
|
873
|
+
return invalidCodeReviewDecisionResult(projectRoot, change, inputDigest, existing, { scope: decision.scope, answer: decision.answer }, "invalid_code_review_decision_answer", `代码审查决策必须是 ${CODE_REVIEW_DECISION_ANSWER_LABELS.reopen_propose}、${CODE_REVIEW_DECISION_ANSWER_LABELS.reopen_apply} 或 ${CODE_REVIEW_DECISION_ANSWER_LABELS.dismiss}`);
|
|
778
874
|
}
|
|
779
875
|
if (!nonEmptyString(decision.reason)) {
|
|
780
|
-
|
|
781
|
-
accepted: false,
|
|
782
|
-
scope: decision.scope,
|
|
783
|
-
answer: decision.answer,
|
|
784
|
-
reason: "missing_reason",
|
|
785
|
-
input_digest: inputDigest,
|
|
786
|
-
}));
|
|
787
|
-
return {
|
|
788
|
-
event_type: "user_decision_recorded",
|
|
789
|
-
accepted: false,
|
|
790
|
-
message: "代码审查决策必须写明原因",
|
|
791
|
-
};
|
|
876
|
+
return invalidCodeReviewDecisionResult(projectRoot, change, inputDigest, existing, { scope: decision.scope, answer: decision.answer }, "missing_reason", "代码审查决策必须写明原因");
|
|
792
877
|
}
|
|
878
|
+
const ref = parseCodeReviewDecisionScope(decision.scope);
|
|
879
|
+
const changeRoot = openspecChangeRoot(projectRoot, change);
|
|
880
|
+
const snapshot = rebuildSnapshot(projectRoot, change, changeRoot);
|
|
881
|
+
const status = latestCodeReviewFailedStatus(events);
|
|
882
|
+
const finding = ref && status?.terminal.job.job_id === ref.jobId
|
|
883
|
+
? status.findings.find(item => item.id === ref.findingId && (item.type === "spec" || item.type === "mixed"))
|
|
884
|
+
: null;
|
|
885
|
+
const staleReason = status && ref && status.terminal.job.job_id === ref.jobId
|
|
886
|
+
? codeReviewJobStaleReason(projectRoot, status.terminal.job, currentCodeReviewWorkingPaths(projectRoot, events))
|
|
887
|
+
: null;
|
|
888
|
+
if (!ref || snapshot.state !== "apply_done" || !status || !finding || staleReason) {
|
|
889
|
+
const reason = !ref
|
|
890
|
+
? "invalid_code_review_decision_scope"
|
|
891
|
+
: snapshot.state !== "apply_done"
|
|
892
|
+
? "code_review_decision_not_current"
|
|
893
|
+
: staleReason
|
|
894
|
+
? "stale_code_review_decision_target"
|
|
895
|
+
: "code_review_decision_target_not_found";
|
|
896
|
+
const message = staleReason
|
|
897
|
+
? "代码审查材料已不再匹配当前代码,请先重新执行 review-ready 获取新的审查结论"
|
|
898
|
+
: "代码审查决策只能关联当前代码审查报告中的待决定问题,请先执行 next 获取当前选择";
|
|
899
|
+
return invalidCodeReviewDecisionResult(projectRoot, change, inputDigest, existing, { scope: decision.scope, answer: decision.answer }, reason, message);
|
|
900
|
+
}
|
|
901
|
+
codeReviewDecisionReference = {
|
|
902
|
+
job_id: status.terminal.job.job_id,
|
|
903
|
+
finding_id: finding.id,
|
|
904
|
+
rejection_event_id: status.terminal.event.event_id,
|
|
905
|
+
rejection_event_digest: status.terminal.event.event_digest,
|
|
906
|
+
packet_digest: status.terminal.job.packet_digest,
|
|
907
|
+
};
|
|
793
908
|
}
|
|
794
909
|
const normalizedAnswer = decision.scope.startsWith(CODE_REVIEW_DECISION_SCOPE_PREFIX)
|
|
795
910
|
? normalizeCodeReviewDecisionAnswer(decision.answer)
|
|
@@ -798,13 +913,25 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
|
|
|
798
913
|
scope: decision.scope,
|
|
799
914
|
question: phaseConfirmation
|
|
800
915
|
? phaseConfirmation.ask.question
|
|
801
|
-
:
|
|
916
|
+
: exploreOpenQuestion
|
|
917
|
+
? discoveryOpenQuestionDisplayText(exploreOpenQuestion)
|
|
918
|
+
: typeof decision.question === "string" ? decision.question : "",
|
|
802
919
|
answer: phaseAction
|
|
803
920
|
? phaseAction.label
|
|
804
921
|
: normalizedAnswer ? codeReviewDecisionAnswerLabel(normalizedAnswer) : decision.answer,
|
|
805
922
|
...(typeof decision.reason === "string" ? { reason: decision.reason.trim() } : {}),
|
|
806
923
|
...(reviewRejectionDecisionSource ? { decision_source: reviewRejectionDecisionSource } : {}),
|
|
807
924
|
...(reviewRejectionOverride ? { review_rejection_override: reviewRejectionOverride } : {}),
|
|
925
|
+
...(codeReviewDecisionReference ? { code_review_decision: codeReviewDecisionReference } : {}),
|
|
926
|
+
...(exploreOpenQuestion && exploreOpenQuestionRoundId && exploreOpenQuestionContextFingerprint ? {
|
|
927
|
+
explore_open_question: {
|
|
928
|
+
round_id: exploreOpenQuestionRoundId,
|
|
929
|
+
question_id: exploreOpenQuestion.id,
|
|
930
|
+
question_ordinal: exploreOpenQuestion.ordinal,
|
|
931
|
+
document_fingerprint: exploreOpenQuestion.documentFingerprint,
|
|
932
|
+
context_fingerprint: exploreOpenQuestionContextFingerprint,
|
|
933
|
+
},
|
|
934
|
+
} : {}),
|
|
808
935
|
...(phaseAction ? {
|
|
809
936
|
phase_confirmation: {
|
|
810
937
|
boundary: phaseAction.boundary,
|
|
@@ -824,7 +951,9 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
|
|
|
824
951
|
return {
|
|
825
952
|
event_type: "user_decision_recorded",
|
|
826
953
|
accepted: true,
|
|
827
|
-
message:
|
|
954
|
+
message: exploreOpenQuestion
|
|
955
|
+
? "这件事的答复已登记"
|
|
956
|
+
: `用户决策已登记:决策范围(scope)=${decision.scope}`,
|
|
828
957
|
};
|
|
829
958
|
}
|
|
830
959
|
/** record user-decision:登记用户决策 */
|
|
@@ -913,6 +1042,7 @@ function packetFieldDescriptions() {
|
|
|
913
1042
|
unattributed_paths: "代码审查范围中暂时无法归属到某个任务(task)的文件。",
|
|
914
1043
|
unknown_attribution_tasks: "因为缺少边界快照或提交段 diff 失败而无法完整计算改动归属的任务(task)。",
|
|
915
1044
|
coverage_exemption_refs: "测试覆盖豁免引用:说明某个 TEST 为什么没有绑定到任务(task)。",
|
|
1045
|
+
code_review_gate: "最终验证读取的代码审查门禁事实:passed 指向已接受的代码审查工作项,skipped 表示本轮没有代码类改动。",
|
|
916
1046
|
code_state_check: "代码状态检查:最终验证时用于判断代码审查后代码是否又发生变化。",
|
|
917
1047
|
event_id: "事件 ID,用于追溯证据来源。",
|
|
918
1048
|
event_digest: "事件摘要,用于确认引用的证据事件没有被替换。",
|
|
@@ -932,6 +1062,7 @@ export function jobsPacket(projectRoot, change, jobId) {
|
|
|
932
1062
|
return { found: false, message: `工作项 ${jobId} 不存在` };
|
|
933
1063
|
}
|
|
934
1064
|
const isCodeReviewer = job.role === "code-reviewer";
|
|
1065
|
+
const isReviewer = isReviewRole(job.role);
|
|
935
1066
|
const hasReviewScope = requiresReviewScope(job);
|
|
936
1067
|
const packetContext = job.packet_context;
|
|
937
1068
|
const { reviewTargets, readOnlyRefs } = reviewScopeForJob(job);
|
|
@@ -943,12 +1074,13 @@ export function jobsPacket(projectRoot, change, jobId) {
|
|
|
943
1074
|
...(job.gate_id ? { gate_id: job.gate_id } : {}),
|
|
944
1075
|
recommended_agent: recommendedAgentForRole(job.role),
|
|
945
1076
|
boundFiles: job.boundFiles,
|
|
946
|
-
...(reviewTargets.length > 0 ? { review_targets: reviewTargets } : {}),
|
|
947
|
-
...(readOnlyRefs.length > 0 ? { read_only_refs: readOnlyRefs } : {}),
|
|
1077
|
+
...(isReviewer && reviewTargets.length > 0 ? { review_targets: reviewTargets } : {}),
|
|
1078
|
+
...(isReviewer && readOnlyRefs.length > 0 ? { read_only_refs: readOnlyRefs } : {}),
|
|
948
1079
|
...(job.review_evidence_digest ? { review_evidence_digest: job.review_evidence_digest } : {}),
|
|
949
1080
|
...(job.previous_rejection ? { previous_rejection: job.previous_rejection } : {}),
|
|
950
1081
|
...(packetContext ? { packet_context: packetContext } : {}),
|
|
951
1082
|
...(packetContext?.code_review_scope ? { code_review_scope: packetContext.code_review_scope } : {}),
|
|
1083
|
+
...(packetContext?.code_review_gate ? { code_review_gate: packetContext.code_review_gate } : {}),
|
|
952
1084
|
...(packetContext?.coverage_exemption_refs ? { coverage_exemption_refs: packetContext.coverage_exemption_refs } : {}),
|
|
953
1085
|
...(packetContext?.task_execution_index ? { task_execution_index: packetContext.task_execution_index } : {}),
|
|
954
1086
|
...(packetContext?.unattributed_paths ? { unattributed_paths: packetContext.unattributed_paths } : {}),
|
|
@@ -970,29 +1102,31 @@ export function jobsPacket(projectRoot, change, jobId) {
|
|
|
970
1102
|
output_contract_optional_fields: [...REVIEW_REPORT_OPTIONAL_FIELDS],
|
|
971
1103
|
字段说明: packetFieldDescriptions(),
|
|
972
1104
|
output_instructions: `${roleDescription(job.role)}。` +
|
|
973
|
-
reviewScopeInstruction(job, reviewTargets, readOnlyRefs) +
|
|
974
|
-
migrationEvidenceInstruction(job) +
|
|
1105
|
+
(isReviewer ? reviewScopeInstruction(job, reviewTargets, readOnlyRefs) : "") +
|
|
1106
|
+
(isReviewer ? migrationEvidenceInstruction(job) : "") +
|
|
975
1107
|
(job.review_evidence_digest ? `本工作项对应的执行证据版本为 ${job.review_evidence_digest},` : "") +
|
|
976
|
-
|
|
977
|
-
previousRejectionInstruction(job) +
|
|
1108
|
+
(isReviewer ? genericReviewCoverageInstruction(job) + proposalIncrementalReviewInstruction(job) + previousRejectionInstruction(job) : "") +
|
|
978
1109
|
(requiresReviewer(job.role) ? `必须由独立 ${recommendedAgentForRole(job.role)} 审查角色执行,并在审查者来源字段(reviewer.kind/id)中记录来源,` : "") +
|
|
979
|
-
ordinaryReviewerFindingInstruction(job) +
|
|
980
1110
|
`产出 JSON 报告内容并优先通过 --report - 从 stdin 登记;文件路径模式仅作备用。${recordInputInstruction(job)}协议字段含义见 packet 顶层“字段说明”,普通对话不要原样复述 JSON。` +
|
|
981
1111
|
(isCodeReviewer
|
|
982
|
-
? `最小格式:{"role":"code-reviewer","verdict":"pass
|
|
1112
|
+
? `最小格式:{"role":"code-reviewer","verdict":"pass","review_scope":{"job_id":"${job.job_id}","packet_digest":"${job.packet_digest}","checked_paths":${JSON.stringify(job.boundFiles.map(f => f.path))},"checked_docs":${JSON.stringify(REVIEW_DOC_PATHS)},"unchecked":[]},"findings":[],"reviewer":{"kind":"codex-subagent","id":"<thread-or-agent-id>"}}。verdict 只能为 pass 或 fail;审查覆盖范围(review_scope)用来说明本次审查覆盖了哪些文件和文档,已检查路径(checked_paths)与未检查项(unchecked)必须合起来覆盖全部绑定文件(boundFiles),unchecked 条目格式为 {"path":"<path>","reason":"<reason>"}。`
|
|
983
1113
|
+ `报告结论为 fail 时,问题列表(findings)至少包含一个可处理、可追溯的阻塞问题,字段为 {"id":"<stable-id>","blocking":true,"type":"implementation|spec|mixed","description":"<what>","evidence":"<why>","source_refs":["<path:line>"],"impact":"<impact>","suggested_action":"apply|propose"}。问题类型(type)中 implementation 表示纯代码实现问题,spec 表示方案/需求文档问题,mixed 表示需要使用者判断的混合问题。`
|
|
984
1114
|
+ (packetContext?.task_execution_index
|
|
985
|
-
? `本工作项带任务执行索引(task_execution_index):按 task 对照其执行依据快照(contract)审查——实现路线对照 design 引用原文、累计 diff 对照 guard 边界、测试断言对照 tests 声明的 scenario;每项的 required_evidence 是 task-start 冻结的证据口径,red_required/green_required 分别说明是否需要 RED/GREEN;每项的 scope_note 是执行者登记的范围扩大说明,判断其合理性与验证充分性;changed_paths 是归属线索不是结论(null 表示未知);unattributed_paths 中的无主改动逐个判断合理性;coverage_exemption_refs 解释未绑定 task 的 TEST 豁免。`
|
|
1115
|
+
? `本工作项带任务执行索引(task_execution_index):按 task 对照其执行依据快照(contract)审查——实现路线对照 design 引用原文、累计 diff 对照 guard 边界、测试断言对照 tests 声明的 scenario;每项的 required_evidence 是 task-start 冻结的证据口径,red_required/green_required 分别说明是否需要 RED/GREEN;fix 非空表示状态机创建的实现修复,source、parent_task_id 和 reason 说明其归属,code_review 来源还需核对 review_finding;每项的 scope_note 是执行者登记的范围扩大说明,判断其合理性与验证充分性;changed_paths 是归属线索不是结论(null 表示未知);unattributed_paths 中的无主改动逐个判断合理性;coverage_exemption_refs 解释未绑定 task 的 TEST 豁免。`
|
|
986
1116
|
: "")
|
|
987
1117
|
: job.role === "verifier"
|
|
988
|
-
? `最小格式:{"role":"verifier","verdict":"pass
|
|
1118
|
+
? `最小格式:{"role":"verifier","verdict":"pass","findings":[]${hasReviewScope ? `,"review_scope":{"checked_paths":${JSON.stringify(job.boundFiles.map(file => file.path))}}` : ""}}。verdict 只能为 pass 或 fail;核对代码审查记录(code_review_gate):passed 必须能追溯到已接受的代码审查工作项,skipped 必须能证明本次没有代码类改动。核对修复闭环:task_execution_index.fix.source=code_review 时必须核对 review_finding 对应问题是否关闭;source=self_test 时必须核对 parent_task_id、记录的自测原因、本次 attempt 验证和最新代码审查是否共同闭环。方案/混合问题必须有用户决策或后续修复证据。按 task_execution_index 的 required_evidence 核对测试证据:red_required 时需要同一 TEST 的 RED(expected_failure)后 GREEN;green_required 时每个声明 TEST 都需要允许的 GREEN 语义状态;测试运行证据应包含测试 ID(test_id)、命令(command)、工作目录(cwd)、退出码(exit_code)、语义状态(semantic_status)。修复 task 的回归测试运行可用回归覆盖任务列表(covers_task_ids)说明覆盖了哪些已完成任务;缺少任务尝试 ID(attempt_id)的旧证据只能弱引用。` +
|
|
989
1119
|
(packetContext?.code_state_check
|
|
990
|
-
? `本工作项带代码状态检查(code_state_check
|
|
1120
|
+
? `本工作项带代码状态检查(code_state_check),它是创建 packet 时的快照:验证期间若代码状态已变化,不要提交该报告;主流程会通过 next 创建携带最新事实的验证工作项。`
|
|
991
1121
|
: "")
|
|
992
|
-
:
|
|
993
|
-
? `最小格式:{"role":"${job.role}","verdict":"pass
|
|
994
|
-
: `最小格式:{"role":"${job.role}","verdict":"pass
|
|
995
|
-
stop_conditions:
|
|
1122
|
+
: isReviewer
|
|
1123
|
+
? `最小格式:{"role":"${job.role}","verdict":"pass","findings":[]${hasReviewScope ? `,"review_scope":{"checked_paths":${JSON.stringify(job.boundFiles.map(file => file.path))}}` : ""},"reviewer":{"kind":"codex-subagent","id":"<thread-or-agent-id>"}}。verdict 只能为 pass 或 fail。`
|
|
1124
|
+
: `最小格式:{"role":"${job.role}","verdict":"pass","findings":[]}。verdict 只能为 pass 或 fail。`),
|
|
1125
|
+
stop_conditions: isReviewer
|
|
1126
|
+
? ["完成审查后提交报告,不要修改文档"]
|
|
1127
|
+
: job.role === "executor"
|
|
1128
|
+
? ["完成绑定范围内的实现后提交执行报告,不要修改未绑定范围"]
|
|
1129
|
+
: ["完成指定验证后提交测试报告,不要修改项目文档"],
|
|
996
1130
|
created_from_transition: job.created_from_transition,
|
|
997
1131
|
},
|
|
998
1132
|
message: `工作项 ${jobId} 的执行说明`,
|
package/dist/review.js
CHANGED
|
@@ -90,6 +90,8 @@ function reviewCycleState(gate) {
|
|
|
90
90
|
return "explore";
|
|
91
91
|
if (gate.gate_id === PROPOSE_FINAL_REVIEW_GATE_ID)
|
|
92
92
|
return "propose";
|
|
93
|
+
if (gate.gate_id === REVIEW_FINAL_VERIFIER_GATE.gate_id)
|
|
94
|
+
return "review";
|
|
93
95
|
return null;
|
|
94
96
|
}
|
|
95
97
|
function currentReviewGateCycleStart(events, gate) {
|
package/dist/task_evidence.js
CHANGED
|
@@ -4,6 +4,11 @@ import { parseTasksMd } from "./format.js";
|
|
|
4
4
|
import { readEvents, sha256Text } from "./store.js";
|
|
5
5
|
import { GREEN_ONLY_NO_TDD_REASON } from "./types.js";
|
|
6
6
|
export function taskEvidenceReadiness(projectRoot, change, changeRoot, attempt) {
|
|
7
|
+
// 有效证据计划是 task-start 在当时策略下冻结的事实。状态机新建的 Fix
|
|
8
|
+
// 即使来自历史/v1 Apply,也必须优先按这个快照回放。
|
|
9
|
+
if (attempt.required_evidence) {
|
|
10
|
+
return effectiveContractTaskEvidenceReadiness(projectRoot, change, attempt);
|
|
11
|
+
}
|
|
7
12
|
if (attempt.contract_mode === true) {
|
|
8
13
|
return contractTaskEvidenceReadiness(projectRoot, change, attempt);
|
|
9
14
|
}
|
|
@@ -52,9 +57,6 @@ export function taskEvidenceReadiness(projectRoot, change, changeRoot, attempt)
|
|
|
52
57
|
: { ready: false, missing, reason: missing.join("、") };
|
|
53
58
|
}
|
|
54
59
|
function contractTaskEvidenceReadiness(projectRoot, change, attempt) {
|
|
55
|
-
if (attempt.required_evidence) {
|
|
56
|
-
return effectiveContractTaskEvidenceReadiness(projectRoot, change, attempt);
|
|
57
|
-
}
|
|
58
60
|
return legacyContractTaskEvidenceReadiness(projectRoot, change, attempt);
|
|
59
61
|
}
|
|
60
62
|
/** 新执行依据模式:只消费 task-start 冻结的有效证据计划。 */
|
package/dist/transition.d.ts
CHANGED
|
@@ -40,6 +40,7 @@ export declare function taskStart(projectRoot: string, change: string, changeRoo
|
|
|
40
40
|
export declare function reopen(projectRoot: string, change: string, changeRoot: string, to: State, reason: string, opts?: {
|
|
41
41
|
reviewFix?: string;
|
|
42
42
|
reviewFinding?: string;
|
|
43
|
+
selfTestFix?: string;
|
|
43
44
|
}): TransitionResult;
|
|
44
45
|
export declare function reviewReady(projectRoot: string, change: string, changeRoot: string, risk?: ReviewRisk): TransitionResult;
|
|
45
46
|
export declare function accept(projectRoot: string, change: string, changeRoot: string): TransitionResult;
|