@peterxiaoyang/superspec 0.1.34 → 0.1.35
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/README.md +4 -4
- package/dist/cli.js +6 -2
- package/dist/code_review.d.ts +74 -0
- package/dist/code_review.js +323 -0
- package/dist/format.js +11 -1
- package/dist/next.js +80 -2
- package/dist/record.js +385 -58
- package/dist/review.js +17 -7
- package/dist/store.d.ts +1 -0
- package/dist/store.js +10 -0
- package/dist/sync.js +12 -6
- package/dist/task.js +19 -0
- package/dist/transition.d.ts +4 -1
- package/dist/transition.js +244 -26
- package/dist/types.d.ts +15 -1
- package/package.json +1 -1
- package/templates/workflow/agents/architect.toml +2 -2
- package/templates/workflow/agents/code-reviewer.toml +5 -5
- package/templates/workflow/agents/critic.toml +2 -2
- package/templates/workflow/agents/test-engineer.toml +3 -3
- package/templates/workflow/agents/verifier.toml +4 -4
- package/templates/workflow/prompts/architect.md +9 -20
- package/templates/workflow/prompts/code-reviewer.md +56 -16
- package/templates/workflow/prompts/critic.md +20 -33
- package/templates/workflow/prompts/test-engineer.md +15 -26
- package/templates/workflow/prompts/test-runner.md +1 -0
- package/templates/workflow/prompts/verifier.md +27 -46
- package/templates/workflow/skills/superspec-apply/SKILL.md +46 -41
- package/templates/workflow/skills/superspec-review/SKILL.md +65 -28
package/dist/sync.js
CHANGED
|
@@ -3,6 +3,7 @@ import { readFileSync, existsSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { readEvents, eventsDigest, computeDocumentDigests, sha256Text, ensureChangeLayout, } from "./store.js";
|
|
5
5
|
import { reviewEvidenceDigest, reviewVerifierStaleReason } from "./review.js";
|
|
6
|
+
import { codeReviewJobStaleReason } from "./code_review.js";
|
|
6
7
|
const TRACKED_DOCS = [
|
|
7
8
|
"proposal.md", "design.md", "tasks.md",
|
|
8
9
|
".superspec/artifacts/discovery.md",
|
|
@@ -80,10 +81,12 @@ function replayEvents(events) {
|
|
|
80
81
|
return { state, openJobs, acceptedJobs, activeAttempts, taskStatuses, lastTransition };
|
|
81
82
|
}
|
|
82
83
|
/** 粗粒度失效:检查 job 的 boundFiles 是否仍匹配当前文档 */
|
|
83
|
-
function checkStaleJobs(jobs, changeRoot, currentReviewEvidenceDigest) {
|
|
84
|
+
function checkStaleJobs(jobs, projectRoot, changeRoot, currentReviewEvidenceDigest) {
|
|
84
85
|
const stale = [];
|
|
85
86
|
for (const job of jobs) {
|
|
86
|
-
const reason =
|
|
87
|
+
const reason = job.role === "code-reviewer"
|
|
88
|
+
? codeReviewJobStaleReason(projectRoot, job)
|
|
89
|
+
: reviewVerifierStaleReason(job, changeRoot, currentReviewEvidenceDigest);
|
|
87
90
|
if (reason) {
|
|
88
91
|
stale.push({ job_id: job.job_id, reason });
|
|
89
92
|
}
|
|
@@ -109,10 +112,13 @@ export function rebuildSnapshot(projectRoot, change, changeRoot, openspecStatusD
|
|
|
109
112
|
const tsDigest = tasksStructureDigest(changeRoot);
|
|
110
113
|
const { state, openJobs, acceptedJobs, activeAttempts, taskStatuses, lastTransition } = replayEvents(events);
|
|
111
114
|
const currentReviewEvidenceDigest = reviewEvidenceDigest(events);
|
|
112
|
-
// 粗粒度失效检查(只读,不写事件):
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
const
|
|
115
|
+
// 粗粒度失效检查(只读,不写事件):open code-reviewer job 防止提交过期报告;
|
|
116
|
+
// accepted code-reviewer pass 不做持续 freshness gate,避免 apply_done 循环重审。
|
|
117
|
+
const staleOpenInfo = checkStaleJobs(openJobs, projectRoot, changeRoot, currentReviewEvidenceDigest);
|
|
118
|
+
const staleAcceptedInfo = checkStaleJobs(acceptedJobs.filter(j => j.role !== "code-reviewer"), projectRoot, changeRoot, currentReviewEvidenceDigest);
|
|
119
|
+
const freshOpen = openJobs
|
|
120
|
+
.filter(j => !staleOpenInfo.some(s => s.job_id === j.job_id))
|
|
121
|
+
.filter(j => j.role !== "code-reviewer" || state === "apply_done");
|
|
116
122
|
const freshAccepted = acceptedJobs.filter(j => !staleAcceptedInfo.some(s => s.job_id === j.job_id));
|
|
117
123
|
return {
|
|
118
124
|
change_id: change,
|
package/dist/task.js
CHANGED
|
@@ -22,10 +22,29 @@ function recordTestRunLoaded(projectRoot, change, content) {
|
|
|
22
22
|
if (!tr.test_id || !tr.task_structure_digest) {
|
|
23
23
|
return { accepted: false, message: "缺少 test_id 或 task_structure_digest" };
|
|
24
24
|
}
|
|
25
|
+
let coversTaskIds;
|
|
26
|
+
if (tr.covers_task_ids !== undefined) {
|
|
27
|
+
if (!Array.isArray(tr.covers_task_ids)) {
|
|
28
|
+
return { accepted: false, message: "covers_task_ids 必须是字符串数组" };
|
|
29
|
+
}
|
|
30
|
+
if (tr.covers_task_ids.length === 0) {
|
|
31
|
+
return { accepted: false, message: "covers_task_ids 不能是空数组" };
|
|
32
|
+
}
|
|
33
|
+
for (const raw of tr.covers_task_ids) {
|
|
34
|
+
if (typeof raw !== "string")
|
|
35
|
+
return { accepted: false, message: "covers_task_ids 必须是字符串数组" };
|
|
36
|
+
const value = raw.trim();
|
|
37
|
+
if (!value)
|
|
38
|
+
return { accepted: false, message: "covers_task_ids 不能包含空字符串" };
|
|
39
|
+
(coversTaskIds ??= []).push(value);
|
|
40
|
+
}
|
|
41
|
+
coversTaskIds = [...new Set(coversTaskIds)].sort();
|
|
42
|
+
}
|
|
25
43
|
const normalizedTestRun = {
|
|
26
44
|
test_id: tr.test_id,
|
|
27
45
|
task_structure_digest: tr.task_structure_digest,
|
|
28
46
|
attempt_id: tr.attempt_id ?? null,
|
|
47
|
+
...(coversTaskIds ? { covers_task_ids: coversTaskIds } : {}),
|
|
29
48
|
command: tr.command ?? "",
|
|
30
49
|
cwd: tr.cwd ?? "",
|
|
31
50
|
exit_code: tr.exit_code ?? -1,
|
package/dist/transition.d.ts
CHANGED
|
@@ -36,7 +36,10 @@ export declare function transitionInit(projectRoot: string, change: string, chan
|
|
|
36
36
|
export declare function transitionExplore(projectRoot: string, change: string, changeRoot: string, risk?: "minimal" | "normal" | "strict"): TransitionResult;
|
|
37
37
|
export declare function startApply(projectRoot: string, change: string, changeRoot: string): TransitionResult;
|
|
38
38
|
export declare function taskStart(projectRoot: string, change: string, changeRoot: string, taskId: string): TransitionResult;
|
|
39
|
-
export declare function reopen(projectRoot: string, change: string, changeRoot: string, to: State, reason: string
|
|
39
|
+
export declare function reopen(projectRoot: string, change: string, changeRoot: string, to: State, reason: string, opts?: {
|
|
40
|
+
reviewFix?: string;
|
|
41
|
+
reviewFinding?: string;
|
|
42
|
+
}): TransitionResult;
|
|
40
43
|
export declare function reviewReady(projectRoot: string, change: string, changeRoot: string, risk?: "minimal" | "normal" | "strict"): TransitionResult;
|
|
41
44
|
export declare function accept(projectRoot: string, change: string, changeRoot: string): TransitionResult;
|
|
42
45
|
export declare function archive(projectRoot: string, change: string, changeRoot: string): TransitionResult;
|
package/dist/transition.js
CHANGED
|
@@ -4,7 +4,8 @@ import { existsSync, readFileSync, writeFileSync, readdirSync } from "node:fs";
|
|
|
4
4
|
import { ensureChangeLayout, readEvents, appendEvent, makeEvent, writeSnapshot, snapshotDigest, withLock, idempotencyKey, sha256File, sha256Text, } from "./store.js";
|
|
5
5
|
import { rebuildSnapshot } from "./sync.js";
|
|
6
6
|
import { requiredJobActions } from "./job_action.js";
|
|
7
|
-
import { assertCommitPayloadExtension, isFreshReviewVerifier, isReviewReadyVerifier, readReviewPolicyFromEvents, reviewBoundFiles, reviewEvidenceDigest, reviewPolicyForRisk, } from "./review.js";
|
|
7
|
+
import { assertCommitPayloadExtension, isFreshReviewVerifier, isReviewReadyVerifier, readReviewPolicyFromEvents, reviewBoundFiles, reviewEvidenceDigest, reviewPolicyForRisk, REVIEW_DOC_PATHS, } from "./review.js";
|
|
8
|
+
import { codeReviewBoundFiles, codeReviewDecisionScope, codeReviewJobStaleReason, codeReviewPacketDigest, collectCodeReviewGateFacts, dismissedCodeReviewSummary, latestCodeReviewDecision, latestCodeReviewFailedStatus, requiresFinalVerifierForCurrentReview, scanCodeChanges, } from "./code_review.js";
|
|
8
9
|
import { validateDiscovery, collectProposeOpenQuestions, findTaskInLines, parseTasksMd, pendingTasksInContent, tasksStructureDigest } from "./format.js";
|
|
9
10
|
let transitionSeq = 0;
|
|
10
11
|
function newTransitionId() { return `T-${Date.now()}-${++transitionSeq}`; }
|
|
@@ -101,6 +102,106 @@ function hasRejectedReviewReadyVerifier(events) {
|
|
|
101
102
|
return events.some(ev => ev.event_type === "job_rejected" &&
|
|
102
103
|
reviewReadyVerifierIds.has(ev.payload.job_id ?? ""));
|
|
103
104
|
}
|
|
105
|
+
function createCodeReviewerJob(change, projectRoot, events) {
|
|
106
|
+
const scan = scanCodeChanges(projectRoot);
|
|
107
|
+
const boundFiles = codeReviewBoundFiles(projectRoot, scan.paths);
|
|
108
|
+
const facts = collectCodeReviewGateFacts(events);
|
|
109
|
+
const latestRejected = facts.latestRejected;
|
|
110
|
+
const reviewFailedStatus = latestCodeReviewFailedStatus(events);
|
|
111
|
+
const previousRejection = latestRejected && latestRejected.state === "rejected"
|
|
112
|
+
? {
|
|
113
|
+
result_kind: latestRejected.result_kind ?? "invalid_report",
|
|
114
|
+
reason: latestRejected.result_kind === "review_failed" && reviewFailedStatus && reviewFailedStatus.unresolved.length === 0 && reviewFailedStatus.dismissed.length > 0
|
|
115
|
+
? dismissedCodeReviewSummary(reviewFailedStatus)
|
|
116
|
+
: latestRejected.reason ?? "缺少拒绝原因",
|
|
117
|
+
job_id: latestRejected.job.job_id,
|
|
118
|
+
}
|
|
119
|
+
: undefined;
|
|
120
|
+
const packetInput = {
|
|
121
|
+
role: "code-reviewer",
|
|
122
|
+
boundFiles,
|
|
123
|
+
checkedDocs: REVIEW_DOC_PATHS,
|
|
124
|
+
created_from_transition: "review-ready",
|
|
125
|
+
...(previousRejection ? { previous_rejection: previousRejection } : {}),
|
|
126
|
+
};
|
|
127
|
+
return {
|
|
128
|
+
scanReason: scan.reason,
|
|
129
|
+
job: {
|
|
130
|
+
job_id: newJobId(change, "code-reviewer"),
|
|
131
|
+
role: "code-reviewer",
|
|
132
|
+
state: "requested",
|
|
133
|
+
boundFiles,
|
|
134
|
+
packet_digest: codeReviewPacketDigest(packetInput),
|
|
135
|
+
created_from_transition: "review-ready",
|
|
136
|
+
created_at: new Date().toISOString(),
|
|
137
|
+
...(previousRejection ? { previous_rejection: previousRejection } : {}),
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function parseCodeReviewFindingRef(value) {
|
|
142
|
+
const idx = value.indexOf("#");
|
|
143
|
+
if (idx <= 0 || idx === value.length - 1)
|
|
144
|
+
return null;
|
|
145
|
+
return { jobId: value.slice(0, idx), findingId: value.slice(idx + 1) };
|
|
146
|
+
}
|
|
147
|
+
function findReviewFailedFinding(events, ref) {
|
|
148
|
+
const status = latestCodeReviewFailedStatus(events);
|
|
149
|
+
if (!status || status.terminal.job.job_id !== ref.jobId)
|
|
150
|
+
return null;
|
|
151
|
+
const finding = status.findings.find(item => item.id === ref.findingId)?.finding;
|
|
152
|
+
if (!finding)
|
|
153
|
+
return null;
|
|
154
|
+
return { event: status.terminal.event, finding };
|
|
155
|
+
}
|
|
156
|
+
function reviewFixMarker(ref) {
|
|
157
|
+
return `review_fix_of:${ref.jobId}#${ref.findingId}`;
|
|
158
|
+
}
|
|
159
|
+
function reviewFixTaskId(ref) {
|
|
160
|
+
return `REVIEW-FIX-${ref.jobId}#${ref.findingId}`;
|
|
161
|
+
}
|
|
162
|
+
function appendReviewFixTask(changeRoot, ref, finding) {
|
|
163
|
+
const tasksPath = join(changeRoot, "tasks.md");
|
|
164
|
+
const content = readFileSync(tasksPath, "utf8");
|
|
165
|
+
const marker = reviewFixMarker(ref);
|
|
166
|
+
if (content.includes(marker))
|
|
167
|
+
return "exists";
|
|
168
|
+
const description = typeof finding.description === "string" && finding.description.trim()
|
|
169
|
+
? finding.description.trim().replace(/\s+/g, " ")
|
|
170
|
+
: `修复代码审查问题 ${ref.findingId}`;
|
|
171
|
+
const line = `- [ ] ${reviewFixTaskId(ref)} ${description} tdd_required:true ${marker}`;
|
|
172
|
+
const suffix = content.endsWith("\n") ? "" : "\n";
|
|
173
|
+
writeFileSync(tasksPath, `${content}${suffix}${line}\n`);
|
|
174
|
+
return "created";
|
|
175
|
+
}
|
|
176
|
+
function isFreshOpenCodeReviewerJob(job, projectRoot) {
|
|
177
|
+
return codeReviewJobStaleReason(projectRoot, job) == null;
|
|
178
|
+
}
|
|
179
|
+
function documentBaseline(changeRoot) {
|
|
180
|
+
const docs = ["proposal.md", "design.md", "tasks.md", ".superspec/artifacts/test-contract.md"];
|
|
181
|
+
const baseline = {};
|
|
182
|
+
for (const doc of docs) {
|
|
183
|
+
baseline[doc] = sha256File(join(changeRoot, doc)) ?? "sha256:missing";
|
|
184
|
+
}
|
|
185
|
+
return baseline;
|
|
186
|
+
}
|
|
187
|
+
function latestReopenProposeBaseline(events) {
|
|
188
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
189
|
+
const ev = events[i];
|
|
190
|
+
if (ev.event_type !== "transition_commit")
|
|
191
|
+
continue;
|
|
192
|
+
const payload = ev.payload;
|
|
193
|
+
if (payload.transition !== "reopen" || payload.reopen_target !== "propose")
|
|
194
|
+
continue;
|
|
195
|
+
if (!payload.baseline_docs || typeof payload.baseline_docs !== "object" || Array.isArray(payload.baseline_docs))
|
|
196
|
+
return null;
|
|
197
|
+
return payload.baseline_docs;
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
function proposalDocsChangedSinceBaseline(changeRoot, baseline) {
|
|
202
|
+
const current = documentBaseline(changeRoot);
|
|
203
|
+
return Object.entries(baseline).some(([path, digest]) => current[path] !== digest);
|
|
204
|
+
}
|
|
104
205
|
/**
|
|
105
206
|
* 统一 transition 提交协议——所有校验在锁内。
|
|
106
207
|
*/
|
|
@@ -265,13 +366,18 @@ export function startApply(projectRoot, change, changeRoot) {
|
|
|
265
366
|
decide: (snapshot) => {
|
|
266
367
|
if (snapshot.state !== "propose_ready")
|
|
267
368
|
return { skip: true, message: `当前状态 ${snapshot.state},需要 propose_ready` };
|
|
369
|
+
const events = readEvents(projectRoot, change);
|
|
370
|
+
const reopenBaseline = latestReopenProposeBaseline(events);
|
|
371
|
+
if (reopenBaseline && !proposalDocsChangedSinceBaseline(changeRoot, reopenBaseline)) {
|
|
372
|
+
return { skip: true, message: "回到 propose 后 proposal/design/tasks/test-contract 至少一个文档必须变化" };
|
|
373
|
+
}
|
|
268
374
|
const reviewedRoles = historicalProposeReadyRoles(projectRoot, change);
|
|
269
375
|
if (reviewedRoles.length > 0) {
|
|
270
376
|
const reviewResult = checkOrCreateReviewJobs(snapshot, reviewedRoles, changeRoot, change, "propose-ready", ["proposal.md", "tasks.md", "design.md", ".superspec/artifacts/discovery.md", ".superspec/artifacts/business-invariants.md", ".superspec/artifacts/test-contract.md"]);
|
|
271
377
|
if (reviewResult) {
|
|
272
378
|
return {
|
|
273
379
|
...reviewResult,
|
|
274
|
-
reason:
|
|
380
|
+
reason: `进入执行阶段前需要重新完成计划文档审查:${reviewResult.reason}`,
|
|
275
381
|
};
|
|
276
382
|
}
|
|
277
383
|
}
|
|
@@ -317,14 +423,83 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
|
|
|
317
423
|
});
|
|
318
424
|
}
|
|
319
425
|
// ===== reopen =====
|
|
320
|
-
export function reopen(projectRoot, change, changeRoot, to, reason) {
|
|
426
|
+
export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
321
427
|
return commitTransition(projectRoot, change, changeRoot, {
|
|
322
|
-
name: "reopen", idempotencyInputs: { to, reason },
|
|
428
|
+
name: "reopen", idempotencyInputs: { to, reason, reviewFix: opts.reviewFix ?? "", reviewFinding: opts.reviewFinding ?? "" },
|
|
323
429
|
decide: (snapshot) => {
|
|
324
|
-
if (to !== "apply")
|
|
325
|
-
return { skip: true, message: `reopen 当前只支持 --to apply,不支持 ${to}` };
|
|
326
430
|
if (!reason || reason.trim() === "")
|
|
327
431
|
return { skip: true, message: "reopen 需要非空 --reason" };
|
|
432
|
+
const events = readEvents(projectRoot, change);
|
|
433
|
+
if (opts.reviewFinding) {
|
|
434
|
+
if (to !== "propose")
|
|
435
|
+
return { skip: true, message: "--review-finding 只能用于回到计划阶段(reopen --to propose)" };
|
|
436
|
+
if (snapshot.state !== "apply_done")
|
|
437
|
+
return { skip: true, message: `当前状态 ${snapshot.state},不能通过代码审查问题回到计划阶段` };
|
|
438
|
+
const ref = parseCodeReviewFindingRef(opts.reviewFinding);
|
|
439
|
+
if (!ref)
|
|
440
|
+
return { skip: true, message: "--review-finding 必须是 <job_id>#<finding_id>" };
|
|
441
|
+
const found = findReviewFailedFinding(events, ref);
|
|
442
|
+
if (!found)
|
|
443
|
+
return { skip: true, message: `找不到有效的代码审查问题 ${opts.reviewFinding}` };
|
|
444
|
+
const type = found.finding.type;
|
|
445
|
+
if (type !== "spec" && type !== "mixed")
|
|
446
|
+
return { skip: true, message: "只有方案/需求文档问题或混合问题可以回到计划阶段" };
|
|
447
|
+
const scope = codeReviewDecisionScope(ref.jobId, ref.findingId);
|
|
448
|
+
const decision = latestCodeReviewDecision(events, scope);
|
|
449
|
+
if (decision?.answer !== "reopen_propose")
|
|
450
|
+
return { skip: true, message: `缺少使用者确认:需要先确认问题 ${ref.findingId} 是否回到计划阶段` };
|
|
451
|
+
return {
|
|
452
|
+
fromState: "apply_done",
|
|
453
|
+
toState: "propose",
|
|
454
|
+
outcome: "advanced",
|
|
455
|
+
reason: reason.trim(),
|
|
456
|
+
commitPayload: {
|
|
457
|
+
reopen_target: "propose",
|
|
458
|
+
source_job_id: ref.jobId,
|
|
459
|
+
finding_id: ref.findingId,
|
|
460
|
+
decision_scope: scope,
|
|
461
|
+
baseline_docs: documentBaseline(changeRoot),
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
if (opts.reviewFix) {
|
|
466
|
+
if (to !== "apply")
|
|
467
|
+
return { skip: true, message: "--review-fix 只能用于回到实现阶段(reopen --to apply)" };
|
|
468
|
+
if (snapshot.state !== "apply_done")
|
|
469
|
+
return { skip: true, message: `当前状态 ${snapshot.state},不能通过代码审查修复回到实现阶段` };
|
|
470
|
+
const ref = parseCodeReviewFindingRef(opts.reviewFix);
|
|
471
|
+
if (!ref)
|
|
472
|
+
return { skip: true, message: "--review-fix 必须是 <job_id>#<finding_id>" };
|
|
473
|
+
const found = findReviewFailedFinding(events, ref);
|
|
474
|
+
if (!found)
|
|
475
|
+
return { skip: true, message: `找不到有效的代码审查问题 ${opts.reviewFix}` };
|
|
476
|
+
const type = found.finding.type;
|
|
477
|
+
if (type === "spec" || type === "mixed") {
|
|
478
|
+
const scope = codeReviewDecisionScope(ref.jobId, ref.findingId);
|
|
479
|
+
const decision = latestCodeReviewDecision(events, scope);
|
|
480
|
+
if (decision?.answer !== "reopen_apply")
|
|
481
|
+
return { skip: true, message: `缺少使用者确认:需要先确认问题 ${ref.findingId} 是否直接回到实现阶段修复` };
|
|
482
|
+
}
|
|
483
|
+
else if (type !== "implementation") {
|
|
484
|
+
return { skip: true, message: "这个代码审查问题不能直接回到实现阶段处理" };
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
fromState: "apply_done",
|
|
488
|
+
toState: "apply",
|
|
489
|
+
outcome: "advanced",
|
|
490
|
+
reason: reason.trim(),
|
|
491
|
+
commitPayload: {
|
|
492
|
+
review_fix_of: `${ref.jobId}#${ref.findingId}`,
|
|
493
|
+
source_job_id: ref.jobId,
|
|
494
|
+
finding_id: ref.findingId,
|
|
495
|
+
},
|
|
496
|
+
postCommit: (_pr, _ch, cr) => {
|
|
497
|
+
appendReviewFixTask(cr, ref, found.finding);
|
|
498
|
+
},
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
if (to !== "apply")
|
|
502
|
+
return { skip: true, message: `reopen 当前只支持 --to apply 或 --to propose,不支持 ${to}` };
|
|
328
503
|
if (snapshot.state !== "apply_done" && snapshot.state !== "review") {
|
|
329
504
|
return { skip: true, message: `当前状态 ${snapshot.state},不能 reopen 到 apply` };
|
|
330
505
|
}
|
|
@@ -362,19 +537,69 @@ export function reviewReady(projectRoot, change, changeRoot, risk = "strict") {
|
|
|
362
537
|
commitPayload: policyPayload,
|
|
363
538
|
};
|
|
364
539
|
}
|
|
365
|
-
if (snapshot.state === "apply_done"
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
540
|
+
if (snapshot.state === "apply_done") {
|
|
541
|
+
const scan = scanCodeChanges(projectRoot);
|
|
542
|
+
const facts = collectCodeReviewGateFacts(events);
|
|
543
|
+
if (scan.hasCodeChanges) {
|
|
544
|
+
const freshOpenJobs = facts.openJobs.filter(job => isFreshOpenCodeReviewerJob(job, projectRoot));
|
|
545
|
+
if (freshOpenJobs.length > 0) {
|
|
546
|
+
return {
|
|
547
|
+
blocked: true,
|
|
548
|
+
reason: `状态未推进;已有待完成代码审查工作项 ${freshOpenJobs[0].job_id}`,
|
|
549
|
+
jobs: [freshOpenJobs[0]],
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
const latest = facts.latestTerminal;
|
|
553
|
+
if (latest?.state === "accepted") {
|
|
372
554
|
return {
|
|
373
555
|
fromState: "apply_done", toState: "review", outcome: "advanced",
|
|
374
|
-
reason:
|
|
375
|
-
commitPayload:
|
|
556
|
+
reason: "代码审查已通过,进入最终审查阶段",
|
|
557
|
+
commitPayload: {
|
|
558
|
+
...policyPayload,
|
|
559
|
+
code_review_gate: { decision: "passed", job_id: latest.job.job_id },
|
|
560
|
+
},
|
|
376
561
|
};
|
|
377
562
|
}
|
|
563
|
+
if (latest?.state === "rejected" && latest.result_kind === "review_failed") {
|
|
564
|
+
const reviewFailedStatus = latestCodeReviewFailedStatus(events);
|
|
565
|
+
if (reviewFailedStatus && reviewFailedStatus.findings.length > 0 && reviewFailedStatus.unresolved.length === 0) {
|
|
566
|
+
const { job, scanReason } = createCodeReviewerJob(change, projectRoot, events);
|
|
567
|
+
return {
|
|
568
|
+
fromState: "apply_done", toState: "apply_done", outcome: "job_created",
|
|
569
|
+
newJobs: [job],
|
|
570
|
+
reason: `重新创建代码审查工作项;${scanReason};上一次阻塞问题已被主流程复核驳回`,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
return {
|
|
574
|
+
skip: true,
|
|
575
|
+
message: "代码审查发现需要处理的问题,请先执行 next,根据提示回到实现阶段修复或让使用者决定是否回到计划阶段",
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
const { job, scanReason } = createCodeReviewerJob(change, projectRoot, events);
|
|
579
|
+
return {
|
|
580
|
+
fromState: "apply_done", toState: "apply_done", outcome: "job_created",
|
|
581
|
+
newJobs: [job],
|
|
582
|
+
reason: latest?.state === "rejected"
|
|
583
|
+
? `重新创建代码审查工作项;上一次报告未被接受,原因:${latest.reason ?? "报告不符合要求"}`
|
|
584
|
+
: `创建代码审查工作项;${scanReason}`,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
return {
|
|
588
|
+
fromState: "apply_done", toState: "review", outcome: "advanced",
|
|
589
|
+
reason: "没有代码类改动,直接进入最终审查阶段",
|
|
590
|
+
commitPayload: {
|
|
591
|
+
...policyPayload,
|
|
592
|
+
code_review_gate: { decision: "skipped", reason: "no_code_changes" },
|
|
593
|
+
},
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
if (snapshot.state === "review") {
|
|
597
|
+
const verifierOpen = snapshot.open_jobs.find(isReviewReadyVerifier);
|
|
598
|
+
if (verifierOpen)
|
|
599
|
+
return { blocked: true, reason: `状态未推进;已有待完成最终验证工作项 ${verifierOpen.job_id}`, jobs: [verifierOpen] };
|
|
600
|
+
const verifierAccepted = snapshot.accepted_jobs.find(job => isFreshReviewVerifier(job, changeRoot, currentEvidenceDigest));
|
|
601
|
+
const finalVerifierRequired = requiresFinalVerifierForCurrentReview(events) || policy.requires_verifier;
|
|
602
|
+
if (!finalVerifierRequired) {
|
|
378
603
|
if (!storedPolicy) {
|
|
379
604
|
return {
|
|
380
605
|
fromState: "review", toState: "review", outcome: "advanced",
|
|
@@ -402,21 +627,14 @@ export function reviewReady(projectRoot, change, changeRoot, risk = "strict") {
|
|
|
402
627
|
fromState: snapshot.state, toState: snapshot.state, outcome: "job_created",
|
|
403
628
|
newJobs: [job],
|
|
404
629
|
reason: previousVerifierRejected
|
|
405
|
-
? "
|
|
630
|
+
? "此前最终验证未通过;请先根据验证报告修改任务或文档,确认无需修改时再执行新的最终验证工作项"
|
|
406
631
|
: "创建最终验证工作项",
|
|
407
632
|
commitPayload: policyPayload,
|
|
408
633
|
...(previousVerifierRejected ? {
|
|
409
|
-
details: { advisory: "
|
|
634
|
+
details: { advisory: "此前最终验证未通过;请先根据验证报告修改任务或文档,确认无需修改时再执行新的最终验证工作项" },
|
|
410
635
|
} : {}),
|
|
411
636
|
};
|
|
412
637
|
}
|
|
413
|
-
if (snapshot.state === "apply_done") {
|
|
414
|
-
return {
|
|
415
|
-
fromState: "apply_done", toState: "review", outcome: "advanced",
|
|
416
|
-
reason: "最终验证已接受,进入审查阶段",
|
|
417
|
-
commitPayload: policyPayload,
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
638
|
if (!storedPolicy) {
|
|
421
639
|
return {
|
|
422
640
|
fromState: "review", toState: "review", outcome: "advanced",
|
|
@@ -444,11 +662,11 @@ export function accept(projectRoot, change, changeRoot) {
|
|
|
444
662
|
const policy = readReviewPolicyFromEvents(events);
|
|
445
663
|
if (!policy)
|
|
446
664
|
return { skip: true, message: "缺少审查策略,请先运行 review-ready" };
|
|
447
|
-
if (policy.requires_verifier) {
|
|
665
|
+
if (requiresFinalVerifierForCurrentReview(events) || policy.requires_verifier) {
|
|
448
666
|
const currentEvidenceDigest = reviewEvidenceDigest(events);
|
|
449
667
|
const verifierAccepted = snapshot.accepted_jobs.find(job => isFreshReviewVerifier(job, changeRoot, currentEvidenceDigest));
|
|
450
668
|
if (!verifierAccepted)
|
|
451
|
-
return { skip: true, message: "
|
|
669
|
+
return { skip: true, message: "缺少仍然匹配当前证据的最终验证,请先运行 review-ready" };
|
|
452
670
|
}
|
|
453
671
|
return { fromState: "review", toState: "accepted", outcome: "advanced", reason: "审查通过" };
|
|
454
672
|
},
|
package/dist/types.d.ts
CHANGED
|
@@ -5,7 +5,13 @@ export type Ref = {
|
|
|
5
5
|
sha: string;
|
|
6
6
|
};
|
|
7
7
|
export type JobState = "requested" | "accepted" | "rejected";
|
|
8
|
-
export type JobRole = "critic" | "architect" | "test-engineer" | "executor" | "test-run" | "verifier";
|
|
8
|
+
export type JobRole = "critic" | "architect" | "test-engineer" | "executor" | "test-run" | "verifier" | "code-reviewer";
|
|
9
|
+
export type CodeReviewResultKind = "invalid_report" | "non_actionable_report" | "review_failed";
|
|
10
|
+
export interface CodeReviewPreviousRejection {
|
|
11
|
+
result_kind: CodeReviewResultKind;
|
|
12
|
+
reason: string;
|
|
13
|
+
job_id: string;
|
|
14
|
+
}
|
|
9
15
|
export interface Job {
|
|
10
16
|
job_id: string;
|
|
11
17
|
role: JobRole;
|
|
@@ -15,6 +21,7 @@ export interface Job {
|
|
|
15
21
|
packet_digest: string;
|
|
16
22
|
created_from_transition: string;
|
|
17
23
|
created_at: string;
|
|
24
|
+
previous_rejection?: CodeReviewPreviousRejection;
|
|
18
25
|
}
|
|
19
26
|
export interface JobPacket {
|
|
20
27
|
job_id: string;
|
|
@@ -30,6 +37,7 @@ export interface JobPacket {
|
|
|
30
37
|
file_fallback?: boolean;
|
|
31
38
|
output_contract_fields?: string[];
|
|
32
39
|
output_contract_optional_fields?: string[];
|
|
40
|
+
output_instructions?: string;
|
|
33
41
|
stop_conditions: string[];
|
|
34
42
|
created_from_transition: string;
|
|
35
43
|
}
|
|
@@ -66,6 +74,11 @@ export interface TransitionCommitPayload {
|
|
|
66
74
|
review_risk: "minimal" | "normal" | "strict";
|
|
67
75
|
requires_verifier: boolean;
|
|
68
76
|
};
|
|
77
|
+
code_review_gate?: {
|
|
78
|
+
decision: "passed" | "skipped";
|
|
79
|
+
job_id?: string;
|
|
80
|
+
reason?: "no_code_changes";
|
|
81
|
+
};
|
|
69
82
|
}
|
|
70
83
|
export interface Snapshot {
|
|
71
84
|
change_id: string;
|
|
@@ -100,6 +113,7 @@ export interface TestRun {
|
|
|
100
113
|
test_id: string;
|
|
101
114
|
attempt_id?: string | null;
|
|
102
115
|
task_structure_digest: string;
|
|
116
|
+
covers_task_ids?: string[];
|
|
103
117
|
command: string;
|
|
104
118
|
cwd: string;
|
|
105
119
|
exit_code: number;
|
package/package.json
CHANGED
|
@@ -5,9 +5,9 @@ model_reasoning_effort = "high"
|
|
|
5
5
|
developer_instructions = """
|
|
6
6
|
Role: Architect. Review system boundaries, interface contracts, data flow, maintenance risk, rollback risk, and design tradeoffs.
|
|
7
7
|
|
|
8
|
-
Task binding: load `.codex/prompts/architect.md` first, then read the current task instructions.
|
|
8
|
+
Task binding: load `.codex/prompts/architect.md` first, then read the current job packet and task instructions. The job packet is the runtime contract; follow it over static prompt memory, including any previous rejection it asks you to correct.
|
|
9
9
|
|
|
10
10
|
Boundary: read-only. Do not edit files or judge materials you have not opened. Report missing context upward instead of guessing.
|
|
11
11
|
|
|
12
|
-
Output: concise Simplified Chinese. For
|
|
12
|
+
Output: concise Simplified Chinese. For JSON reports, follow the packet's report contract exactly. Otherwise put the conclusion first, cite file:line evidence, and write `无阻塞问题` when no blocking issue is found.
|
|
13
13
|
"""
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# SuperSpec Codex agent: code-reviewer
|
|
2
2
|
name = "code-reviewer"
|
|
3
|
-
description = "
|
|
3
|
+
description = "Code-level review for spec fit, bugs, safety, and test gaps"
|
|
4
4
|
model_reasoning_effort = "high"
|
|
5
5
|
developer_instructions = """
|
|
6
|
-
Role: Code Reviewer.
|
|
6
|
+
Role: Code Reviewer. Check spec fit, correctness, security, test adequacy, code quality, performance, and maintainability without making the workflow heavy.
|
|
7
7
|
|
|
8
|
-
Task binding: load `.codex/prompts/code-reviewer.md` first, then read the current task instructions.
|
|
8
|
+
Task binding: load `.codex/prompts/code-reviewer.md` first, then read the current job packet and task instructions. The job packet is the runtime contract; follow it over static prompt memory, including any previous rejection it asks you to correct.
|
|
9
9
|
|
|
10
|
-
Boundary: read-only. Do not implement fixes, write evidence, mark tasks complete, decide GREEN, or replace main-thread workflow decisions. Start from
|
|
10
|
+
Boundary: read-only. Do not implement fixes, write evidence, mark tasks complete, decide GREEN, reopen, accept, or replace main-thread workflow decisions. Start from packet-provided materials and report missing context upward instead of guessing.
|
|
11
11
|
|
|
12
|
-
Output: concise Simplified Chinese.
|
|
12
|
+
Output: concise Simplified Chinese. For JSON reports, follow the packet's report contract exactly. Blocking issues must be traceable and actionable. Write `无阻塞问题` when no blocking issue is found.
|
|
13
13
|
"""
|
|
@@ -5,9 +5,9 @@ model_reasoning_effort = "high"
|
|
|
5
5
|
developer_instructions = """
|
|
6
6
|
Role: Critic. Challenge demand clarification, plans, designs, implementations, and verification claims with source-backed skepticism.
|
|
7
7
|
|
|
8
|
-
Task binding: load `.codex/prompts/critic.md` first, then read the current task instructions.
|
|
8
|
+
Task binding: load `.codex/prompts/critic.md` first, then read the current job packet and task instructions. The job packet is the runtime contract; follow it over static prompt memory, including any previous rejection it asks you to correct.
|
|
9
9
|
|
|
10
10
|
Boundary: read-only by default. Do not edit files, invent issues, or widen scope silently. Report missing source refs or claim gaps upward.
|
|
11
11
|
|
|
12
|
-
Output: concise Simplified Chinese. For
|
|
12
|
+
Output: concise Simplified Chinese. For JSON reports, follow the packet's report contract exactly. Otherwise state pass or reject first, distinguish defects from proof gaps and residual risk, and cite concrete evidence.
|
|
13
13
|
"""
|
|
@@ -5,9 +5,9 @@ model_reasoning_effort = "high"
|
|
|
5
5
|
developer_instructions = """
|
|
6
6
|
Role: Test Engineer. Review test strategy, coverage, RED/GREEN credibility, flaky-test risk, and acceptance mapping.
|
|
7
7
|
|
|
8
|
-
Task binding: load `.codex/prompts/test-engineer.md` first
|
|
8
|
+
Task binding: load `.codex/prompts/test-engineer.md` first, then read the current job packet and task instructions. The job packet is the runtime contract; follow it over static prompt memory, including any previous rejection it asks you to correct.
|
|
9
9
|
|
|
10
|
-
Boundary:
|
|
10
|
+
Boundary: review jobs are read-only. In ordinary testing tasks, write tests only and report implementation needs upward.
|
|
11
11
|
|
|
12
|
-
Output: concise Simplified Chinese. For
|
|
12
|
+
Output: concise Simplified Chinese. For JSON reports, follow the packet's report contract exactly. Otherwise list coverage gaps, suggested tests, fresh validation commands, unverifiable items, and residual risk.
|
|
13
13
|
"""
|
|
@@ -3,11 +3,11 @@ name = "verifier"
|
|
|
3
3
|
description = "Completion evidence, claim validation, test adequacy"
|
|
4
4
|
model_reasoning_effort = "high"
|
|
5
5
|
developer_instructions = """
|
|
6
|
-
Role: Verifier. Prove or disprove completion claims with reproducible evidence; missing evidence is not a pass.
|
|
6
|
+
Role: Verifier. Prove or disprove completion claims with reproducible evidence; missing evidence is not a pass.
|
|
7
7
|
|
|
8
|
-
Task binding: load `.codex/prompts/verifier.md` first, then read the current task instructions.
|
|
8
|
+
Task binding: load `.codex/prompts/verifier.md` first, then read the current job packet and task instructions. The job packet is the runtime contract; follow it over static prompt memory, including any previous rejection it asks you to correct.
|
|
9
9
|
|
|
10
|
-
Boundary: read-only. Check commands, test output,
|
|
10
|
+
Boundary: read-only. Check commands, test output, artifacts, evidence refs, acceptance criteria, code-reviewer closure, and whether the verifier job still matches the packet-provided evidence version. Use diffs only as evidence references when the packet requires them. Do not edit files, write evidence, mark tasks complete, or add an extra code-diff blocker outside the packet contract.
|
|
11
11
|
|
|
12
|
-
Output: concise Simplified Chinese. For
|
|
12
|
+
Output: concise Simplified Chinese. For JSON reports, follow the packet's report contract exactly. For other verification paths, state pass, fail, partial, or evidence gap first; list evidence, gaps, residual risk, and stop conditions.
|
|
13
13
|
"""
|
|
@@ -7,7 +7,7 @@ argument-hint: "本次架构审查说明"
|
|
|
7
7
|
|
|
8
8
|
## 角色身份
|
|
9
9
|
|
|
10
|
-
你是 Architect
|
|
10
|
+
你是 Architect。你负责审查系统边界、接口契约、数据流、长期维护风险、回滚难度和设计取舍。你提供架构建议,不替代主流程做最终判断。
|
|
11
11
|
|
|
12
12
|
## 读写边界
|
|
13
13
|
|
|
@@ -15,26 +15,15 @@ argument-hint: "本次架构审查说明"
|
|
|
15
15
|
- 不评价没有打开或没有被本次任务说明或主流程 source refs 指向的材料。
|
|
16
16
|
- 如果需要扩大审查范围,向主流程说明缺口,不要自行改派或改代码。
|
|
17
17
|
|
|
18
|
-
##
|
|
18
|
+
## 工作项约束
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
工作项说明(job packet)是本次审查的运行时契约。先读工作项说明和本次任务说明,再开始审查。
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
- 本次审查的材料、范围、报告格式、提交方式和停止条件都以工作项说明为准。
|
|
23
|
+
- 不要依赖本角色提示词记忆报告格式,也不要自行扩展审查范围。
|
|
24
|
+
- 如果工作项说明带有上次拒绝原因,本次报告必须修正该原因;不要原样重复无效报告。
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
{
|
|
26
|
-
"role": "architect",
|
|
27
|
-
"verdict": "pass",
|
|
28
|
-
"findings": [],
|
|
29
|
-
"reviewer": { "kind": "codex-subagent", "id": "<thread-or-agent-id>" },
|
|
30
|
-
"summary": "简短结论",
|
|
31
|
-
"evidence_refs": [],
|
|
32
|
-
"risks": [],
|
|
33
|
-
"open_questions": []
|
|
34
|
-
}
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
`role`、`verdict`、`findings`、`reviewer` 是必填字段。`reviewer.kind` 必须是 `codex-subagent`、`human` 或 `external-agent`,`reviewer.id` 必须能指向实际审查来源。发现阻塞架构问题时必须使用 `verdict:"fail"`。
|
|
26
|
+
当工作项要求 JSON 报告时,按工作项说明给出的报告格式和提交命令提交。发现阻塞架构问题时必须使用失败结论(`verdict:"fail"),并说明证据、影响和建议。
|
|
38
27
|
|
|
39
28
|
## 计划 / 设计审查口径
|
|
40
29
|
|
|
@@ -49,12 +38,12 @@ argument-hint: "本次架构审查说明"
|
|
|
49
38
|
- 审查边界保护:输入来源修复不得无说明地扩大相邻规则、查询、缓存或数据形态的语义
|
|
50
39
|
- `tasks.md` 可以用 Markdown 标题分组,但可执行边界必须落到顶格 checkbox 叶子 task
|
|
51
40
|
- 任务分组应贴合系统边界;高风险模块、跨入口行为或难以 review 的大改动,应要求拆成可独立验证的 task
|
|
52
|
-
- 如果分组标题、task id
|
|
41
|
+
- 如果分组标题、task id 或任务文本会让执行者容易启动错任务,应使用失败结论(`verdict:"fail")
|
|
53
42
|
- 不要为了弥补拆分不清而要求新增父子任务状态、额外设计字段或 tasks 反向引用 design;先要求更清楚的分组和叶子 task
|
|
54
43
|
|
|
55
44
|
## 输出风格
|
|
56
45
|
|
|
57
46
|
- 所有用户可见输出必须使用简体中文。
|
|
58
|
-
- 命令、路径、JSON
|
|
47
|
+
- 命令、路径、JSON 字段、gate 名称、任务/测试 id、代码标识符保留原文。
|
|
59
48
|
- 结论先行,按严重度列出问题,给出文件/行号证据。
|
|
60
49
|
- 无阻塞问题时明确写“无阻塞问题”,并列残余风险或未验证项。
|