@peterxiaoyang/superspec 0.1.44 → 0.1.46
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 +20 -2
- package/dist/format.js +217 -26
- 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/openspec.d.ts +13 -0
- package/dist/openspec.js +28 -0
- package/dist/phase_confirmation.d.ts +6 -0
- package/dist/phase_confirmation.js +22 -6
- package/dist/phase_plan.d.ts +10 -1
- package/dist/phase_plan.js +250 -37
- 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 +166 -29
- package/dist/types.d.ts +38 -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/agents/architect.toml +1 -1
- package/templates/workflow/agents/code-reviewer.toml +1 -1
- package/templates/workflow/agents/critic.toml +1 -1
- package/templates/workflow/agents/executor.toml +1 -1
- package/templates/workflow/agents/explore.toml +1 -1
- package/templates/workflow/agents/test-engineer.toml +1 -1
- package/templates/workflow/agents/test-runner.toml +1 -1
- package/templates/workflow/agents/verifier.toml +1 -1
- package/templates/workflow/prompts/architect.md +25 -33
- package/templates/workflow/prompts/code-reviewer.md +19 -67
- package/templates/workflow/prompts/critic.md +36 -87
- package/templates/workflow/prompts/executor.md +17 -19
- package/templates/workflow/prompts/explore.md +12 -46
- package/templates/workflow/prompts/test-engineer.md +22 -35
- package/templates/workflow/prompts/test-runner.md +11 -21
- package/templates/workflow/prompts/verifier.md +13 -37
- package/templates/workflow/skills/superspec-apply/SKILL.md +17 -85
- package/templates/workflow/skills/superspec-explore/SKILL.md +57 -66
- package/templates/workflow/skills/superspec-propose/SKILL.md +76 -129
- package/templates/workflow/skills/superspec-review/SKILL.md +14 -73
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
package/dist/task_evidence.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { parseTasksMd } from "./format.js";
|
|
4
4
|
import { readEvents, sha256Text } from "./store.js";
|
|
5
|
+
import { GREEN_ONLY_NO_TDD_REASON } from "./types.js";
|
|
5
6
|
export function taskEvidenceReadiness(projectRoot, change, changeRoot, attempt) {
|
|
6
7
|
if (attempt.contract_mode === true) {
|
|
7
8
|
return contractTaskEvidenceReadiness(projectRoot, change, attempt);
|
|
@@ -18,6 +19,9 @@ export function taskEvidenceReadiness(projectRoot, change, changeRoot, attempt)
|
|
|
18
19
|
if (currentDigest !== attempt.task_structure_digest)
|
|
19
20
|
missing.push("任务结构指纹");
|
|
20
21
|
if (!taskInfo.tddRequired) {
|
|
22
|
+
if (taskInfo.noTddReason === GREEN_ONLY_NO_TDD_REASON) {
|
|
23
|
+
missing.push(`${GREEN_ONLY_NO_TDD_REASON} 任务必须使用执行依据模式`);
|
|
24
|
+
}
|
|
21
25
|
if (!taskInfo.noTddReason)
|
|
22
26
|
missing.push("no_tdd_reason");
|
|
23
27
|
return missing.length === 0
|
|
@@ -48,7 +52,76 @@ export function taskEvidenceReadiness(projectRoot, change, changeRoot, attempt)
|
|
|
48
52
|
: { ready: false, missing, reason: missing.join("、") };
|
|
49
53
|
}
|
|
50
54
|
function contractTaskEvidenceReadiness(projectRoot, change, attempt) {
|
|
55
|
+
if (attempt.required_evidence) {
|
|
56
|
+
return effectiveContractTaskEvidenceReadiness(projectRoot, change, attempt);
|
|
57
|
+
}
|
|
58
|
+
return legacyContractTaskEvidenceReadiness(projectRoot, change, attempt);
|
|
59
|
+
}
|
|
60
|
+
/** 新执行依据模式:只消费 task-start 冻结的有效证据计划。 */
|
|
61
|
+
function effectiveContractTaskEvidenceReadiness(projectRoot, change, attempt) {
|
|
51
62
|
const missing = [];
|
|
63
|
+
const required = attempt.required_evidence;
|
|
64
|
+
const declaredTests = required.test_ids;
|
|
65
|
+
if (declaredTests.length === 0) {
|
|
66
|
+
const evidence = attemptLevelEvidence(projectRoot, change, attempt.attempt_id, new Set(required.accepted_green_statuses));
|
|
67
|
+
if (required.red_required && !evidence.hasRed)
|
|
68
|
+
missing.push("RED 证据");
|
|
69
|
+
if (required.green_required && !evidence.hasGreen)
|
|
70
|
+
missing.push("GREEN 证据");
|
|
71
|
+
return missing.length === 0
|
|
72
|
+
? { ready: true, missing: [] }
|
|
73
|
+
: { ready: false, missing, reason: missing.join("、") };
|
|
74
|
+
}
|
|
75
|
+
const perTest = new Map();
|
|
76
|
+
for (const testId of declaredTests) {
|
|
77
|
+
perTest.set(testId, { sawRed: false, sawGreen: false, paired: false });
|
|
78
|
+
}
|
|
79
|
+
for (const ev of readEvents(projectRoot, change)) {
|
|
80
|
+
if (ev.event_type !== "test_run_recorded")
|
|
81
|
+
continue;
|
|
82
|
+
const tr = ev.payload;
|
|
83
|
+
if (tr.attempt_id !== attempt.attempt_id)
|
|
84
|
+
continue;
|
|
85
|
+
if (typeof tr.test_id !== "string")
|
|
86
|
+
continue;
|
|
87
|
+
const state = perTest.get(tr.test_id);
|
|
88
|
+
if (!state)
|
|
89
|
+
continue;
|
|
90
|
+
if (tr.semantic_status === "expected_failure" && typeof tr.exit_code === "number" && tr.exit_code !== 0) {
|
|
91
|
+
state.sawRed = true;
|
|
92
|
+
}
|
|
93
|
+
const isAcceptedGreen = typeof tr.semantic_status === "string" &&
|
|
94
|
+
required.accepted_green_statuses.includes(tr.semantic_status) &&
|
|
95
|
+
tr.exit_code === 0;
|
|
96
|
+
if (isAcceptedGreen) {
|
|
97
|
+
if (state.sawRed)
|
|
98
|
+
state.paired = true;
|
|
99
|
+
state.sawGreen = true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (required.green_required) {
|
|
103
|
+
for (const [testId, state] of perTest) {
|
|
104
|
+
if (!state.sawGreen)
|
|
105
|
+
missing.push(`${testId} GREEN 证据`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (required.red_required) {
|
|
109
|
+
const hasPairedTest = [...perTest.values()].some(state => state.paired);
|
|
110
|
+
if (!hasPairedTest)
|
|
111
|
+
missing.push("同一 TEST 的 RED/GREEN 配对");
|
|
112
|
+
}
|
|
113
|
+
return missing.length === 0
|
|
114
|
+
? { ready: true, missing: [] }
|
|
115
|
+
: { ready: false, missing, reason: missing.join("、") };
|
|
116
|
+
}
|
|
117
|
+
/** 历史执行依据模式仍按当时写入 attempt 的标记回放。 */
|
|
118
|
+
function legacyContractTaskEvidenceReadiness(projectRoot, change, attempt) {
|
|
119
|
+
const missing = [];
|
|
120
|
+
const executionPolicy = attempt.execution_policy ?? "tdd";
|
|
121
|
+
const greenOnly = executionPolicy === "green_only" && attempt.no_tdd_reason === GREEN_ONLY_NO_TDD_REASON;
|
|
122
|
+
if (attempt.no_tdd_reason === GREEN_ONLY_NO_TDD_REASON && !greenOnly) {
|
|
123
|
+
missing.push(`${GREEN_ONLY_NO_TDD_REASON} 任务不适用于当前 TDD apply`);
|
|
124
|
+
}
|
|
52
125
|
const tddRequired = attempt.tdd_required !== false;
|
|
53
126
|
const characterization = attempt.tdd_required === false && attempt.no_tdd_reason === "characterization";
|
|
54
127
|
const declaredTests = attempt.contract?.tests ?? [];
|
|
@@ -56,8 +129,11 @@ function contractTaskEvidenceReadiness(projectRoot, change, attempt) {
|
|
|
56
129
|
missing.push("no_tdd_reason");
|
|
57
130
|
}
|
|
58
131
|
if (declaredTests.length === 0) {
|
|
59
|
-
if (
|
|
60
|
-
|
|
132
|
+
if (greenOnly) {
|
|
133
|
+
missing.push(`${GREEN_ONLY_NO_TDD_REASON} 任务缺少声明 TEST`);
|
|
134
|
+
}
|
|
135
|
+
else if (tddRequired) {
|
|
136
|
+
const hasAttemptEvidence = attemptLevelEvidence(projectRoot, change, attempt.attempt_id, new Set(["expected_success"]));
|
|
61
137
|
if (!hasAttemptEvidence.hasRed)
|
|
62
138
|
missing.push("RED 证据");
|
|
63
139
|
if (!hasAttemptEvidence.hasGreen)
|
|
@@ -75,16 +151,13 @@ function contractTaskEvidenceReadiness(projectRoot, change, attempt) {
|
|
|
75
151
|
if (ev.event_type !== "test_run_recorded")
|
|
76
152
|
continue;
|
|
77
153
|
const tr = ev.payload;
|
|
78
|
-
if (tr.attempt_id !== attempt.attempt_id)
|
|
79
|
-
continue;
|
|
80
|
-
if (typeof tr.test_id !== "string")
|
|
154
|
+
if (tr.attempt_id !== attempt.attempt_id || typeof tr.test_id !== "string")
|
|
81
155
|
continue;
|
|
82
156
|
const state = perTest.get(tr.test_id);
|
|
83
157
|
if (!state)
|
|
84
158
|
continue;
|
|
85
|
-
if (tr.semantic_status === "expected_failure" && typeof tr.exit_code === "number" && tr.exit_code !== 0)
|
|
159
|
+
if (tr.semantic_status === "expected_failure" && typeof tr.exit_code === "number" && tr.exit_code !== 0)
|
|
86
160
|
state.sawRed = true;
|
|
87
|
-
}
|
|
88
161
|
const isExpectedSuccess = tr.semantic_status === "expected_success" && tr.exit_code === 0;
|
|
89
162
|
const isCharacterizationPass = characterization && tr.semantic_status === "characterization_pass" && tr.exit_code === 0;
|
|
90
163
|
if (isExpectedSuccess || isCharacterizationPass) {
|
|
@@ -97,7 +170,7 @@ function contractTaskEvidenceReadiness(projectRoot, change, attempt) {
|
|
|
97
170
|
if (!state.sawGreen)
|
|
98
171
|
missing.push(`${testId} GREEN 证据`);
|
|
99
172
|
}
|
|
100
|
-
if (tddRequired) {
|
|
173
|
+
if (tddRequired && !greenOnly) {
|
|
101
174
|
const hasPairedTest = [...perTest.values()].some(state => state.paired);
|
|
102
175
|
if (!hasPairedTest)
|
|
103
176
|
missing.push("同一 TEST 的 RED/GREEN 配对");
|
|
@@ -106,7 +179,7 @@ function contractTaskEvidenceReadiness(projectRoot, change, attempt) {
|
|
|
106
179
|
? { ready: true, missing: [] }
|
|
107
180
|
: { ready: false, missing, reason: missing.join("、") };
|
|
108
181
|
}
|
|
109
|
-
function
|
|
182
|
+
function attemptLevelEvidence(projectRoot, change, attemptId, acceptedGreenStatuses) {
|
|
110
183
|
let hasRed = false;
|
|
111
184
|
let hasGreen = false;
|
|
112
185
|
for (const ev of readEvents(projectRoot, change)) {
|
|
@@ -118,7 +191,9 @@ function attemptLevelRedGreenEvidence(projectRoot, change, attemptId) {
|
|
|
118
191
|
if (tr.semantic_status === "expected_failure" && typeof tr.exit_code === "number" && tr.exit_code !== 0) {
|
|
119
192
|
hasRed = true;
|
|
120
193
|
}
|
|
121
|
-
if (tr.semantic_status === "
|
|
194
|
+
if (typeof tr.semantic_status === "string" &&
|
|
195
|
+
acceptedGreenStatuses.has(tr.semantic_status) &&
|
|
196
|
+
tr.exit_code === 0) {
|
|
122
197
|
hasGreen = true;
|
|
123
198
|
}
|
|
124
199
|
}
|
package/dist/transition.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ReviewRisk } from "./review.ts";
|
|
1
2
|
import type { Snapshot, State, Job, TransitionResult } from "./types.ts";
|
|
2
3
|
interface Decision {
|
|
3
4
|
fromState: State;
|
|
@@ -31,16 +32,16 @@ export declare function commitTransition(projectRoot: string, change: string, ch
|
|
|
31
32
|
decide: (snapshot: Snapshot) => Decision | SkipDecision | BlockedDecision;
|
|
32
33
|
idempotencyInputs?: Record<string, unknown>;
|
|
33
34
|
}): TransitionResult;
|
|
34
|
-
export declare function proposeReady(projectRoot: string, change: string, changeRoot: string, risk?:
|
|
35
|
+
export declare function proposeReady(projectRoot: string, change: string, changeRoot: string, risk?: ReviewRisk): TransitionResult;
|
|
35
36
|
export declare function transitionInit(projectRoot: string, change: string, changeRoot: string): TransitionResult;
|
|
36
|
-
export declare function transitionExplore(projectRoot: string, change: string, changeRoot: string, risk?:
|
|
37
|
+
export declare function transitionExplore(projectRoot: string, change: string, changeRoot: string, risk?: ReviewRisk): TransitionResult;
|
|
37
38
|
export declare function startApply(projectRoot: string, change: string, changeRoot: string): TransitionResult;
|
|
38
39
|
export declare function taskStart(projectRoot: string, change: string, changeRoot: string, taskId: string): TransitionResult;
|
|
39
40
|
export declare function reopen(projectRoot: string, change: string, changeRoot: string, to: State, reason: string, opts?: {
|
|
40
41
|
reviewFix?: string;
|
|
41
42
|
reviewFinding?: string;
|
|
42
43
|
}): TransitionResult;
|
|
43
|
-
export declare function reviewReady(projectRoot: string, change: string, changeRoot: string, risk?:
|
|
44
|
+
export declare function reviewReady(projectRoot: string, change: string, changeRoot: string, risk?: ReviewRisk): TransitionResult;
|
|
44
45
|
export declare function accept(projectRoot: string, change: string, changeRoot: string): TransitionResult;
|
|
45
46
|
export declare function taskComplete(projectRoot: string, change: string, changeRoot: string, taskId: string, inputContent?: string | null): TransitionResult;
|
|
46
47
|
export {};
|
package/dist/transition.js
CHANGED
|
@@ -9,9 +9,10 @@ import { REVIEW_CODE_REVIEW_GATE_ID, REVIEW_FINAL_VERIFIER_GATE_ID, reviewScopeF
|
|
|
9
9
|
import { codeReviewBoundFiles, codeReviewDecisionScope, codeReviewJobStaleReason, codeReviewPacketContext, codeReviewPacketDigest, collectCodeReviewGateFacts, computeCodeStateCheck, currentCodeReviewWorkingPaths, dismissedCodeReviewSummary, latestCodeReviewDecision, latestCodeReviewFailedStatus, missingCoverageExemptionTestIds, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, } from "./code_review.js";
|
|
10
10
|
import { taskEvidenceReadiness } from "./task_evidence.js";
|
|
11
11
|
import { adoptedContractForTask, findTaskInLines, isReviewFixTaskId, parseTasksMd, parseTestContractEntries, } from "./format.js";
|
|
12
|
-
import { applyRequirementModeForCurrentRound, blockingJobsForApplyDone, formatPendingTaskMessage, latestAcceptedProposalBaseline, pendingTaskStatusForApply, planTransition, proposalDocsBaseline, } from "./phase_plan.js";
|
|
12
|
+
import { applyRequirementModeForCurrentRound, executionRequirementVersionForCurrentRound, blockingJobsForApplyDone, executionPolicyForCurrentRound, formatPendingTaskMessage, latestAcceptedProposalBaseline, pendingTaskStatusForApply, planningValidationProfileForNewRound, planTransition, discoveryDocsBaseline, proposalDocsBaseline, } from "./phase_plan.js";
|
|
13
13
|
import { latestAcceptedPhaseDecision, phaseConfirmationCommitPayload, phaseConfirmationForBoundary, phaseConfirmationMissingMessage, } from "./phase_confirmation.js";
|
|
14
|
-
import { currentGitHead, dirtyCodeFiles } from "./git_state.js";
|
|
14
|
+
import { currentGitHead, dirtyCodeFiles, stageProductionJavaFilesSince } from "./git_state.js";
|
|
15
|
+
import { workflowRiskForProject } from "./workflow_config.js";
|
|
15
16
|
let transitionSeq = 0;
|
|
16
17
|
function newTransitionId() { return `T-${Date.now()}-${++transitionSeq}`; }
|
|
17
18
|
let jobSeq = 0;
|
|
@@ -81,6 +82,17 @@ function boundarySnapshotPayload(projectRoot) {
|
|
|
81
82
|
...(result.reason ? { boundary_snapshot_reason: result.reason } : {}),
|
|
82
83
|
};
|
|
83
84
|
}
|
|
85
|
+
function boundarySnapshotForTaskAttempt(events, attemptId) {
|
|
86
|
+
const start = events.findLast(event => event.event_type === "task_started" &&
|
|
87
|
+
event.payload.attempt_id === attemptId);
|
|
88
|
+
const boundary = start?.payload?.boundary_snapshot;
|
|
89
|
+
if (!boundary || typeof boundary !== "object" || Array.isArray(boundary))
|
|
90
|
+
return null;
|
|
91
|
+
const value = boundary;
|
|
92
|
+
if (!Array.isArray(value.dirty_files))
|
|
93
|
+
return null;
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
84
96
|
function parseScopeNoteInput(inputContent) {
|
|
85
97
|
if (inputContent == null)
|
|
86
98
|
return { ok: true, value: null, digest: null };
|
|
@@ -130,9 +142,41 @@ function parseScopeNoteInput(inputContent) {
|
|
|
130
142
|
},
|
|
131
143
|
};
|
|
132
144
|
}
|
|
133
|
-
function validateTaskStartContract(changeRoot, taskId,
|
|
134
|
-
if (
|
|
145
|
+
function validateTaskStartContract(changeRoot, taskId, parsedContract) {
|
|
146
|
+
if (parsedContract.errors.length > 0)
|
|
147
|
+
return parsedContract.errors.join(";");
|
|
148
|
+
if (!parsedContract.declaredFields.includes("tests"))
|
|
149
|
+
return `${taskId} 的执行依据缺少测试字段`;
|
|
150
|
+
if (!parsedContract.contract.design)
|
|
151
|
+
return `${taskId} 的执行依据缺少设计`;
|
|
152
|
+
if (parsedContract.contract.source.length === 0)
|
|
153
|
+
return `${taskId} 的执行依据缺少来源`;
|
|
154
|
+
if (!parsedContract.contract.acceptance)
|
|
155
|
+
return `${taskId} 的执行依据缺少验收目标`;
|
|
156
|
+
if (!parsedContract.contract.guard)
|
|
157
|
+
return `${taskId} 的执行依据缺少边界`;
|
|
158
|
+
if (parsedContract.contract.tests.length === 0)
|
|
135
159
|
return null;
|
|
160
|
+
const testContractPath = join(changeRoot, ".superspec", "artifacts", "test-contract.md");
|
|
161
|
+
if (!existsSync(testContractPath))
|
|
162
|
+
return "test-contract.md 不存在,无法校验执行依据测试引用";
|
|
163
|
+
const parsed = parseTestContractEntries(readFileSync(testContractPath, "utf8"));
|
|
164
|
+
if (!parsed.ok)
|
|
165
|
+
return parsed.message;
|
|
166
|
+
const known = new Set(parsed.entries.map(entry => entry.test_id));
|
|
167
|
+
const missing = parsedContract.contract.tests.filter(testId => !known.has(testId));
|
|
168
|
+
return missing.length > 0 ? `执行依据引用了不存在的 TEST ID:${missing.join(", ")}` : null;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* v1 只回放旧 contract 约束:普通 TDD task 必须保有执行依据和 TEST 引用,
|
|
172
|
+
* 但不要求 v2 新增的五字段,避免阻断历史 documentation-only task。
|
|
173
|
+
*/
|
|
174
|
+
function validateLegacyTaskStartContract(changeRoot, taskId, tddRequired, parsedContract) {
|
|
175
|
+
if (!parsedContract) {
|
|
176
|
+
return tddRequired && !isReviewFixTaskId(taskId)
|
|
177
|
+
? `执行依据模式下,普通 TDD 任务 ${taskId} 缺少执行依据`
|
|
178
|
+
: null;
|
|
179
|
+
}
|
|
136
180
|
if (parsedContract.errors.length > 0)
|
|
137
181
|
return parsedContract.errors.join(";");
|
|
138
182
|
if (tddRequired && !isReviewFixTaskId(taskId) && parsedContract.contract.tests.length === 0) {
|
|
@@ -150,6 +194,21 @@ function validateTaskStartContract(changeRoot, taskId, tddRequired, parsedContra
|
|
|
150
194
|
const missing = parsedContract.contract.tests.filter(testId => !known.has(testId));
|
|
151
195
|
return missing.length > 0 ? `执行依据引用了不存在的 TEST ID:${missing.join(", ")}` : null;
|
|
152
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* 只在 task-start 编译新模式 task 的有效证据要求。Propose 只声明 TEST,
|
|
199
|
+
* RED 是否要求由这里读取已冻结的 execution_policy 决定,之后不再依赖 tasks.md。
|
|
200
|
+
*/
|
|
201
|
+
function compileRequiredEvidence(executionPolicy, testIds, requiresVerificationWithoutDeclaredTest) {
|
|
202
|
+
// REVIEW-FIX 没有计划阶段声明的 TEST,但仍必须登记一次真实回归验证。
|
|
203
|
+
// 是否需要 RED 始终由已冻结的 execution_policy 决定。
|
|
204
|
+
const requiresVerification = testIds.length > 0 || requiresVerificationWithoutDeclaredTest;
|
|
205
|
+
return {
|
|
206
|
+
test_ids: testIds,
|
|
207
|
+
red_required: executionPolicy === "tdd" && requiresVerification,
|
|
208
|
+
green_required: requiresVerification,
|
|
209
|
+
accepted_green_statuses: ["expected_success"],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
153
212
|
function hasRejectedReviewReadyVerifier(events) {
|
|
154
213
|
const reviewReadyVerifierIds = new Set();
|
|
155
214
|
for (const ev of events) {
|
|
@@ -235,7 +294,7 @@ function appendReviewFixTask(changeRoot, ref, finding) {
|
|
|
235
294
|
const description = typeof finding.description === "string" && finding.description.trim()
|
|
236
295
|
? finding.description.trim().replace(/\s+/g, " ")
|
|
237
296
|
: `修复代码审查问题 ${ref.findingId}`;
|
|
238
|
-
const line = `- [ ] ${reviewFixTaskId(ref)} ${description}
|
|
297
|
+
const line = `- [ ] ${reviewFixTaskId(ref)} ${description} ${marker}`;
|
|
239
298
|
const suffix = content.endsWith("\n") ? "" : "\n";
|
|
240
299
|
writeFileSync(tasksPath, `${content}${suffix}${line}\n`);
|
|
241
300
|
return "created";
|
|
@@ -416,7 +475,7 @@ function evaluateFinalVerifierGate(input) {
|
|
|
416
475
|
return { skip: true, message: "已在 review 状态,最终验证仍然有效" };
|
|
417
476
|
}
|
|
418
477
|
function authorizePhaseAdvance(input) {
|
|
419
|
-
const confirmation = phaseConfirmationForBoundary(input.projectRoot, input.events, input.snapshot, input.boundary);
|
|
478
|
+
const confirmation = phaseConfirmationForBoundary(input.projectRoot, input.events, input.snapshot, input.boundary, input.risk);
|
|
420
479
|
const phaseDecision = confirmation
|
|
421
480
|
? latestAcceptedPhaseDecision(input.events, confirmation)
|
|
422
481
|
: null;
|
|
@@ -540,7 +599,7 @@ export function commitTransition(projectRoot, change, changeRoot, opts) {
|
|
|
540
599
|
});
|
|
541
600
|
}
|
|
542
601
|
// ===== propose-ready =====
|
|
543
|
-
export function proposeReady(projectRoot, change, changeRoot, risk =
|
|
602
|
+
export function proposeReady(projectRoot, change, changeRoot, risk = workflowRiskForProject(projectRoot)) {
|
|
544
603
|
return commitTransition(projectRoot, change, changeRoot, {
|
|
545
604
|
name: "propose-ready", idempotencyInputs: { risk },
|
|
546
605
|
decide: (snapshot) => {
|
|
@@ -570,7 +629,7 @@ export function transitionInit(projectRoot, change, changeRoot) {
|
|
|
570
629
|
});
|
|
571
630
|
}
|
|
572
631
|
// ===== explore =====
|
|
573
|
-
export function transitionExplore(projectRoot, change, changeRoot, risk =
|
|
632
|
+
export function transitionExplore(projectRoot, change, changeRoot, risk = workflowRiskForProject(projectRoot)) {
|
|
574
633
|
return commitTransition(projectRoot, change, changeRoot, {
|
|
575
634
|
name: "explore", idempotencyInputs: { phase: "explore", risk },
|
|
576
635
|
decide: (snapshot) => {
|
|
@@ -599,7 +658,7 @@ export function startApply(projectRoot, change, changeRoot) {
|
|
|
599
658
|
changeRoot,
|
|
600
659
|
events,
|
|
601
660
|
snapshot,
|
|
602
|
-
mode: { kind: "risk", risk:
|
|
661
|
+
mode: { kind: "risk", risk: workflowRiskForProject(projectRoot) },
|
|
603
662
|
});
|
|
604
663
|
return transitionPlanToDecision(snapshot, changeRoot, change, plan, events);
|
|
605
664
|
},
|
|
@@ -620,6 +679,8 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
|
|
|
620
679
|
if (taskLineIdx < 0)
|
|
621
680
|
return { skip: true, message: `任务 ${taskId} 不存在` };
|
|
622
681
|
const contractMode = applyRequirementModeForCurrentRound(events);
|
|
682
|
+
const executionRequirementVersion = executionRequirementVersionForCurrentRound(events);
|
|
683
|
+
const executionPolicy = executionPolicyForCurrentRound(events);
|
|
623
684
|
if (contractMode && pendingTaskStatusForApply(changeRoot, events).completedByEvent.includes(taskId)) {
|
|
624
685
|
return { skip: true, message: `任务 ${taskId} 已通过完成事件完成` };
|
|
625
686
|
}
|
|
@@ -634,23 +695,38 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
|
|
|
634
695
|
// 统一出口:adopted.contract 非 null 当且仅当契约模式下有绑定块;
|
|
635
696
|
// legacy 轮即使 task 带执行依据文本也输出 null,避免"看似契约、实按 legacy 校验"的误导态
|
|
636
697
|
const adopted = adoptedContractForTask(tasksContent, taskId, contractMode);
|
|
637
|
-
|
|
638
|
-
|
|
698
|
+
const reviewFix = isReviewFixTaskId(taskId);
|
|
699
|
+
if (contractMode && executionRequirementVersion === 2 && !reviewFix && !adopted.parsed) {
|
|
700
|
+
return { skip: true, message: `执行依据模式下,任务 ${taskId} 缺少执行依据` };
|
|
701
|
+
}
|
|
702
|
+
if (contractMode && executionRequirementVersion === 2 && !reviewFix && adopted.parsed) {
|
|
703
|
+
const contractError = validateTaskStartContract(changeRoot, taskId, adopted.parsed);
|
|
704
|
+
if (contractError)
|
|
705
|
+
return { skip: true, message: contractError };
|
|
639
706
|
}
|
|
640
|
-
if (contractMode) {
|
|
641
|
-
const contractError =
|
|
707
|
+
if (contractMode && executionRequirementVersion === 1) {
|
|
708
|
+
const contractError = validateLegacyTaskStartContract(changeRoot, taskId, taskInfo.tddRequired, adopted.parsed);
|
|
642
709
|
if (contractError)
|
|
643
710
|
return { skip: true, message: contractError };
|
|
644
711
|
}
|
|
645
712
|
const structureDigest = sha256Text(tasksContent.replace(/- \[[xX]\]/g, "- [ ]"));
|
|
713
|
+
const effectivePolicy = executionPolicy;
|
|
714
|
+
const requiredEvidence = contractMode && executionRequirementVersion === 2
|
|
715
|
+
? compileRequiredEvidence(effectivePolicy, adopted.contract?.tests ?? [], reviewFix)
|
|
716
|
+
: null;
|
|
646
717
|
const attempt = {
|
|
647
718
|
attempt_id: `ATT-${taskId}-${Date.now()}-${++attemptSeq}`,
|
|
648
719
|
task_id: taskId, state: "active",
|
|
649
720
|
task_structure_digest: structureDigest,
|
|
650
721
|
contract_mode: contractMode,
|
|
651
722
|
contract: adopted.contract,
|
|
652
|
-
|
|
653
|
-
|
|
723
|
+
...(requiredEvidence ? { required_evidence: requiredEvidence } : {
|
|
724
|
+
// 历史模式以及 v1 契约轮继续使用 task 行标记回放;v2 只消费快照。
|
|
725
|
+
tdd_required: taskInfo.tddRequired,
|
|
726
|
+
no_tdd_reason: taskInfo.noTddReason,
|
|
727
|
+
}),
|
|
728
|
+
// REVIEW-FIX 没有计划阶段测试契约,但有效证据要求仍由本轮冻结策略编译。
|
|
729
|
+
execution_policy: effectivePolicy,
|
|
654
730
|
declared_write_scope: [], pre_edit_source_fingerprint: null,
|
|
655
731
|
pre_edit_red_ref: null, executor_packet_digest: null,
|
|
656
732
|
executor_result_ref: null, post_edit_green_ref: null,
|
|
@@ -667,7 +743,9 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
|
|
|
667
743
|
details: {
|
|
668
744
|
attempt_id: attempt.attempt_id,
|
|
669
745
|
task_id: taskId,
|
|
746
|
+
execution_policy: effectivePolicy,
|
|
670
747
|
contract: adopted.contract,
|
|
748
|
+
...(requiredEvidence ? { required_evidence: requiredEvidence } : {}),
|
|
671
749
|
// legacy 轮统一标注历史模式(无论有无执行依据文本);契约轮无块时标 false(如 REVIEW-FIX)
|
|
672
750
|
...(adopted.contract ? {} : { legacy_contract: !contractMode }),
|
|
673
751
|
},
|
|
@@ -676,6 +754,42 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
|
|
|
676
754
|
});
|
|
677
755
|
}
|
|
678
756
|
// ===== reopen =====
|
|
757
|
+
function abandonActiveTaskAttempts(snapshot, to, reason) {
|
|
758
|
+
return snapshot.active_task_attempts
|
|
759
|
+
.filter(attempt => attempt.state === "active")
|
|
760
|
+
.map(attempt => ({
|
|
761
|
+
type: "task_abandoned",
|
|
762
|
+
payload: {
|
|
763
|
+
attempt_id: attempt.attempt_id,
|
|
764
|
+
task_id: attempt.task_id,
|
|
765
|
+
reason: `reopen --to ${to}:${reason}`,
|
|
766
|
+
},
|
|
767
|
+
}));
|
|
768
|
+
}
|
|
769
|
+
function invalidateOpenJobs(snapshot, to, reason) {
|
|
770
|
+
return snapshot.open_jobs.map(job => ({
|
|
771
|
+
type: "job_invalidated",
|
|
772
|
+
payload: {
|
|
773
|
+
job_id: job.job_id,
|
|
774
|
+
role: job.role,
|
|
775
|
+
reason: `reopen --to ${to}:${reason}`,
|
|
776
|
+
},
|
|
777
|
+
}));
|
|
778
|
+
}
|
|
779
|
+
function planningReopenExtraEvents(snapshot, to, reason) {
|
|
780
|
+
return [
|
|
781
|
+
...invalidateOpenJobs(snapshot, to, reason),
|
|
782
|
+
...abandonActiveTaskAttempts(snapshot, to, reason),
|
|
783
|
+
];
|
|
784
|
+
}
|
|
785
|
+
function canReopenToExplore(from) {
|
|
786
|
+
return from === "propose" || from === "propose_ready" || from === "apply" ||
|
|
787
|
+
from === "apply_done" || from === "review" || from === "accepted";
|
|
788
|
+
}
|
|
789
|
+
function canReopenToPropose(from) {
|
|
790
|
+
return from === "propose_ready" || from === "apply" || from === "apply_done" ||
|
|
791
|
+
from === "review" || from === "accepted";
|
|
792
|
+
}
|
|
679
793
|
export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
680
794
|
return commitTransition(projectRoot, change, changeRoot, {
|
|
681
795
|
name: "reopen", idempotencyInputs: { to, reason, reviewFix: opts.reviewFix ?? "", reviewFinding: opts.reviewFinding ?? "" },
|
|
@@ -712,6 +826,8 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
712
826
|
finding_id: ref.findingId,
|
|
713
827
|
decision_scope: scope,
|
|
714
828
|
baseline_docs: proposalDocsBaseline(changeRoot),
|
|
829
|
+
planning_validation_version: 2,
|
|
830
|
+
planning_validation_profile: planningValidationProfileForNewRound(projectRoot),
|
|
715
831
|
},
|
|
716
832
|
};
|
|
717
833
|
}
|
|
@@ -751,22 +867,36 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
751
867
|
},
|
|
752
868
|
};
|
|
753
869
|
}
|
|
870
|
+
if (to === "explore") {
|
|
871
|
+
if (!canReopenToExplore(snapshot.state)) {
|
|
872
|
+
return { skip: true, message: `当前状态 ${snapshot.state},不能 reopen 到 explore` };
|
|
873
|
+
}
|
|
874
|
+
return {
|
|
875
|
+
fromState: snapshot.state,
|
|
876
|
+
toState: "explore",
|
|
877
|
+
outcome: "advanced",
|
|
878
|
+
reason: reason.trim(),
|
|
879
|
+
commitPayload: {
|
|
880
|
+
reopen_target: "explore",
|
|
881
|
+
reopen_source: snapshot.state,
|
|
882
|
+
baseline_docs: discoveryDocsBaseline(changeRoot),
|
|
883
|
+
},
|
|
884
|
+
extraEvents: planningReopenExtraEvents(snapshot, "explore", reason.trim()),
|
|
885
|
+
};
|
|
886
|
+
}
|
|
754
887
|
if (to === "propose") {
|
|
755
|
-
if (snapshot.state
|
|
756
|
-
return {
|
|
757
|
-
skip: true,
|
|
758
|
-
message: `当前状态 ${snapshot.state},主动 reopen --to propose 只允许从 accepted 发起;apply_done 的代码审查问题请使用 --review-finding`,
|
|
759
|
-
};
|
|
888
|
+
if (!canReopenToPropose(snapshot.state)) {
|
|
889
|
+
return { skip: true, message: `当前状态 ${snapshot.state},不能 reopen 到 propose` };
|
|
760
890
|
}
|
|
761
|
-
if (snapshot.open_jobs.length > 0) {
|
|
891
|
+
if (snapshot.state === "accepted" && snapshot.open_jobs.length > 0) {
|
|
762
892
|
return {
|
|
763
893
|
blocked: true,
|
|
764
|
-
reason: `状态未推进;accepted
|
|
894
|
+
reason: `状态未推进;accepted 状态仍有 ${snapshot.open_jobs.length} 个待完成工作项`,
|
|
765
895
|
jobs: snapshot.open_jobs,
|
|
766
896
|
};
|
|
767
897
|
}
|
|
768
|
-
const acceptedBaseline = latestAcceptedProposalBaseline(events);
|
|
769
898
|
const currentBaseline = proposalDocsBaseline(changeRoot);
|
|
899
|
+
const acceptedBaseline = snapshot.state === "accepted" ? latestAcceptedProposalBaseline(events) : null;
|
|
770
900
|
// 旧版 accepted 事件的基线可能缺少后来纳入 Propose gate 的材料。保留其已冻结
|
|
771
901
|
// 的摘要,并用 reopen 当刻的摘要补齐缺项,确保本轮之后对任一审查目标的修改都能被检测。
|
|
772
902
|
const baselineNeedsBackfill = acceptedBaseline !== null && Object.keys(currentBaseline)
|
|
@@ -778,20 +908,23 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
778
908
|
]))
|
|
779
909
|
: currentBaseline;
|
|
780
910
|
return {
|
|
781
|
-
fromState:
|
|
911
|
+
fromState: snapshot.state,
|
|
782
912
|
toState: "propose",
|
|
783
913
|
outcome: "advanced",
|
|
784
914
|
reason: reason.trim(),
|
|
785
915
|
commitPayload: {
|
|
786
916
|
reopen_target: "propose",
|
|
787
|
-
reopen_source:
|
|
917
|
+
reopen_source: snapshot.state,
|
|
788
918
|
baseline_source: acceptedBaseline ? (baselineNeedsBackfill ? "accepted_backfill" : "accepted") : "reopen_fallback",
|
|
789
919
|
baseline_docs: baselineDocs,
|
|
920
|
+
planning_validation_version: 2,
|
|
921
|
+
planning_validation_profile: planningValidationProfileForNewRound(projectRoot),
|
|
790
922
|
},
|
|
923
|
+
extraEvents: planningReopenExtraEvents(snapshot, "propose", reason.trim()),
|
|
791
924
|
};
|
|
792
925
|
}
|
|
793
926
|
if (to !== "apply")
|
|
794
|
-
return { skip: true, message: `reopen 当前只支持 --to
|
|
927
|
+
return { skip: true, message: `reopen 当前只支持 --to explore、--to propose 或 --to apply,不支持 ${to}` };
|
|
795
928
|
if (snapshot.state !== "apply_done" && snapshot.state !== "review") {
|
|
796
929
|
return { skip: true, message: `当前状态 ${snapshot.state},不能 reopen 到 apply` };
|
|
797
930
|
}
|
|
@@ -808,7 +941,7 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
808
941
|
});
|
|
809
942
|
}
|
|
810
943
|
// ===== review-ready =====
|
|
811
|
-
export function reviewReady(projectRoot, change, changeRoot, risk =
|
|
944
|
+
export function reviewReady(projectRoot, change, changeRoot, risk = workflowRiskForProject(projectRoot)) {
|
|
812
945
|
return commitTransition(projectRoot, change, changeRoot, {
|
|
813
946
|
name: "review-ready", idempotencyInputs: { phase: "review-ready", risk },
|
|
814
947
|
decide: (snapshot) => {
|
|
@@ -863,6 +996,7 @@ export function reviewReady(projectRoot, change, changeRoot, risk = "strict") {
|
|
|
863
996
|
events,
|
|
864
997
|
snapshot,
|
|
865
998
|
boundary: "apply_to_review",
|
|
999
|
+
risk: policy.review_risk,
|
|
866
1000
|
decision: codeReviewDecision,
|
|
867
1001
|
});
|
|
868
1002
|
}
|
|
@@ -897,7 +1031,7 @@ export function accept(projectRoot, change, changeRoot) {
|
|
|
897
1031
|
changeRoot,
|
|
898
1032
|
events,
|
|
899
1033
|
snapshot,
|
|
900
|
-
mode: { kind: "risk", risk:
|
|
1034
|
+
mode: { kind: "risk", risk: workflowRiskForProject(projectRoot) },
|
|
901
1035
|
});
|
|
902
1036
|
return transitionPlanToDecision(snapshot, changeRoot, change, plan, events);
|
|
903
1037
|
},
|
|
@@ -919,9 +1053,11 @@ export function taskComplete(projectRoot, change, changeRoot, taskId, inputConte
|
|
|
919
1053
|
const readiness = taskEvidenceReadiness(projectRoot, change, changeRoot, attempt);
|
|
920
1054
|
if (!readiness.ready)
|
|
921
1055
|
return { skip: true, message: `任务 ${taskId} 无法完成:${readiness.reason}` };
|
|
1056
|
+
const taskStartBoundary = boundarySnapshotForTaskAttempt(readEvents(projectRoot, change), attempt.attempt_id);
|
|
922
1057
|
const completedPayload = {
|
|
923
1058
|
task_id: taskId,
|
|
924
1059
|
attempt_id: attempt.attempt_id,
|
|
1060
|
+
execution_policy: attempt.execution_policy ?? "tdd",
|
|
925
1061
|
...boundarySnapshotPayload(projectRoot),
|
|
926
1062
|
checkbox_update: { status: "pending" },
|
|
927
1063
|
...(scopeInput.value ? { scope_note: scopeInput.value } : {}),
|
|
@@ -930,7 +1066,8 @@ export function taskComplete(projectRoot, change, changeRoot, taskId, inputConte
|
|
|
930
1066
|
fromState: "apply", toState: "apply", outcome: "advanced",
|
|
931
1067
|
reason: `任务 ${taskId} 完成`,
|
|
932
1068
|
extraEvents: [{ type: "task_completed", payload: completedPayload }],
|
|
933
|
-
postCommit: (
|
|
1069
|
+
postCommit: (pr, _ch, cr) => {
|
|
1070
|
+
completedPayload.java_staging = stageProductionJavaFilesSince(pr, taskStartBoundary);
|
|
934
1071
|
const lines = readFileSync(join(cr, "tasks.md"), "utf8").split("\n");
|
|
935
1072
|
const idx = findTaskLine(lines, taskId);
|
|
936
1073
|
if (idx < 0) {
|