@peterxiaoyang/superspec 0.1.40 → 0.1.41
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 +8 -14
- package/dist/cli.js +3 -8
- package/dist/code_review.d.ts +2 -6
- package/dist/install.d.ts +1 -1
- package/dist/install.js +5 -2
- package/dist/next.js +6 -9
- package/dist/phase_confirmation.d.ts +31 -0
- package/dist/phase_confirmation.js +218 -0
- package/dist/phase_plan.d.ts +5 -6
- package/dist/phase_plan.js +155 -15
- package/dist/record.js +158 -33
- package/dist/review.d.ts +3 -1
- package/dist/review.js +103 -1
- package/dist/review_job_gates.d.ts +2 -1
- package/dist/review_job_gates.js +8 -5
- package/dist/transition.d.ts +0 -1
- package/dist/transition.js +108 -42
- package/dist/types.d.ts +57 -3
- package/package.json +1 -1
- package/templates/workflow/AGENTS.md +2 -0
- package/templates/workflow/prompts/architect.md +16 -15
- package/templates/workflow/prompts/code-reviewer.md +1 -1
- package/templates/workflow/prompts/critic.md +22 -16
- package/templates/workflow/prompts/executor.md +1 -1
- package/templates/workflow/prompts/test-engineer.md +11 -7
- package/templates/workflow/prompts/test-runner.md +1 -1
- package/templates/workflow/skills/superspec-propose/SKILL.md +92 -23
- package/templates/workflow/skills/superspec-review/SKILL.md +9 -6
- package/templates/workflow/skills/superspec-archive/SKILL.md +0 -38
package/dist/phase_plan.js
CHANGED
|
@@ -5,11 +5,23 @@ import { collectProposeOpenQuestions, countDiscoveryOpenQuestions, parseTasksMd,
|
|
|
5
5
|
import { currentGitHead } from "./git_state.js";
|
|
6
6
|
import { docRef, sha256File } from "./store.js";
|
|
7
7
|
import { isReviewReadyVerifier, isFreshReviewVerifier, readReviewPolicyFromEvents, reviewEvidenceDigest, } from "./review.js";
|
|
8
|
-
import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_REPAIR_SCOPE_PREFIX, codeReviewDecisionScope, collectCodeReviewGateFacts, latestCodeReviewFailedStatus, requiresFinalVerifierForCurrentReview, } from "./code_review.js";
|
|
8
|
+
import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_REPAIR_SCOPE_PREFIX, codeReviewDecisionScope, codeReviewJobStaleReason, collectCodeReviewGateFacts, currentCodeReviewWorkingPaths, latestCodeReviewFailedStatus, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, } from "./code_review.js";
|
|
9
|
+
import { isPhaseAdvanceAuthorized, latestAcceptedPhaseDecision, phaseConfirmationCommitPayload, phaseConfirmationForBoundary, phaseConfirmationMissingMessage, } from "./phase_confirmation.js";
|
|
9
10
|
import { taskEvidenceReadiness } from "./task_evidence.js";
|
|
10
11
|
function requiredJobs(state, jobs, reason) {
|
|
11
12
|
return { kind: "required_jobs", state, jobs, reason };
|
|
12
13
|
}
|
|
14
|
+
function phaseConfirmationStep(context, boundary, reason) {
|
|
15
|
+
const confirmation = phaseConfirmationForBoundary(context.projectRoot, context.events, context.snapshot, boundary, context.mode.risk);
|
|
16
|
+
if (!confirmation || isPhaseAdvanceAuthorized(context.events, confirmation))
|
|
17
|
+
return null;
|
|
18
|
+
return {
|
|
19
|
+
kind: "ask_user",
|
|
20
|
+
state: context.snapshot.state,
|
|
21
|
+
ask: confirmation.ask,
|
|
22
|
+
reason,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
13
25
|
function reviewGatePlan(snapshot, gate, requiredRoles) {
|
|
14
26
|
const missingRoles = [];
|
|
15
27
|
for (const role of requiredRoles) {
|
|
@@ -63,14 +75,36 @@ function missingBaseArtifact(changeRoot, risk) {
|
|
|
63
75
|
return null;
|
|
64
76
|
}
|
|
65
77
|
export function proposalDocsBaseline(changeRoot) {
|
|
66
|
-
// specs/
|
|
67
|
-
const docs =
|
|
78
|
+
// 与 Propose gate 的可修改审查目标保持同一来源:specs/ 用目录聚合指纹,避免 reopen 基线漏掉任一个可修改的计划材料。
|
|
79
|
+
const docs = PROPOSE_FINAL_REVIEW_GATE.reviewTargets;
|
|
68
80
|
const baseline = {};
|
|
69
81
|
for (const doc of docs) {
|
|
70
82
|
baseline[doc] = docRef(changeRoot, doc).sha;
|
|
71
83
|
}
|
|
72
84
|
return baseline;
|
|
73
85
|
}
|
|
86
|
+
function isDigestMap(value) {
|
|
87
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
88
|
+
return false;
|
|
89
|
+
const entries = Object.entries(value);
|
|
90
|
+
return entries.length > 0 && entries.every(([, digest]) => typeof digest === "string");
|
|
91
|
+
}
|
|
92
|
+
export function latestAcceptedProposalBaseline(events) {
|
|
93
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
94
|
+
const ev = events[i];
|
|
95
|
+
if (ev.event_type !== "transition_commit")
|
|
96
|
+
continue;
|
|
97
|
+
const payload = ev.payload;
|
|
98
|
+
if (payload.transition !== "accept" ||
|
|
99
|
+
payload.from_state !== "review" ||
|
|
100
|
+
payload.to_state !== "accepted")
|
|
101
|
+
continue;
|
|
102
|
+
return isDigestMap(payload.accepted_baseline_docs)
|
|
103
|
+
? payload.accepted_baseline_docs
|
|
104
|
+
: null;
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
74
108
|
export function latestReopenProposeBaseline(events) {
|
|
75
109
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
76
110
|
const ev = events[i];
|
|
@@ -185,8 +219,42 @@ export function pendingTaskStatusForApply(changeRoot, events) {
|
|
|
185
219
|
export function formatPendingTaskMessage(ids, action) {
|
|
186
220
|
return `尚有未完成任务:${ids.join(", ")};${action}`;
|
|
187
221
|
}
|
|
222
|
+
function nextArgv(change, risk) {
|
|
223
|
+
return [
|
|
224
|
+
"superspec",
|
|
225
|
+
"transition",
|
|
226
|
+
"next",
|
|
227
|
+
"--change",
|
|
228
|
+
change,
|
|
229
|
+
...(risk === "strict" ? [] : ["--risk", risk]),
|
|
230
|
+
];
|
|
231
|
+
}
|
|
232
|
+
function acceptedMaterialFollowup(change, risk, planDocsChangedSinceAccept) {
|
|
233
|
+
return {
|
|
234
|
+
kind: "accepted_material_followup",
|
|
235
|
+
trigger: "material_user_followup",
|
|
236
|
+
reason_source: "summarize_user_input",
|
|
237
|
+
reopen_argv_template: [
|
|
238
|
+
"superspec",
|
|
239
|
+
"transition",
|
|
240
|
+
"reopen",
|
|
241
|
+
"--change",
|
|
242
|
+
change,
|
|
243
|
+
"--to",
|
|
244
|
+
"propose",
|
|
245
|
+
"--reason",
|
|
246
|
+
"{{reason}}",
|
|
247
|
+
],
|
|
248
|
+
resume: {
|
|
249
|
+
kind: "continue_current_phase",
|
|
250
|
+
instruction: "根据用户补充更新 proposal/specs/design/tasks/test-contract;完成后重新执行 next。",
|
|
251
|
+
next_argv_after_completion: nextArgv(change, risk),
|
|
252
|
+
},
|
|
253
|
+
plan_docs_changed_since_accept: planDocsChangedSinceAccept,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
188
256
|
export function planNextStep(context) {
|
|
189
|
-
const { changeRoot, mode, snapshot } = context;
|
|
257
|
+
const { change, changeRoot, events, mode, snapshot } = context;
|
|
190
258
|
switch (snapshot.state) {
|
|
191
259
|
case "init":
|
|
192
260
|
if (snapshot.open_jobs.length > 0) {
|
|
@@ -217,6 +285,12 @@ export function planNextStep(context) {
|
|
|
217
285
|
};
|
|
218
286
|
return { kind: "ask_user", state: "explore", ask, reason: `有 ${openQs} 个未确认问题` };
|
|
219
287
|
}
|
|
288
|
+
const requiredRoles = EXPLORE_DISCOVERY_REVIEW_GATE.requiredRolesForRisk(mode.risk);
|
|
289
|
+
if (!reviewGatePlan(snapshot, EXPLORE_DISCOVERY_REVIEW_GATE, requiredRoles)) {
|
|
290
|
+
const confirmation = phaseConfirmationStep(context, "explore_to_propose", "探索完成,等待用户确认进入计划阶段");
|
|
291
|
+
if (confirmation)
|
|
292
|
+
return confirmation;
|
|
293
|
+
}
|
|
220
294
|
return {
|
|
221
295
|
kind: "run_transition",
|
|
222
296
|
state: "explore",
|
|
@@ -253,6 +327,12 @@ export function planNextStep(context) {
|
|
|
253
327
|
if (proposalReviewJobs.length > 0) {
|
|
254
328
|
return requiredJobs("propose_ready", proposalReviewJobs, `有 ${proposalReviewJobs.length} 个待完成 proposal 审查工作项`);
|
|
255
329
|
}
|
|
330
|
+
const startApplyPlan = planStartApplyTransition(context, false);
|
|
331
|
+
if (startApplyPlan.kind === "advance") {
|
|
332
|
+
const confirmation = phaseConfirmationStep(context, "propose_to_apply", "计划阶段完成,等待用户确认开始实现");
|
|
333
|
+
if (confirmation)
|
|
334
|
+
return confirmation;
|
|
335
|
+
}
|
|
256
336
|
return { kind: "run_transition", state: "propose_ready", transition: "start-apply", reason: "计划就绪,开始执行" };
|
|
257
337
|
}
|
|
258
338
|
case "apply":
|
|
@@ -263,14 +343,28 @@ export function planNextStep(context) {
|
|
|
263
343
|
return planReviewNext(context);
|
|
264
344
|
case "accepted":
|
|
265
345
|
if (snapshot.open_jobs.length > 0) {
|
|
266
|
-
return requiredJobs("accepted", snapshot.open_jobs, `有 ${snapshot.open_jobs.length}
|
|
346
|
+
return requiredJobs("accepted", snapshot.open_jobs, `有 ${snapshot.open_jobs.length} 个待完成工作项,暂不结束流程`);
|
|
347
|
+
}
|
|
348
|
+
{
|
|
349
|
+
const acceptedBaseline = latestAcceptedProposalBaseline(events);
|
|
350
|
+
const planDocsChanged = acceptedBaseline
|
|
351
|
+
? proposalDocsChangedSinceBaseline(changeRoot, acceptedBaseline)
|
|
352
|
+
: null;
|
|
353
|
+
const driftNote = planDocsChanged === true
|
|
354
|
+
? "检测到 accepted 后计划材料已变化;当前完成结论仍对应 accepted 时冻结的版本。"
|
|
355
|
+
: "";
|
|
356
|
+
return {
|
|
357
|
+
kind: "done",
|
|
358
|
+
state: "accepted",
|
|
359
|
+
reason: `${driftNote}审查已接受,流程完成;后续若使用者补充或修改需求、方案、验收或实现约束,按 continuation 自动回到 propose 后继续,不得要求使用者执行工作流命令`,
|
|
360
|
+
continuation: acceptedMaterialFollowup(change, mode.risk, planDocsChanged),
|
|
361
|
+
};
|
|
267
362
|
}
|
|
268
|
-
return { kind: "ask_archive_confirmation", state: "accepted", reason: "审查通过,等待用户确认归档" };
|
|
269
363
|
case "archive":
|
|
270
364
|
if (snapshot.open_jobs.length > 0) {
|
|
271
|
-
return requiredJobs("archive", snapshot.open_jobs,
|
|
365
|
+
return requiredJobs("archive", snapshot.open_jobs, `历史 archive 状态仍有 ${snapshot.open_jobs.length} 个待完成工作项`);
|
|
272
366
|
}
|
|
273
|
-
return { kind: "done", state: "archive", reason: "
|
|
367
|
+
return { kind: "done", state: "archive", reason: "历史 archive 状态,流程已结束。" };
|
|
274
368
|
case "abandoned":
|
|
275
369
|
return { kind: "done", state: "abandoned", reason: "变更已放弃,流程终止。" };
|
|
276
370
|
default:
|
|
@@ -320,6 +414,14 @@ function planApplyNext(context) {
|
|
|
320
414
|
reason: "所有任务完成,进入审查",
|
|
321
415
|
};
|
|
322
416
|
}
|
|
417
|
+
export function blockingJobsForApplyDone(projectRoot, events, snapshot) {
|
|
418
|
+
const facts = collectCodeReviewGateFacts(events);
|
|
419
|
+
const currentWorkingPaths = currentCodeReviewWorkingPaths(projectRoot, events);
|
|
420
|
+
const freshCodeReviewJobIds = new Set(facts.openJobs
|
|
421
|
+
.filter(job => codeReviewJobStaleReason(projectRoot, job, currentWorkingPaths) == null)
|
|
422
|
+
.map(job => job.job_id));
|
|
423
|
+
return snapshot.open_jobs.filter(job => job.role !== "code-reviewer" || freshCodeReviewJobIds.has(job.job_id));
|
|
424
|
+
}
|
|
323
425
|
function planApplyDoneNext(context) {
|
|
324
426
|
const { change, changeRoot, events, mode, snapshot } = context;
|
|
325
427
|
const pendingTasks = pendingTaskStatusForApply(changeRoot, events).pending;
|
|
@@ -332,10 +434,12 @@ function planApplyDoneNext(context) {
|
|
|
332
434
|
reason: `发现未完成任务 ${pendingTasks[0]},回到执行阶段`,
|
|
333
435
|
};
|
|
334
436
|
}
|
|
335
|
-
if (snapshot.open_jobs.length > 0) {
|
|
336
|
-
return requiredJobs("apply_done", snapshot.open_jobs, `有 ${snapshot.open_jobs.length} 个待完成工作项`);
|
|
337
|
-
}
|
|
338
437
|
const facts = collectCodeReviewGateFacts(events);
|
|
438
|
+
const currentWorkingPaths = currentCodeReviewWorkingPaths(context.projectRoot, events);
|
|
439
|
+
const relevantOpenJobs = blockingJobsForApplyDone(context.projectRoot, events, snapshot);
|
|
440
|
+
if (relevantOpenJobs.length > 0) {
|
|
441
|
+
return requiredJobs("apply_done", relevantOpenJobs, `有 ${relevantOpenJobs.length} 个待完成工作项`);
|
|
442
|
+
}
|
|
339
443
|
const latest = facts.latestTerminal;
|
|
340
444
|
if (latest?.state === "rejected" && latest.result_kind === "review_failed") {
|
|
341
445
|
const status = latestCodeReviewFailedStatus(events);
|
|
@@ -423,6 +527,19 @@ function planApplyDoneNext(context) {
|
|
|
423
527
|
};
|
|
424
528
|
return { kind: "ask_user", state: "apply_done", ask, reason: "代码审查报告连续不符合要求或没有可处理问题" };
|
|
425
529
|
}
|
|
530
|
+
const codeScan = scanCodeChangesForReview(context.projectRoot, events);
|
|
531
|
+
const acceptedScope = latest?.state === "accepted"
|
|
532
|
+
? latest.job.packet_context?.code_review_scope
|
|
533
|
+
: undefined;
|
|
534
|
+
const acceptedCurrentHead = acceptedScope?.current_head;
|
|
535
|
+
const acceptedReviewReady = latest?.state === "accepted" &&
|
|
536
|
+
codeReviewJobStaleReason(context.projectRoot, latest.job, currentWorkingPaths) == null && (acceptedCurrentHead === null ||
|
|
537
|
+
(typeof acceptedCurrentHead === "string" && acceptedCurrentHead.trim() !== ""));
|
|
538
|
+
if (!codeScan.hasCodeChanges || acceptedReviewReady) {
|
|
539
|
+
const confirmation = phaseConfirmationStep(context, "apply_to_review", "Apply 与代码审查完成,等待用户确认进入最终审查");
|
|
540
|
+
if (confirmation)
|
|
541
|
+
return confirmation;
|
|
542
|
+
}
|
|
426
543
|
return {
|
|
427
544
|
kind: "run_transition",
|
|
428
545
|
state: "apply_done",
|
|
@@ -485,7 +602,7 @@ export function planTransition(name, context) {
|
|
|
485
602
|
}
|
|
486
603
|
}
|
|
487
604
|
function planExploreTransition(context) {
|
|
488
|
-
const { changeRoot, mode, snapshot } = context;
|
|
605
|
+
const { changeRoot, events, mode, projectRoot, snapshot } = context;
|
|
489
606
|
if (snapshot.state === "init") {
|
|
490
607
|
return { kind: "advance", fromState: "init", toState: "explore", reason: "进入探索阶段" };
|
|
491
608
|
}
|
|
@@ -499,7 +616,18 @@ function planExploreTransition(context) {
|
|
|
499
616
|
const gatePlan = reviewGatePlan(snapshot, EXPLORE_DISCOVERY_REVIEW_GATE, requiredRoles);
|
|
500
617
|
if (gatePlan)
|
|
501
618
|
return gatePlan;
|
|
502
|
-
|
|
619
|
+
const confirmation = phaseConfirmationForBoundary(projectRoot, events, snapshot, "explore_to_propose");
|
|
620
|
+
const decision = confirmation ? latestAcceptedPhaseDecision(events, confirmation) : null;
|
|
621
|
+
if (!confirmation || decision?.decision !== "advance") {
|
|
622
|
+
return { kind: "skip", message: confirmation ? phaseConfirmationMissingMessage(confirmation) : "无法建立 Explore 阶段确认范围" };
|
|
623
|
+
}
|
|
624
|
+
return {
|
|
625
|
+
kind: "advance",
|
|
626
|
+
fromState: "explore",
|
|
627
|
+
toState: "propose",
|
|
628
|
+
reason: "探索完成",
|
|
629
|
+
payload: phaseConfirmationCommitPayload(confirmation, decision),
|
|
630
|
+
};
|
|
503
631
|
}
|
|
504
632
|
function planProposeReadyTransition(context) {
|
|
505
633
|
const { changeRoot, mode, snapshot } = context;
|
|
@@ -526,7 +654,7 @@ function planProposeReadyTransition(context) {
|
|
|
526
654
|
return gatePlan;
|
|
527
655
|
return { kind: "advance", fromState: "propose", toState: "propose_ready", reason: `risk=${risk},所有需求已满足` };
|
|
528
656
|
}
|
|
529
|
-
function planStartApplyTransition(context) {
|
|
657
|
+
function planStartApplyTransition(context, enforceConfirmation = true) {
|
|
530
658
|
const { changeRoot, events, projectRoot, snapshot } = context;
|
|
531
659
|
if (snapshot.state !== "propose_ready")
|
|
532
660
|
return { kind: "skip", message: `当前状态 ${snapshot.state},需要 propose_ready` };
|
|
@@ -548,6 +676,11 @@ function planStartApplyTransition(context) {
|
|
|
548
676
|
const executionRequirementPlan = validateExecutionRequirementPlan(changeRoot);
|
|
549
677
|
if (!executionRequirementPlan.ok)
|
|
550
678
|
return { kind: "skip", message: executionRequirementPlan.message };
|
|
679
|
+
const confirmation = phaseConfirmationForBoundary(projectRoot, events, snapshot, "propose_to_apply");
|
|
680
|
+
const decision = confirmation ? latestAcceptedPhaseDecision(events, confirmation) : null;
|
|
681
|
+
if (enforceConfirmation && (!confirmation || decision?.decision !== "advance")) {
|
|
682
|
+
return { kind: "skip", message: confirmation ? phaseConfirmationMissingMessage(confirmation) : "无法建立 Propose 阶段确认范围" };
|
|
683
|
+
}
|
|
551
684
|
const gitHead = currentGitHead(projectRoot);
|
|
552
685
|
return {
|
|
553
686
|
kind: "advance",
|
|
@@ -558,6 +691,7 @@ function planStartApplyTransition(context) {
|
|
|
558
691
|
apply_start_head: gitHead.head,
|
|
559
692
|
apply_start_head_reason: gitHead.reason,
|
|
560
693
|
apply_contract_mode: executionRequirementPlan.mode,
|
|
694
|
+
...(confirmation && decision?.decision === "advance" ? phaseConfirmationCommitPayload(confirmation, decision) : {}),
|
|
561
695
|
},
|
|
562
696
|
};
|
|
563
697
|
}
|
|
@@ -578,5 +712,11 @@ function planAcceptTransition(context) {
|
|
|
578
712
|
if (!verifierAccepted)
|
|
579
713
|
return { kind: "skip", message: "缺少仍然匹配当前证据的最终验证,请先运行 review-ready" };
|
|
580
714
|
}
|
|
581
|
-
return {
|
|
715
|
+
return {
|
|
716
|
+
kind: "advance",
|
|
717
|
+
fromState: "review",
|
|
718
|
+
toState: "accepted",
|
|
719
|
+
reason: "审查通过",
|
|
720
|
+
payload: { accepted_baseline_docs: proposalDocsBaseline(changeRoot) },
|
|
721
|
+
};
|
|
582
722
|
}
|
package/dist/record.js
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
import { readFileSync, existsSync } 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
|
+
import { rebuildSnapshot } from "./sync.js";
|
|
6
|
+
import { changeRoot as openspecChangeRoot } from "./openspec.js";
|
|
7
|
+
import { isPhaseConfirmationScope, phaseActionForAnswer, phaseConfirmationForCurrentState, } from "./phase_confirmation.js";
|
|
5
8
|
import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_DECISION_SCOPE_PREFIX, codeReviewDecisionAnswerLabel, normalizeCodeReviewDecisionAnswer, } from "./code_review.js";
|
|
6
9
|
import { invalidReasonForSubmittedReport } from "./job_validity.js";
|
|
7
10
|
import { jobSubmitArgv } from "./job_action.js";
|
|
8
11
|
import { REVIEW_DOC_PATHS } from "./review.js";
|
|
12
|
+
import { EXPLORE_DISCOVERY_REVIEW_GATE, PROPOSE_FINAL_REVIEW_GATE } from "./review_job_gates.js";
|
|
9
13
|
const REVIEW_REPORT_REQUIRED_FIELDS = ["role", "verdict", "findings"];
|
|
10
14
|
const REVIEW_REPORT_OPTIONAL_FIELDS = ["summary", "evidence_refs", "risks", "open_questions"];
|
|
11
15
|
const REVIEWER_KINDS = new Set(["codex-subagent", "human", "external-agent"]);
|
|
@@ -13,6 +17,9 @@ const CODE_REVIEW_FINDING_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
|
|
|
13
17
|
function requiresReviewer(role) {
|
|
14
18
|
return role === "critic" || role === "architect" || role === "test-engineer" || role === "code-reviewer";
|
|
15
19
|
}
|
|
20
|
+
function isOrdinaryReviewer(role) {
|
|
21
|
+
return role === "critic" || role === "architect" || role === "test-engineer";
|
|
22
|
+
}
|
|
16
23
|
function recommendedAgentForRole(role) {
|
|
17
24
|
switch (role) {
|
|
18
25
|
case "critic": return "critic";
|
|
@@ -42,6 +49,43 @@ function roleDescription(role) {
|
|
|
42
49
|
return "执行受限测试工作项";
|
|
43
50
|
}
|
|
44
51
|
}
|
|
52
|
+
function previousRejectionInstruction(job) {
|
|
53
|
+
const previous = job.previous_rejection;
|
|
54
|
+
if (!previous)
|
|
55
|
+
return "";
|
|
56
|
+
const reason = `上一次同角色审查没有形成可推进结论,原因:${previous.reason}。`;
|
|
57
|
+
if (!previous.findings || previous.findings.length === 0)
|
|
58
|
+
return `${reason}本次请先针对该原因重新审查,`;
|
|
59
|
+
return `${reason}本工作项附带上一次同角色审查尚未闭环的问题列表;请优先逐项复核:同一问题仍存在时复用原 finding ID;legacy finding 没有 ID 时沿用其原始语义并补一个稳定 ID;已解决的问题不要重复报告,不得通过更换 ID、标题或措辞重复同一问题;新增问题必须提供与历史问题不同的具体证据。`;
|
|
60
|
+
}
|
|
61
|
+
function ordinaryReviewerFindingInstruction(job) {
|
|
62
|
+
if (!isOrdinaryReviewer(job.role))
|
|
63
|
+
return "";
|
|
64
|
+
return "问题列表中的每个新 finding 必须分配稳定 ID,后续同一问题沿用该 ID,";
|
|
65
|
+
}
|
|
66
|
+
function reviewScopeForJob(job) {
|
|
67
|
+
if (job.review_targets !== undefined || job.read_only_refs !== undefined) {
|
|
68
|
+
return {
|
|
69
|
+
reviewTargets: job.review_targets ?? [],
|
|
70
|
+
readOnlyRefs: job.read_only_refs ?? [],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const gate = [EXPLORE_DISCOVERY_REVIEW_GATE, PROPOSE_FINAL_REVIEW_GATE]
|
|
74
|
+
.find(candidate => candidate.isJobForGate(job));
|
|
75
|
+
return gate
|
|
76
|
+
? { reviewTargets: [...gate.reviewTargets], readOnlyRefs: [...gate.readOnlyRefs] }
|
|
77
|
+
: { reviewTargets: [], readOnlyRefs: [] };
|
|
78
|
+
}
|
|
79
|
+
function reviewScopeInstruction(job, reviewTargets, readOnlyRefs) {
|
|
80
|
+
if (reviewTargets.length === 0 && readOnlyRefs.length === 0) {
|
|
81
|
+
return `请审查 ${job.boundFiles.map(file => file.path).join(", ")},`;
|
|
82
|
+
}
|
|
83
|
+
const targets = reviewTargets.length > 0 ? `本 gate 可提出修改建议的审查目标为 ${reviewTargets.join(", ")}。` : "";
|
|
84
|
+
const refs = readOnlyRefs.length > 0
|
|
85
|
+
? `只读上游引用为 ${readOnlyRefs.join(", ")};只允许读取和核对一致性,不得要求在当前阶段修改、追加、删除、重排或格式化这些文件,不得把修改只读引用列为本阶段 required fix。只读引用自身的缺失、错误或矛盾可在 risks 或 open_questions 中上报给主流程,但不得单独作为本 gate 的 fail。只有能定位到审查目标的不一致,才能作为 fail 并指向该审查目标的修复。`
|
|
86
|
+
: "";
|
|
87
|
+
return targets + refs;
|
|
88
|
+
}
|
|
45
89
|
function asObject(value) {
|
|
46
90
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
47
91
|
}
|
|
@@ -205,6 +249,7 @@ function failedReviewReportRejectEventPayload(input) {
|
|
|
205
249
|
job_id: input.jobId,
|
|
206
250
|
role: input.job.role,
|
|
207
251
|
report_digest: input.reportDigest,
|
|
252
|
+
result_kind: "review_failed",
|
|
208
253
|
reason: "报告结论为 fail,工作项未通过",
|
|
209
254
|
findings: Array.isArray(input.parsedReport.findings) ? input.parsedReport.findings : [],
|
|
210
255
|
...input.rawRef,
|
|
@@ -286,6 +331,9 @@ function recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, even
|
|
|
286
331
|
if (!Array.isArray(obj.findings)) {
|
|
287
332
|
checks.push("报告问题列表 findings 必须是数组");
|
|
288
333
|
}
|
|
334
|
+
else if (isOrdinaryReviewer(job.role) && obj.verdict === "fail" && obj.findings.length === 0) {
|
|
335
|
+
checks.push("普通审查报告结论为 fail 时 findings 至少包含一个问题");
|
|
336
|
+
}
|
|
289
337
|
if (requiresReviewer(job.role)) {
|
|
290
338
|
validateReviewer(obj, checks);
|
|
291
339
|
}
|
|
@@ -311,29 +359,21 @@ function recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, even
|
|
|
311
359
|
checks.push("报告内容为空");
|
|
312
360
|
}
|
|
313
361
|
if (checks.length > 0) {
|
|
314
|
-
const resultKind =
|
|
315
|
-
const rawRef =
|
|
362
|
+
const resultKind = "invalid_report";
|
|
363
|
+
const rawRef = job.role === "code-reviewer" && parsedReport
|
|
316
364
|
? appendRawRecord(projectRoot, change, "review-reports", parsedReport)
|
|
317
365
|
: null;
|
|
318
366
|
const rejectEvent = makeEvent(change, "job_rejected", {
|
|
319
|
-
...(
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
})
|
|
330
|
-
: {
|
|
331
|
-
job_id: jobId,
|
|
332
|
-
role: job.role,
|
|
333
|
-
report_digest: reportDigest,
|
|
334
|
-
...(reportPath ? { report_path: reportPath } : {}),
|
|
335
|
-
reason: checks.join("; "),
|
|
336
|
-
}),
|
|
367
|
+
...codeReviewRejectEventPayload({
|
|
368
|
+
jobId,
|
|
369
|
+
job,
|
|
370
|
+
reportDigest,
|
|
371
|
+
resultKind,
|
|
372
|
+
reason: checks.join("; "),
|
|
373
|
+
parsedReport,
|
|
374
|
+
rawRef,
|
|
375
|
+
reportPath,
|
|
376
|
+
}),
|
|
337
377
|
});
|
|
338
378
|
appendEvent(projectRoot, change, rejectEvent);
|
|
339
379
|
return {
|
|
@@ -475,21 +515,21 @@ export function recordJobSubmitContent(projectRoot, change, changeRoot, jobId, r
|
|
|
475
515
|
});
|
|
476
516
|
}
|
|
477
517
|
function recordUserDecisionLoaded(projectRoot, change, events, content, inputDigest) {
|
|
478
|
-
const existing = events.find(e => e.event_type === "user_decision_recorded"
|
|
518
|
+
const existing = [...events].reverse().find(e => e.event_type === "user_decision_recorded"
|
|
479
519
|
&& e.payload.input_digest === inputDigest);
|
|
480
|
-
if (existing) {
|
|
481
|
-
const accepted = existing.payload.accepted !== false;
|
|
482
|
-
return {
|
|
483
|
-
event_type: "user_decision_recorded",
|
|
484
|
-
accepted,
|
|
485
|
-
message: accepted ? "幂等返回:同一用户决策已登记" : "幂等返回:同一无效用户决策已登记",
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
520
|
let decision;
|
|
489
521
|
try {
|
|
490
522
|
decision = JSON.parse(content);
|
|
491
523
|
}
|
|
492
524
|
catch {
|
|
525
|
+
if (existing) {
|
|
526
|
+
const accepted = existing.payload.accepted !== false;
|
|
527
|
+
return {
|
|
528
|
+
event_type: "user_decision_recorded",
|
|
529
|
+
accepted,
|
|
530
|
+
message: accepted ? "幂等返回:同一用户决策已登记" : "幂等返回:同一无效用户决策已登记",
|
|
531
|
+
};
|
|
532
|
+
}
|
|
493
533
|
appendEvent(projectRoot, change, makeEvent(change, "user_decision_recorded", {
|
|
494
534
|
accepted: false,
|
|
495
535
|
reason: "invalid_json",
|
|
@@ -505,6 +545,76 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
|
|
|
505
545
|
}));
|
|
506
546
|
return { event_type: "user_decision_recorded", accepted: false, message: "决策文件缺少决策范围(scope)或答复内容(answer)" };
|
|
507
547
|
}
|
|
548
|
+
let phaseConfirmation = null;
|
|
549
|
+
let phaseAction = null;
|
|
550
|
+
if (isPhaseConfirmationScope(decision.scope)) {
|
|
551
|
+
const existingAccepted = existing &&
|
|
552
|
+
existing.payload.accepted !== false;
|
|
553
|
+
const latestAcceptedForScope = [...events].reverse().find(event => {
|
|
554
|
+
if (event.event_type !== "user_decision_recorded")
|
|
555
|
+
return false;
|
|
556
|
+
const payload = event.payload;
|
|
557
|
+
return payload.accepted !== false &&
|
|
558
|
+
payload.scope === decision.scope &&
|
|
559
|
+
payload.phase_confirmation != null;
|
|
560
|
+
});
|
|
561
|
+
const changeRoot = openspecChangeRoot(projectRoot, change);
|
|
562
|
+
const snapshot = rebuildSnapshot(projectRoot, change, changeRoot);
|
|
563
|
+
const current = phaseConfirmationForCurrentState(projectRoot, events, snapshot);
|
|
564
|
+
if (existingAccepted &&
|
|
565
|
+
current?.scope === decision.scope &&
|
|
566
|
+
latestAcceptedForScope?.event_id === existing.event_id) {
|
|
567
|
+
return {
|
|
568
|
+
event_type: "user_decision_recorded",
|
|
569
|
+
accepted: true,
|
|
570
|
+
message: "幂等返回:同一用户决策已登记",
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
const action = current ? phaseActionForAnswer(current, decision.answer) : null;
|
|
574
|
+
const rejectionReason = !current
|
|
575
|
+
? "phase_confirmation_not_pending"
|
|
576
|
+
: decision.scope !== current.scope
|
|
577
|
+
? "stale_phase_confirmation_scope"
|
|
578
|
+
: !action
|
|
579
|
+
? "invalid_phase_confirmation_answer"
|
|
580
|
+
: action.reason === "required" && !nonEmptyString(decision.reason)
|
|
581
|
+
? "missing_phase_confirmation_reason"
|
|
582
|
+
: null;
|
|
583
|
+
if (rejectionReason) {
|
|
584
|
+
if (existing &&
|
|
585
|
+
existing.payload.accepted === false &&
|
|
586
|
+
existing.payload.reason === rejectionReason) {
|
|
587
|
+
return {
|
|
588
|
+
event_type: "user_decision_recorded",
|
|
589
|
+
accepted: false,
|
|
590
|
+
message: "幂等返回:同一无效阶段确认已登记",
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
appendEvent(projectRoot, change, makeEvent(change, "user_decision_recorded", {
|
|
594
|
+
accepted: false,
|
|
595
|
+
scope: decision.scope,
|
|
596
|
+
answer: decision.answer,
|
|
597
|
+
reason: rejectionReason,
|
|
598
|
+
input_digest: inputDigest,
|
|
599
|
+
}));
|
|
600
|
+
const message = rejectionReason === "invalid_phase_confirmation_answer" && current
|
|
601
|
+
? `阶段确认答复必须精确为:${current.ask.allowed_answers.join("、")}`
|
|
602
|
+
: rejectionReason === "missing_phase_confirmation_reason" && action
|
|
603
|
+
? action.reason_prompt ?? "当前选择必须写明原因"
|
|
604
|
+
: "阶段确认已失效或当前没有待确认的阶段边界,请重新执行 next";
|
|
605
|
+
return { event_type: "user_decision_recorded", accepted: false, message };
|
|
606
|
+
}
|
|
607
|
+
phaseConfirmation = current;
|
|
608
|
+
phaseAction = action;
|
|
609
|
+
}
|
|
610
|
+
if (existing && !phaseAction) {
|
|
611
|
+
const accepted = existing.payload.accepted !== false;
|
|
612
|
+
return {
|
|
613
|
+
event_type: "user_decision_recorded",
|
|
614
|
+
accepted,
|
|
615
|
+
message: accepted ? "幂等返回:同一用户决策已登记" : "幂等返回:同一无效用户决策已登记",
|
|
616
|
+
};
|
|
617
|
+
}
|
|
508
618
|
if (decision.scope.startsWith(CODE_REVIEW_DECISION_SCOPE_PREFIX)) {
|
|
509
619
|
const normalizedAnswer = normalizeCodeReviewDecisionAnswer(decision.answer);
|
|
510
620
|
if (!normalizedAnswer) {
|
|
@@ -541,9 +651,19 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
|
|
|
541
651
|
: null;
|
|
542
652
|
const normalizedDecision = {
|
|
543
653
|
scope: decision.scope,
|
|
544
|
-
question:
|
|
545
|
-
|
|
654
|
+
question: phaseConfirmation
|
|
655
|
+
? phaseConfirmation.ask.question
|
|
656
|
+
: typeof decision.question === "string" ? decision.question : "",
|
|
657
|
+
answer: phaseAction
|
|
658
|
+
? phaseAction.label
|
|
659
|
+
: normalizedAnswer ? codeReviewDecisionAnswerLabel(normalizedAnswer) : decision.answer,
|
|
546
660
|
...(typeof decision.reason === "string" ? { reason: decision.reason.trim() } : {}),
|
|
661
|
+
...(phaseAction ? {
|
|
662
|
+
phase_confirmation: {
|
|
663
|
+
boundary: phaseAction.boundary,
|
|
664
|
+
decision: phaseAction.decision,
|
|
665
|
+
},
|
|
666
|
+
} : {}),
|
|
547
667
|
};
|
|
548
668
|
const rawRef = appendRawRecord(projectRoot, change, "user-decisions", normalizedDecision);
|
|
549
669
|
const event = makeEvent(change, "user_decision_recorded", {
|
|
@@ -649,6 +769,7 @@ export function jobsPacket(projectRoot, change, jobId) {
|
|
|
649
769
|
}
|
|
650
770
|
const isCodeReviewer = job.role === "code-reviewer";
|
|
651
771
|
const packetContext = job.packet_context;
|
|
772
|
+
const { reviewTargets, readOnlyRefs } = reviewScopeForJob(job);
|
|
652
773
|
return {
|
|
653
774
|
found: true,
|
|
654
775
|
packet: {
|
|
@@ -657,6 +778,8 @@ export function jobsPacket(projectRoot, change, jobId) {
|
|
|
657
778
|
...(job.gate_id ? { gate_id: job.gate_id } : {}),
|
|
658
779
|
recommended_agent: recommendedAgentForRole(job.role),
|
|
659
780
|
boundFiles: job.boundFiles,
|
|
781
|
+
...(reviewTargets.length > 0 ? { review_targets: reviewTargets } : {}),
|
|
782
|
+
...(readOnlyRefs.length > 0 ? { read_only_refs: readOnlyRefs } : {}),
|
|
660
783
|
...(job.review_evidence_digest ? { review_evidence_digest: job.review_evidence_digest } : {}),
|
|
661
784
|
...(job.previous_rejection ? { previous_rejection: job.previous_rejection } : {}),
|
|
662
785
|
...(packetContext ? { packet_context: packetContext } : {}),
|
|
@@ -677,10 +800,12 @@ export function jobsPacket(projectRoot, change, jobId) {
|
|
|
677
800
|
: requiresReviewer(job.role) ? [...REVIEW_REPORT_REQUIRED_FIELDS, "reviewer"] : [...REVIEW_REPORT_REQUIRED_FIELDS],
|
|
678
801
|
output_contract_optional_fields: [...REVIEW_REPORT_OPTIONAL_FIELDS],
|
|
679
802
|
字段说明: packetFieldDescriptions(),
|
|
680
|
-
output_instructions: `${roleDescription(job.role)}
|
|
803
|
+
output_instructions: `${roleDescription(job.role)}。` +
|
|
804
|
+
reviewScopeInstruction(job, reviewTargets, readOnlyRefs) +
|
|
681
805
|
(job.review_evidence_digest ? `本工作项对应的执行证据版本为 ${job.review_evidence_digest},` : "") +
|
|
682
|
-
(job
|
|
806
|
+
previousRejectionInstruction(job) +
|
|
683
807
|
(requiresReviewer(job.role) ? `必须由独立 ${recommendedAgentForRole(job.role)} 审查角色执行,并在审查者来源字段(reviewer.kind/id)中记录来源,` : "") +
|
|
808
|
+
ordinaryReviewerFindingInstruction(job) +
|
|
684
809
|
`产出 JSON 报告内容并优先通过 --report - 从 stdin 登记;文件路径模式仅作备用。协议字段含义见 packet 顶层“字段说明”,普通对话不要原样复述 JSON。` +
|
|
685
810
|
(isCodeReviewer
|
|
686
811
|
? `最小格式:{"role":"code-reviewer","verdict":"pass|fail","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>"}};审查覆盖范围(review_scope)用来说明本次审查覆盖了哪些文件和文档,已检查路径(checked_paths)与未检查项(unchecked)必须合起来覆盖全部绑定文件(boundFiles),unchecked 条目格式为 {"path":"<path>","reason":"<reason>"}。`
|
package/dist/review.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type ReviewGateRule } from "./review_job_gates.ts";
|
|
2
|
+
import type { Event, Job, JobRole, Ref, ReviewPreviousRejection } from "./types.ts";
|
|
2
3
|
export type ReviewRisk = "minimal" | "normal" | "strict";
|
|
3
4
|
export interface ReviewPolicy {
|
|
4
5
|
review_risk: ReviewRisk;
|
|
@@ -8,6 +9,7 @@ export declare const REVIEW_DOC_PATHS: string[];
|
|
|
8
9
|
export declare function assertCommitPayloadExtension(payload: Record<string, unknown>): void;
|
|
9
10
|
export declare function reviewPolicyForRisk(risk: ReviewRisk): ReviewPolicy;
|
|
10
11
|
export declare function readReviewPolicyFromEvents(events: Event[]): ReviewPolicy | null;
|
|
12
|
+
export declare function latestReviewHistoryForGateRole(events: Event[], gate: ReviewGateRule, role: JobRole): ReviewPreviousRejection | null;
|
|
11
13
|
export declare function isReviewReadyVerifier(job: Job): boolean;
|
|
12
14
|
export declare function reviewBoundFiles(changeRoot: string): Ref[];
|
|
13
15
|
export declare function reviewEvidenceDigest(events: Event[]): string;
|