@peterxiaoyang/superspec 0.1.44 → 0.1.45
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 +13 -1
- package/dist/cli.js +23 -24
- package/dist/code_review.js +7 -2
- package/dist/format.d.ts +4 -2
- package/dist/format.js +50 -16
- package/dist/git_state.d.ts +12 -1
- package/dist/git_state.js +45 -0
- package/dist/install.d.ts +1 -0
- package/dist/install.js +12 -0
- package/dist/next.d.ts +1 -1
- package/dist/next.js +3 -8
- package/dist/phase_confirmation.d.ts +6 -0
- package/dist/phase_confirmation.js +22 -6
- package/dist/phase_plan.d.ts +8 -1
- package/dist/phase_plan.js +130 -21
- package/dist/record.d.ts +1 -1
- package/dist/record.js +38 -30
- package/dist/review.js +18 -2
- package/dist/sync.js +13 -4
- package/dist/task.js +15 -2
- package/dist/task_evidence.d.ts +1 -1
- package/dist/task_evidence.js +85 -10
- package/dist/transition.d.ts +4 -3
- package/dist/transition.js +162 -29
- package/dist/types.d.ts +24 -1
- package/dist/types.js +1 -0
- package/dist/workflow_config.d.ts +24 -0
- package/dist/workflow_config.js +127 -0
- package/package.json +1 -1
- package/templates/workflow/AGENTS.md +1 -1
- package/templates/workflow/prompts/architect.md +1 -1
- package/templates/workflow/prompts/code-reviewer.md +2 -2
- package/templates/workflow/prompts/critic.md +4 -5
- package/templates/workflow/prompts/executor.md +2 -2
- package/templates/workflow/prompts/test-engineer.md +3 -4
- package/templates/workflow/prompts/verifier.md +1 -1
- package/templates/workflow/skills/superspec-apply/SKILL.md +13 -71
- package/templates/workflow/skills/superspec-explore/SKILL.md +5 -9
- package/templates/workflow/skills/superspec-propose/SKILL.md +26 -26
- package/templates/workflow/skills/superspec-review/SKILL.md +21 -50
package/dist/phase_plan.js
CHANGED
|
@@ -8,11 +8,14 @@ import { isReviewReadyVerifier, isFreshReviewVerifier, historicalProposeReadyRol
|
|
|
8
8
|
import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_REPAIR_SCOPE_PREFIX, codeReviewDecisionScope, codeReviewJobStaleReason, collectCodeReviewGateFacts, currentCodeReviewWorkingPaths, latestCodeReviewFailedStatus, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, } from "./code_review.js";
|
|
9
9
|
import { isPhaseAdvanceAuthorized, latestAcceptedPhaseDecision, phaseConfirmationCommitPayload, phaseConfirmationForBoundary, phaseConfirmationMissingMessage, } from "./phase_confirmation.js";
|
|
10
10
|
import { taskEvidenceReadiness } from "./task_evidence.js";
|
|
11
|
+
import { workflowRiskForProposeRound, workflowRiskForState } from "./workflow_config.js";
|
|
11
12
|
function requiredJobs(state, jobs, reason) {
|
|
12
13
|
return { kind: "required_jobs", state, jobs, reason };
|
|
13
14
|
}
|
|
14
15
|
function phaseConfirmationStep(context, boundary, reason) {
|
|
15
|
-
|
|
16
|
+
// 进入 propose_ready / apply 后,mode 来自本轮快照而非当前配置。
|
|
17
|
+
const risk = workflowRiskForState(context.events, context.snapshot.state, context.mode.risk);
|
|
18
|
+
const confirmation = phaseConfirmationForBoundary(context.projectRoot, context.events, context.snapshot, boundary, risk);
|
|
16
19
|
if (!confirmation || isPhaseAdvanceAuthorized(context.events, confirmation))
|
|
17
20
|
return null;
|
|
18
21
|
return {
|
|
@@ -85,14 +88,26 @@ function validateTasksPlan(changeRoot) {
|
|
|
85
88
|
return "tasks.md 内容不像任务计划文档";
|
|
86
89
|
return null;
|
|
87
90
|
}
|
|
88
|
-
function
|
|
91
|
+
export function executionPolicyForRisk(risk) {
|
|
92
|
+
return risk === "strict" ? "tdd" : "green_only";
|
|
93
|
+
}
|
|
94
|
+
export function executionPolicyForCurrentRound(events) {
|
|
95
|
+
const index = latestStartApplyIndex(events);
|
|
96
|
+
if (index < 0)
|
|
97
|
+
return "tdd";
|
|
98
|
+
const payload = events[index].payload;
|
|
99
|
+
return payload.execution_policy === "green_only" || payload.execution_policy === "tdd"
|
|
100
|
+
? payload.execution_policy
|
|
101
|
+
: "tdd";
|
|
102
|
+
}
|
|
103
|
+
function validateExecutionRequirementPlan(changeRoot, executionPolicy, executionRequirementVersion = 2) {
|
|
89
104
|
const tasksPath = join(changeRoot, "tasks.md");
|
|
90
105
|
if (!existsSync(tasksPath))
|
|
91
106
|
return { ok: true, mode: false };
|
|
92
107
|
const tasksContent = readFileSync(tasksPath, "utf8");
|
|
93
108
|
const testContractPath = join(changeRoot, ".superspec", "artifacts", "test-contract.md");
|
|
94
109
|
const testContractContent = existsSync(testContractPath) ? readFileSync(testContractPath, "utf8") : null;
|
|
95
|
-
const validation = validateExecutionRequirements(tasksContent, testContractContent);
|
|
110
|
+
const validation = validateExecutionRequirements(tasksContent, testContractContent, executionPolicy, executionRequirementVersion);
|
|
96
111
|
return validation.ok
|
|
97
112
|
? { ok: true, mode: validation.mode }
|
|
98
113
|
: { ok: false, mode: validation.mode, message: validation.errors.join(";") };
|
|
@@ -116,6 +131,15 @@ export function proposalDocsBaseline(changeRoot) {
|
|
|
116
131
|
}
|
|
117
132
|
return baseline;
|
|
118
133
|
}
|
|
134
|
+
export function discoveryDocsBaseline(changeRoot) {
|
|
135
|
+
// 与 Explore gate 使用同一组审查目标。回退到 explore 后,至少要更新一项
|
|
136
|
+
// discovery 材料,才允许重新进入 propose,避免把一次纯状态回退误当作新探索轮次。
|
|
137
|
+
const baseline = {};
|
|
138
|
+
for (const doc of EXPLORE_DISCOVERY_REVIEW_GATE.reviewTargets) {
|
|
139
|
+
baseline[doc] = docRef(changeRoot, doc).sha;
|
|
140
|
+
}
|
|
141
|
+
return baseline;
|
|
142
|
+
}
|
|
119
143
|
function isDigestMap(value) {
|
|
120
144
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
121
145
|
return false;
|
|
@@ -152,10 +176,28 @@ export function latestReopenProposeBaseline(events) {
|
|
|
152
176
|
}
|
|
153
177
|
return null;
|
|
154
178
|
}
|
|
179
|
+
export function latestReopenExploreBaseline(events) {
|
|
180
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
181
|
+
const ev = events[i];
|
|
182
|
+
if (ev.event_type !== "transition_commit")
|
|
183
|
+
continue;
|
|
184
|
+
const payload = ev.payload;
|
|
185
|
+
if (payload.transition !== "reopen" || payload.reopen_target !== "explore")
|
|
186
|
+
continue;
|
|
187
|
+
if (!payload.baseline_docs || typeof payload.baseline_docs !== "object" || Array.isArray(payload.baseline_docs))
|
|
188
|
+
return null;
|
|
189
|
+
return payload.baseline_docs;
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
155
193
|
export function proposalDocsChangedSinceBaseline(changeRoot, baseline) {
|
|
156
194
|
const current = proposalDocsBaseline(changeRoot);
|
|
157
195
|
return Object.entries(baseline).some(([path, digest]) => current[path] !== digest);
|
|
158
196
|
}
|
|
197
|
+
export function discoveryDocsChangedSinceBaseline(changeRoot, baseline) {
|
|
198
|
+
const current = discoveryDocsBaseline(changeRoot);
|
|
199
|
+
return Object.entries(baseline).some(([path, digest]) => current[path] !== digest);
|
|
200
|
+
}
|
|
159
201
|
export function pendingTaskIds(changeRoot) {
|
|
160
202
|
const tasksContent = readFileSync(join(changeRoot, "tasks.md"), "utf8");
|
|
161
203
|
return pendingTasksInContent(tasksContent).map(task => task.taskId);
|
|
@@ -171,6 +213,27 @@ function latestStartApplyIndex(events) {
|
|
|
171
213
|
}
|
|
172
214
|
return -1;
|
|
173
215
|
}
|
|
216
|
+
function executionRequirementVersionFromPayload(payload) {
|
|
217
|
+
return payload.execution_requirement_version === 2 ? 2 : 1;
|
|
218
|
+
}
|
|
219
|
+
/** 当前 Apply round 的规则版本;缺失版本的历史 event 保持 v1 回放。 */
|
|
220
|
+
export function executionRequirementVersionForCurrentRound(events) {
|
|
221
|
+
const index = latestStartApplyIndex(events);
|
|
222
|
+
return index < 0 ? 1 : executionRequirementVersionFromPayload(events[index].payload);
|
|
223
|
+
}
|
|
224
|
+
/** Propose-ready 的规则版本决定随后 start-apply 是否采用新的全任务声明要求。 */
|
|
225
|
+
function executionRequirementVersionForProposeRound(events) {
|
|
226
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
227
|
+
const event = events[i];
|
|
228
|
+
if (event.event_type !== "transition_commit")
|
|
229
|
+
continue;
|
|
230
|
+
const payload = event.payload;
|
|
231
|
+
if (payload.transition === "propose-ready" && payload.to_state === "propose_ready") {
|
|
232
|
+
return executionRequirementVersionFromPayload(event.payload);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return 1;
|
|
236
|
+
}
|
|
174
237
|
export function applyRequirementModeForCurrentRound(events) {
|
|
175
238
|
const index = latestStartApplyIndex(events);
|
|
176
239
|
if (index < 0)
|
|
@@ -238,14 +301,13 @@ export function pendingTaskStatusForApply(changeRoot, events) {
|
|
|
238
301
|
export function formatPendingTaskMessage(ids, action) {
|
|
239
302
|
return `尚有未完成任务:${ids.join(", ")};${action}`;
|
|
240
303
|
}
|
|
241
|
-
function nextArgv(change,
|
|
304
|
+
function nextArgv(change, _risk) {
|
|
242
305
|
return [
|
|
243
306
|
"superspec",
|
|
244
307
|
"transition",
|
|
245
308
|
"next",
|
|
246
309
|
"--change",
|
|
247
310
|
change,
|
|
248
|
-
...(risk === "strict" ? [] : ["--risk", risk]),
|
|
249
311
|
];
|
|
250
312
|
}
|
|
251
313
|
function acceptedMaterialFollowup(change, risk, planDocsChangedSinceAccept) {
|
|
@@ -281,6 +343,15 @@ export function planNextStep(context) {
|
|
|
281
343
|
}
|
|
282
344
|
return { kind: "run_transition", state: "init", transition: "explore", reason: "初始化完成,开始探索" };
|
|
283
345
|
case "explore": {
|
|
346
|
+
const reopenBaseline = latestReopenExploreBaseline(events);
|
|
347
|
+
if (reopenBaseline && !discoveryDocsChangedSinceBaseline(changeRoot, reopenBaseline)) {
|
|
348
|
+
return {
|
|
349
|
+
kind: "run_transition",
|
|
350
|
+
state: "explore",
|
|
351
|
+
transition: "explore",
|
|
352
|
+
reason: "回到 explore 后至少一个 discovery 材料必须变化",
|
|
353
|
+
};
|
|
354
|
+
}
|
|
284
355
|
const exploreReviewJobs = EXPLORE_DISCOVERY_REVIEW_GATE.openJobsForGate(snapshot);
|
|
285
356
|
if (exploreReviewJobs.length > 0) {
|
|
286
357
|
return requiredJobs("explore", exploreReviewJobs, `有 ${exploreReviewJobs.length} 个待完成探索审查工作项`);
|
|
@@ -352,6 +423,16 @@ export function planNextStep(context) {
|
|
|
352
423
|
if (confirmation)
|
|
353
424
|
return confirmation;
|
|
354
425
|
}
|
|
426
|
+
// 尚未获得用户确认时,按本次 next 的候选风险预检失败不能退回默认 strict
|
|
427
|
+
// 的 start-apply 命令;否则 normal/minimal 的策略不匹配会被静默改写为 strict。
|
|
428
|
+
if (startApplyPlan.kind === "skip") {
|
|
429
|
+
const ask = {
|
|
430
|
+
question: `${startApplyPlan.message}。请更新计划材料后重新执行 next。`,
|
|
431
|
+
allowed_answers: ["计划已更新"],
|
|
432
|
+
scope: "propose_apply_preflight",
|
|
433
|
+
};
|
|
434
|
+
return { kind: "ask_user", state: "propose_ready", ask, reason: startApplyPlan.message };
|
|
435
|
+
}
|
|
355
436
|
return { kind: "run_transition", state: "propose_ready", transition: "start-apply", reason: "计划就绪,开始执行" };
|
|
356
437
|
}
|
|
357
438
|
case "apply":
|
|
@@ -628,6 +709,10 @@ function planExploreTransition(context) {
|
|
|
628
709
|
if (snapshot.state !== "explore") {
|
|
629
710
|
return { kind: "skip", message: `当前状态 ${snapshot.state},explore 不适用` };
|
|
630
711
|
}
|
|
712
|
+
const reopenBaseline = latestReopenExploreBaseline(events);
|
|
713
|
+
if (reopenBaseline && !discoveryDocsChangedSinceBaseline(changeRoot, reopenBaseline)) {
|
|
714
|
+
return { kind: "skip", message: "回到 explore 后至少一个 discovery 材料必须变化" };
|
|
715
|
+
}
|
|
631
716
|
const discoveryCheck = validateDiscovery(changeRoot);
|
|
632
717
|
if (!discoveryCheck.ok)
|
|
633
718
|
return { kind: "skip", message: discoveryCheck.message };
|
|
@@ -656,7 +741,7 @@ function planProposeReadyTransition(context) {
|
|
|
656
741
|
const tasksPlanError = validateTasksPlan(changeRoot);
|
|
657
742
|
if (tasksPlanError)
|
|
658
743
|
return { kind: "skip", message: tasksPlanError };
|
|
659
|
-
const executionRequirementPlan = validateExecutionRequirementPlan(changeRoot);
|
|
744
|
+
const executionRequirementPlan = validateExecutionRequirementPlan(changeRoot, executionPolicyForRisk(risk), 2);
|
|
660
745
|
if (!executionRequirementPlan.ok)
|
|
661
746
|
return { kind: "skip", message: executionRequirementPlan.message };
|
|
662
747
|
const openQuestions = collectProposeOpenQuestions(changeRoot);
|
|
@@ -676,8 +761,17 @@ function planProposeReadyTransition(context) {
|
|
|
676
761
|
fromState: "propose",
|
|
677
762
|
toState: "propose_ready",
|
|
678
763
|
reason: `risk=${risk},所有需求已满足`,
|
|
764
|
+
payload: {
|
|
765
|
+
workflow_mode: risk,
|
|
766
|
+
execution_requirement_version: 2,
|
|
767
|
+
},
|
|
679
768
|
};
|
|
680
769
|
}
|
|
770
|
+
function acceptedProposeToApplyConfirmation(context, risk) {
|
|
771
|
+
const confirmation = phaseConfirmationForBoundary(context.projectRoot, context.events, context.snapshot, "propose_to_apply", risk);
|
|
772
|
+
const decision = confirmation ? latestAcceptedPhaseDecision(context.events, confirmation) : null;
|
|
773
|
+
return confirmation && decision?.decision === "advance" ? { confirmation, decision } : null;
|
|
774
|
+
}
|
|
681
775
|
function planStartApplyTransition(context, enforceConfirmation = true) {
|
|
682
776
|
const { changeRoot, events, projectRoot, snapshot } = context;
|
|
683
777
|
if (snapshot.state !== "propose_ready")
|
|
@@ -687,23 +781,31 @@ function planStartApplyTransition(context, enforceConfirmation = true) {
|
|
|
687
781
|
// 按基线实际键名提示:升级前留下的旧基线可能不含 specs/,静态清单会误导
|
|
688
782
|
return { kind: "skip", message: `回到 propose 后至少一个计划文档必须变化(基线绑定:${Object.keys(reopenBaseline).join("、")})` };
|
|
689
783
|
}
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
784
|
+
const risk = workflowRiskForProposeRound(events, context.mode.risk);
|
|
785
|
+
const executionRequirementVersion = executionRequirementVersionForProposeRound(events);
|
|
786
|
+
const requiredRoles = executionRequirementVersion === 2
|
|
787
|
+
? PROPOSE_FINAL_REVIEW_GATE.requiredRolesForRisk(risk)
|
|
788
|
+
: historicalProposeReadyRoles(events);
|
|
789
|
+
const gatePlan = reviewGatePlan(snapshot, events, changeRoot, PROPOSE_FINAL_REVIEW_GATE, requiredRoles);
|
|
790
|
+
if (gatePlan) {
|
|
791
|
+
return {
|
|
792
|
+
...gatePlan,
|
|
793
|
+
reason: `进入执行阶段前需要重新完成计划文档审查:${gatePlan.reason}`,
|
|
794
|
+
};
|
|
699
795
|
}
|
|
700
|
-
const
|
|
796
|
+
const acceptedConfirmation = enforceConfirmation
|
|
797
|
+
? acceptedProposeToApplyConfirmation(context, risk)
|
|
798
|
+
: null;
|
|
799
|
+
const executionPolicy = executionPolicyForRisk(risk);
|
|
800
|
+
const executionRequirementPlan = validateExecutionRequirementPlan(changeRoot, executionPolicy, executionRequirementVersion);
|
|
701
801
|
if (!executionRequirementPlan.ok)
|
|
702
802
|
return { kind: "skip", message: executionRequirementPlan.message };
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
803
|
+
if (enforceConfirmation && !acceptedConfirmation) {
|
|
804
|
+
const confirmation = phaseConfirmationForBoundary(projectRoot, events, snapshot, "propose_to_apply", risk);
|
|
805
|
+
return {
|
|
806
|
+
kind: "skip",
|
|
807
|
+
message: confirmation ? phaseConfirmationMissingMessage(confirmation) : "无法建立 Propose 阶段确认范围",
|
|
808
|
+
};
|
|
707
809
|
}
|
|
708
810
|
const gitHead = currentGitHead(projectRoot);
|
|
709
811
|
return {
|
|
@@ -715,7 +817,14 @@ function planStartApplyTransition(context, enforceConfirmation = true) {
|
|
|
715
817
|
apply_start_head: gitHead.head,
|
|
716
818
|
apply_start_head_reason: gitHead.reason,
|
|
717
819
|
apply_contract_mode: executionRequirementPlan.mode,
|
|
718
|
-
...(
|
|
820
|
+
...(executionRequirementVersion === 2 ? { execution_requirement_version: 2 } : {}),
|
|
821
|
+
execution_policy: executionPolicy,
|
|
822
|
+
workflow_mode: risk,
|
|
823
|
+
review_policy: {
|
|
824
|
+
review_risk: risk,
|
|
825
|
+
requires_verifier: risk !== "minimal",
|
|
826
|
+
},
|
|
827
|
+
...(acceptedConfirmation ? phaseConfirmationCommitPayload(acceptedConfirmation.confirmation, acceptedConfirmation.decision) : {}),
|
|
719
828
|
},
|
|
720
829
|
};
|
|
721
830
|
}
|
package/dist/record.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export declare function recordJobSubmitContent(projectRoot: string, change: stri
|
|
|
7
7
|
export declare function recordUserDecision(projectRoot: string, change: string, inputFile: string): RecordResult;
|
|
8
8
|
/** record user-decision:从 JSON 内容登记用户决策 */
|
|
9
9
|
export declare function recordUserDecisionContent(projectRoot: string, change: string, content: string): RecordResult;
|
|
10
|
-
/** jobs list(
|
|
10
|
+
/** jobs list(job 从 transition_commit.new_jobs 提取;reopen 可用 job_invalidated 关闭未完成工作项) */
|
|
11
11
|
export declare function jobsList(projectRoot: string, change: string): {
|
|
12
12
|
open: Job[];
|
|
13
13
|
accepted: Job[];
|
package/dist/record.js
CHANGED
|
@@ -4,7 +4,7 @@ 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
|
-
import { isPhaseConfirmationScope, phaseActionForAnswer, phaseConfirmationForCurrentState, } from "./phase_confirmation.js";
|
|
7
|
+
import { isPhaseConfirmationScope, phaseActionForAnswer, phaseConfirmationForCurrentState, workflowRiskForPhaseConfirmation, } from "./phase_confirmation.js";
|
|
8
8
|
import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_DECISION_SCOPE_PREFIX, codeReviewDecisionAnswerLabel, normalizeCodeReviewDecisionAnswer, } from "./code_review.js";
|
|
9
9
|
import { invalidReasonForSubmittedReport } from "./job_validity.js";
|
|
10
10
|
import { jobSubmitArgv } from "./job_action.js";
|
|
@@ -346,6 +346,10 @@ function jobTerminalState(events, jobId) {
|
|
|
346
346
|
}
|
|
347
347
|
return null;
|
|
348
348
|
}
|
|
349
|
+
function jobInvalidated(events, jobId) {
|
|
350
|
+
return events.some(ev => ev.event_type === "job_invalidated" &&
|
|
351
|
+
ev.payload.job_id === jobId);
|
|
352
|
+
}
|
|
349
353
|
function terminalJobSubmitResult(events, jobId, terminal, reportDigest) {
|
|
350
354
|
const existing = events.find(e => (e.event_type === "job_accepted" || e.event_type === "job_rejected")
|
|
351
355
|
&& e.payload.job_id === jobId
|
|
@@ -547,6 +551,9 @@ export function recordJobSubmit(projectRoot, change, changeRoot, jobId, reportFi
|
|
|
547
551
|
if (!job) {
|
|
548
552
|
return { event_type: "job_rejected", accepted: false, message: `工作项 ${jobId} 不存在` };
|
|
549
553
|
}
|
|
554
|
+
if (jobInvalidated(events, jobId)) {
|
|
555
|
+
return { accepted: false, message: `工作项 ${jobId} 已因回退到更早阶段失效,不接受新报告。` };
|
|
556
|
+
}
|
|
550
557
|
const terminal = jobTerminalState(events, jobId);
|
|
551
558
|
if (terminal) {
|
|
552
559
|
const reportDigest = sha256File(reportFile) ?? "sha256:unknown";
|
|
@@ -584,6 +591,9 @@ export function recordJobSubmitContent(projectRoot, change, changeRoot, jobId, r
|
|
|
584
591
|
if (!job) {
|
|
585
592
|
return { event_type: "job_rejected", accepted: false, message: `工作项 ${jobId} 不存在` };
|
|
586
593
|
}
|
|
594
|
+
if (jobInvalidated(events, jobId)) {
|
|
595
|
+
return { accepted: false, message: `工作项 ${jobId} 已因回退到更早阶段失效,不接受新报告。` };
|
|
596
|
+
}
|
|
587
597
|
const reportDigest = sha256Text(reportContent);
|
|
588
598
|
const terminal = jobTerminalState(events, jobId);
|
|
589
599
|
if (terminal) {
|
|
@@ -639,14 +649,9 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
|
|
|
639
649
|
});
|
|
640
650
|
const changeRoot = openspecChangeRoot(projectRoot, change);
|
|
641
651
|
const snapshot = rebuildSnapshot(projectRoot, change, changeRoot);
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
? decision.review_risk
|
|
646
|
-
: null;
|
|
647
|
-
const current = decisionRisk
|
|
648
|
-
? phaseConfirmationForCurrentState(projectRoot, events, snapshot, decisionRisk)
|
|
649
|
-
: null;
|
|
652
|
+
// mode 是状态机从配置/round 快照推导的输入,不接受 user-decision JSON 注入。
|
|
653
|
+
const decisionRisk = workflowRiskForPhaseConfirmation(projectRoot, events, snapshot);
|
|
654
|
+
const current = phaseConfirmationForCurrentState(projectRoot, events, snapshot, decisionRisk);
|
|
650
655
|
if (existingAccepted &&
|
|
651
656
|
current?.scope === decision.scope &&
|
|
652
657
|
latestAcceptedForScope?.event_id === existing.event_id) {
|
|
@@ -657,17 +662,15 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
|
|
|
657
662
|
};
|
|
658
663
|
}
|
|
659
664
|
const action = current ? phaseActionForAnswer(current, decision.answer) : null;
|
|
660
|
-
const rejectionReason = !
|
|
661
|
-
? "
|
|
662
|
-
:
|
|
663
|
-
? "
|
|
664
|
-
:
|
|
665
|
-
? "
|
|
666
|
-
: !
|
|
667
|
-
? "
|
|
668
|
-
:
|
|
669
|
-
? "missing_phase_confirmation_reason"
|
|
670
|
-
: null;
|
|
665
|
+
const rejectionReason = !current
|
|
666
|
+
? "phase_confirmation_not_pending"
|
|
667
|
+
: decision.scope !== current.scope
|
|
668
|
+
? "stale_phase_confirmation_scope"
|
|
669
|
+
: !action
|
|
670
|
+
? "invalid_phase_confirmation_answer"
|
|
671
|
+
: action.reason === "required" && !nonEmptyString(decision.reason)
|
|
672
|
+
? "missing_phase_confirmation_reason"
|
|
673
|
+
: null;
|
|
671
674
|
if (rejectionReason) {
|
|
672
675
|
if (existing &&
|
|
673
676
|
existing.payload.accepted === false &&
|
|
@@ -687,11 +690,9 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
|
|
|
687
690
|
}));
|
|
688
691
|
const message = rejectionReason === "invalid_phase_confirmation_answer" && current
|
|
689
692
|
? `阶段确认答复必须精确为:${current.ask.allowed_answers.join("、")}`
|
|
690
|
-
: rejectionReason === "
|
|
691
|
-
?
|
|
692
|
-
:
|
|
693
|
-
? action.reason_prompt ?? "当前选择必须写明原因"
|
|
694
|
-
: "阶段确认已失效或当前没有待确认的阶段边界,请重新执行 next";
|
|
693
|
+
: rejectionReason === "missing_phase_confirmation_reason" && action
|
|
694
|
+
? action.reason_prompt ?? "当前选择必须写明原因"
|
|
695
|
+
: "阶段确认已失效或当前没有待确认的阶段边界,请重新执行 next";
|
|
695
696
|
return { event_type: "user_decision_recorded", accepted: false, message };
|
|
696
697
|
}
|
|
697
698
|
phaseConfirmation = current;
|
|
@@ -857,7 +858,7 @@ export function recordUserDecisionContent(projectRoot, change, content) {
|
|
|
857
858
|
return recordUserDecisionLoaded(projectRoot, change, events, content, sha256Text(content));
|
|
858
859
|
});
|
|
859
860
|
}
|
|
860
|
-
/** jobs list(
|
|
861
|
+
/** jobs list(job 从 transition_commit.new_jobs 提取;reopen 可用 job_invalidated 关闭未完成工作项) */
|
|
861
862
|
export function jobsList(projectRoot, change) {
|
|
862
863
|
const events = readEvents(projectRoot, change);
|
|
863
864
|
const open = [];
|
|
@@ -888,6 +889,12 @@ export function jobsList(projectRoot, change) {
|
|
|
888
889
|
open.splice(idx, 1)[0];
|
|
889
890
|
rejected.push({ job_id, role });
|
|
890
891
|
}
|
|
892
|
+
else if (ev.event_type === "job_invalidated") {
|
|
893
|
+
const { job_id } = ev.payload;
|
|
894
|
+
const idx = open.findIndex(j => j.job_id === job_id);
|
|
895
|
+
if (idx >= 0)
|
|
896
|
+
open.splice(idx, 1);
|
|
897
|
+
}
|
|
891
898
|
}
|
|
892
899
|
return { open, accepted, rejected };
|
|
893
900
|
}
|
|
@@ -898,8 +905,9 @@ function packetFieldDescriptions() {
|
|
|
898
905
|
boundFiles: "本工作项绑定的文件清单;审查报告必须说明这些文件是否都看过。",
|
|
899
906
|
review_scope: "报告中的审查覆盖范围;普通 reviewer/verifier 用 checked_paths 回执全部绑定文件,code-reviewer 还需按专用协议说明未检查项。",
|
|
900
907
|
code_review_scope: "代码审查范围:从已审基点到当前 HEAD 的提交改动、工作区改动和未跟踪代码文件。",
|
|
901
|
-
task_execution_index: "按任务汇总的执行证据:每个任务(task
|
|
902
|
-
contract: "任务启动时的执行依据快照:tests/design/source/
|
|
908
|
+
task_execution_index: "按任务汇总的执行证据:每个任务(task)的执行依据、有效证据要求、声明测试、测试证据和改动文件。",
|
|
909
|
+
contract: "任务启动时的执行依据快照:tests/design/source/acceptance/guard 分别对应 测试/设计/来源/验收/边界;null 表示历史任务没有执行依据。",
|
|
910
|
+
required_evidence: "task-start 结合冻结策略编译并写入 attempt 的有效证据要求:test_ids、red_required、green_required 和允许的 GREEN 语义状态;null 表示历史任务按旧记录回放。",
|
|
903
911
|
changed_paths: "与某个任务(task)或代码状态检查相关的改动文件。",
|
|
904
912
|
changed_paths_partial_reason: "该任务(task)的提交段 diff 失败原因;存在时 changed_paths 只包含工作区对比结果,归属可能不完整。",
|
|
905
913
|
unattributed_paths: "代码审查范围中暂时无法归属到某个任务(task)的文件。",
|
|
@@ -974,10 +982,10 @@ export function jobsPacket(projectRoot, change, jobId) {
|
|
|
974
982
|
? `最小格式:{"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>"}。`
|
|
975
983
|
+ `报告结论为 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 表示需要使用者判断的混合问题。`
|
|
976
984
|
+ (packetContext?.task_execution_index
|
|
977
|
-
? `本工作项带任务执行索引(task_execution_index):按 task 对照其执行依据快照(contract)审查——实现路线对照 design 引用原文、累计 diff 对照 guard 边界、测试断言对照 tests 声明的 scenario;每项的 scope_note 是执行者登记的范围扩大说明,判断其合理性与验证充分性;changed_paths 是归属线索不是结论(null 表示未知);unattributed_paths 中的无主改动逐个判断合理性;coverage_exemption_refs 解释未绑定 task 的 TEST 豁免。`
|
|
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 豁免。`
|
|
978
986
|
: "")
|
|
979
987
|
: job.role === "verifier"
|
|
980
|
-
? `最小格式:{"role":"verifier","verdict":"pass|fail","findings":[]${hasReviewScope ? `,"review_scope":{"checked_paths":${JSON.stringify(job.boundFiles.map(file => file.path))}}` : ""}}。核对代码审查记录(code_review_gate):passed 必须能追溯到已接受的代码审查工作项,skipped 必须能证明本次没有代码类改动。核对代码审查问题闭环:实现修复任务必须带审查修复引用(review_fix_of:<job_id>#<problem_id
|
|
988
|
+
? `最小格式:{"role":"verifier","verdict":"pass|fail","findings":[]${hasReviewScope ? `,"review_scope":{"checked_paths":${JSON.stringify(job.boundFiles.map(file => file.path))}}` : ""}}。核对代码审查记录(code_review_gate):passed 必须能追溯到已接受的代码审查工作项,skipped 必须能证明本次没有代码类改动。核对代码审查问题闭环:实现修复任务必须带审查修复引用(review_fix_of:<job_id>#<problem_id>),方案/混合问题必须有用户决策或后续修复证据。按 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)。审查修复的回归测试运行可用回归覆盖任务列表(covers_task_ids)说明覆盖了哪些已完成任务;缺少任务尝试 ID(attempt_id)的旧证据只能弱引用。` +
|
|
981
989
|
(packetContext?.code_state_check
|
|
982
990
|
? `本工作项带代码状态检查(code_state_check):head_matches 为 false 或 changed_paths 非空表示代码审查后代码又发生变化,须在报告中列出差异并交主流程与用户裁决,不自行判定无害,也不据此自动否定已接受的代码审查。`
|
|
983
991
|
: "")
|
package/dist/review.js
CHANGED
|
@@ -42,11 +42,27 @@ function isReviewPolicy(value) {
|
|
|
42
42
|
typeof obj.requires_verifier === "boolean");
|
|
43
43
|
}
|
|
44
44
|
export function readReviewPolicyFromEvents(events) {
|
|
45
|
-
|
|
45
|
+
// 新格式在 start-apply 写入,并且只能读取最新 Apply round 的策略。
|
|
46
|
+
// 回退到该 round 内的 review-ready,是为了回放升级前的历史 event。
|
|
47
|
+
let latestStartApplyIndex = -1;
|
|
48
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
49
|
+
const event = events[i];
|
|
50
|
+
if (event.event_type !== "transition_commit")
|
|
51
|
+
continue;
|
|
52
|
+
const payload = event.payload;
|
|
53
|
+
if (payload.transition === "start-apply" && payload.to_state === "apply") {
|
|
54
|
+
latestStartApplyIndex = i;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (latestStartApplyIndex < 0)
|
|
59
|
+
return null;
|
|
60
|
+
for (let i = events.length - 1; i >= latestStartApplyIndex; i--) {
|
|
61
|
+
const ev = events[i];
|
|
46
62
|
if (ev.event_type !== "transition_commit")
|
|
47
63
|
continue;
|
|
48
64
|
const payload = ev.payload;
|
|
49
|
-
if (payload.transition !== "review-ready")
|
|
65
|
+
if (payload.transition !== "start-apply" && payload.transition !== "review-ready")
|
|
50
66
|
continue;
|
|
51
67
|
if (isReviewPolicy(payload.review_policy))
|
|
52
68
|
return payload.review_policy;
|
package/dist/sync.js
CHANGED
|
@@ -34,7 +34,8 @@ function replayEvents(events) {
|
|
|
34
34
|
}
|
|
35
35
|
break;
|
|
36
36
|
}
|
|
37
|
-
//
|
|
37
|
+
// job 只通过 transition_commit 的 new_jobs payload 创建。reopen 到更早阶段时,
|
|
38
|
+
// 已绑定旧材料的待完成工作项会被 job_invalidated 关闭,避免阻塞新一轮 gate。
|
|
38
39
|
case "job_accepted": {
|
|
39
40
|
const { job_id } = ev.payload;
|
|
40
41
|
const idx = openJobs.findIndex(j => j.job_id === job_id);
|
|
@@ -53,7 +54,13 @@ function replayEvents(events) {
|
|
|
53
54
|
}
|
|
54
55
|
break;
|
|
55
56
|
}
|
|
56
|
-
|
|
57
|
+
case "job_invalidated": {
|
|
58
|
+
const { job_id } = ev.payload;
|
|
59
|
+
const idx = openJobs.findIndex(j => j.job_id === job_id);
|
|
60
|
+
if (idx >= 0)
|
|
61
|
+
openJobs.splice(idx, 1);
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
57
64
|
case "task_started": {
|
|
58
65
|
const attempt = ev.payload;
|
|
59
66
|
activeAttempts.push(attempt);
|
|
@@ -71,8 +78,10 @@ function replayEvents(events) {
|
|
|
71
78
|
case "task_abandoned": {
|
|
72
79
|
const { attempt_id } = ev.payload;
|
|
73
80
|
const idx = activeAttempts.findIndex(a => a.attempt_id === attempt_id);
|
|
74
|
-
if (idx >= 0)
|
|
75
|
-
activeAttempts.splice(idx, 1);
|
|
81
|
+
if (idx >= 0) {
|
|
82
|
+
const [attempt] = activeAttempts.splice(idx, 1);
|
|
83
|
+
taskStatuses[attempt.task_id] = "todo";
|
|
84
|
+
}
|
|
76
85
|
break;
|
|
77
86
|
}
|
|
78
87
|
}
|
package/dist/task.js
CHANGED
|
@@ -4,6 +4,7 @@ import { join } from "node:path";
|
|
|
4
4
|
import { sha256Text, ensureChangeLayout, appendEvent, makeEvent, withLock, appendRawRecord, readEvents } from "./store.js";
|
|
5
5
|
import { tasksStructureDigest as formatDigest } from "./format.js";
|
|
6
6
|
import { RecordInputDecodingError, readRecordInputFile } from "./record_input.js";
|
|
7
|
+
import { GREEN_ONLY_NO_TDD_REASON } from "./types.js";
|
|
7
8
|
/** tasks.md 结构指纹(委托给 format.ts 统一实现) */
|
|
8
9
|
export function tasksStructureDigestOf(changeRoot) {
|
|
9
10
|
const p = join(changeRoot, "tasks.md");
|
|
@@ -124,16 +125,28 @@ function validateContractTestRunInput(tr, attempt) {
|
|
|
124
125
|
if (!["expected_failure", "expected_success", "characterization_pass"].includes(tr.semantic_status)) {
|
|
125
126
|
return { ok: false, message: "语义状态(semantic_status)必须是 expected_failure、expected_success 或 characterization_pass" };
|
|
126
127
|
}
|
|
128
|
+
const requiredEvidence = attempt.required_evidence;
|
|
129
|
+
if (tr.semantic_status === "expected_failure" && requiredEvidence && !requiredEvidence.red_required) {
|
|
130
|
+
return { ok: false, message: "当前任务执行快照不要求 RED(expected_failure);请登记声明 TEST 的 GREEN" };
|
|
131
|
+
}
|
|
132
|
+
if (tr.semantic_status === "expected_failure" && !requiredEvidence &&
|
|
133
|
+
attempt.execution_policy === "green_only" &&
|
|
134
|
+
attempt.no_tdd_reason === GREEN_ONLY_NO_TDD_REASON) {
|
|
135
|
+
return { ok: false, message: "GREEN-only 任务不登记 RED(expected_failure);请在实现后登记声明 TEST 的 GREEN" };
|
|
136
|
+
}
|
|
127
137
|
if (tr.semantic_status === "expected_failure" && tr.exit_code === 0) {
|
|
128
138
|
return { ok: false, message: "RED 预期失败(expected_failure)要求退出码(exit_code)非 0" };
|
|
129
139
|
}
|
|
130
140
|
if ((tr.semantic_status === "expected_success" || tr.semantic_status === "characterization_pass") && tr.exit_code !== 0) {
|
|
131
141
|
return { ok: false, message: `语义状态(semantic_status=${tr.semantic_status})要求退出码(exit_code)为 0` };
|
|
132
142
|
}
|
|
133
|
-
if (tr.semantic_status === "characterization_pass" &&
|
|
143
|
+
if (tr.semantic_status === "characterization_pass" && requiredEvidence && !requiredEvidence.accepted_green_statuses.includes("characterization_pass")) {
|
|
144
|
+
return { ok: false, message: "当前任务执行快照不接受特征化通过(characterization_pass)" };
|
|
145
|
+
}
|
|
146
|
+
if (tr.semantic_status === "characterization_pass" && !requiredEvidence && !(attempt.tdd_required === false && attempt.no_tdd_reason === "characterization")) {
|
|
134
147
|
return { ok: false, message: "特征化通过(characterization_pass)只适用于无需 TDD 的特征化任务(tdd_required:false,no_tdd_reason:characterization)" };
|
|
135
148
|
}
|
|
136
|
-
const declaredTests = attempt.contract?.tests ?? [];
|
|
149
|
+
const declaredTests = requiredEvidence?.test_ids ?? attempt.contract?.tests ?? [];
|
|
137
150
|
if (declaredTests.length > 0 && !declaredTests.includes(tr.test_id)) {
|
|
138
151
|
return { ok: false, message: `测试 ID(test_id=${tr.test_id})不属于当前任务契约声明的测试列表` };
|
|
139
152
|
}
|
package/dist/task_evidence.d.ts
CHANGED