@chenmk/superflow 0.5.5 → 0.5.7
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/INSTALL.en.md +1 -1
- package/INSTALL.md +3 -3
- package/README.md +13 -1
- package/README.zh-CN.md +12 -2
- package/assets/scripts/superflow-managed-work-check.mjs +286 -126
- package/assets/skills/superflow-pipeline/references/managed-work.md +5 -0
- package/assets/skills-en/superflow-pipeline/references/managed-work.md +7 -0
- package/dist/app/cli.js +12 -0
- package/dist/app/cli.js.map +1 -1
- package/dist/app/commands/cleanup.js +17 -0
- package/dist/app/commands/cleanup.js.map +1 -0
- package/dist/app/commands/pipeline.js +16 -5
- package/dist/app/commands/pipeline.js.map +1 -1
- package/dist/domains/config/cli-help.js +8 -0
- package/dist/domains/config/cli-help.js.map +1 -1
- package/dist/domains/managed-work/acceptance-contract.js +139 -0
- package/dist/domains/managed-work/acceptance-contract.js.map +1 -0
- package/dist/domains/managed-work/cleanup.js +247 -0
- package/dist/domains/managed-work/cleanup.js.map +1 -0
- package/dist/domains/managed-work/context-manifest.js +26 -0
- package/dist/domains/managed-work/context-manifest.js.map +1 -1
- package/dist/domains/managed-work/contract.js +36 -4
- package/dist/domains/managed-work/contract.js.map +1 -1
- package/dist/domains/managed-work/control.js +11 -5
- package/dist/domains/managed-work/control.js.map +1 -1
- package/dist/domains/managed-work/execution-contract.js +33 -4
- package/dist/domains/managed-work/execution-contract.js.map +1 -1
- package/dist/domains/managed-work/host-review.js +4 -0
- package/dist/domains/managed-work/host-review.js.map +1 -1
- package/dist/domains/managed-work/prompts.js +52 -0
- package/dist/domains/managed-work/prompts.js.map +1 -1
- package/dist/domains/managed-work/runner.js +12 -4
- package/dist/domains/managed-work/runner.js.map +1 -1
- package/dist/domains/managed-work/schemas.js +13 -7
- package/dist/domains/managed-work/schemas.js.map +1 -1
- package/dist/domains/managed-work/state.js +1 -0
- package/dist/domains/managed-work/state.js.map +1 -1
- package/dist/domains/managed-work/storage.js +68 -0
- package/dist/domains/managed-work/storage.js.map +1 -1
- package/dist/domains/managed-work/types.js.map +1 -1
- package/dist/mcp/server.js +20 -0
- package/dist/mcp/server.js.map +1 -1
- package/installer/bin/superflow-installer.cjs +1 -1
- package/package.json +1 -1
- package/scripts/preinstall.js +2 -2
- package/installer/package.json +0 -16
|
@@ -1,99 +1,110 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import crypto from
|
|
3
|
-
import fs from
|
|
4
|
-
import path from
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
5
|
|
|
6
|
-
const [projectArg =
|
|
7
|
-
const beforeReadyEvent = mode ===
|
|
6
|
+
const [projectArg = ".", taskId, mode] = process.argv.slice(2);
|
|
7
|
+
const beforeReadyEvent = mode === "--before-ready-event";
|
|
8
8
|
if (!taskId) {
|
|
9
|
-
console.error(
|
|
9
|
+
console.error(
|
|
10
|
+
"用法: superflow-managed-work-check.mjs <project-root> <task-id>",
|
|
11
|
+
);
|
|
10
12
|
process.exit(2);
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
const projectRoot = path.resolve(projectArg);
|
|
14
|
-
const taskDir = path.join(projectRoot,
|
|
15
|
-
const taskFile = path.join(taskDir,
|
|
16
|
+
const taskDir = path.join(projectRoot, ".superflow", "tasks", taskId);
|
|
17
|
+
const taskFile = path.join(taskDir, "task.json");
|
|
16
18
|
if (!fs.existsSync(taskFile)) fail(`缺少任务状态: ${taskFile}`);
|
|
17
|
-
const task = JSON.parse(fs.readFileSync(taskFile,
|
|
19
|
+
const task = JSON.parse(fs.readFileSync(taskFile, "utf8"));
|
|
18
20
|
verifyContract(task);
|
|
19
|
-
const runsDir = path.join(taskDir,
|
|
21
|
+
const runsDir = path.join(taskDir, "runs");
|
|
20
22
|
const runs = fs.existsSync(runsDir)
|
|
21
23
|
? fs.readdirSync(runsDir).filter((entry) => {
|
|
22
24
|
const candidate = path.join(runsDir, entry);
|
|
23
25
|
return (
|
|
24
|
-
entry.startsWith(
|
|
26
|
+
entry.startsWith("run-") &&
|
|
25
27
|
fs.statSync(candidate).isDirectory() &&
|
|
26
|
-
fs.existsSync(path.join(candidate,
|
|
28
|
+
fs.existsSync(path.join(candidate, "run-state.json"))
|
|
27
29
|
);
|
|
28
30
|
})
|
|
29
31
|
: [];
|
|
30
32
|
if (runs.length !== 1) fail(`首版要求一个活动 run,实际 ${runs.length}`);
|
|
31
33
|
|
|
32
34
|
const runDir = path.join(runsDir, runs[0]);
|
|
33
|
-
const stateFile = path.join(runDir,
|
|
34
|
-
const journalFile = path.join(runDir,
|
|
35
|
-
const reportFile = path.join(runDir,
|
|
35
|
+
const stateFile = path.join(runDir, "run-state.json");
|
|
36
|
+
const journalFile = path.join(runDir, "progress.jsonl");
|
|
37
|
+
const reportFile = path.join(runDir, "task-report.md");
|
|
36
38
|
for (const file of [stateFile, journalFile, reportFile]) {
|
|
37
39
|
if (!fs.existsSync(file)) fail(`缺少必备运行产物: ${file}`);
|
|
38
40
|
}
|
|
39
|
-
const state = JSON.parse(fs.readFileSync(stateFile,
|
|
41
|
+
const state = JSON.parse(fs.readFileSync(stateFile, "utf8"));
|
|
40
42
|
|
|
41
43
|
if (task.budgets?.unlimitedAgentInvocations !== true) {
|
|
42
|
-
assertMax(
|
|
44
|
+
assertMax("reviewRound", state.reviewRound, 10);
|
|
43
45
|
assertMax(
|
|
44
|
-
|
|
46
|
+
"executorInvocations",
|
|
45
47
|
state.executorInvocations,
|
|
46
48
|
20 + (state.executorInvocationCredits ?? 0),
|
|
47
49
|
);
|
|
48
50
|
assertMax(
|
|
49
|
-
|
|
51
|
+
"totalAgentInvocations",
|
|
50
52
|
state.totalAgentInvocations,
|
|
51
53
|
30 + (state.totalAgentInvocationCredits ?? 0),
|
|
52
54
|
);
|
|
53
|
-
assertMax(
|
|
55
|
+
assertMax("reviewInvocations", state.reviewInvocations, 10);
|
|
54
56
|
}
|
|
55
|
-
if (state.contractHash !== task.contractHash)
|
|
56
|
-
|
|
57
|
+
if (state.contractHash !== task.contractHash)
|
|
58
|
+
fail("任务合同哈希与运行状态不一致");
|
|
59
|
+
if (state.status !== task.status) fail("任务合同状态与运行状态不一致");
|
|
57
60
|
if (state.executorInvocations > 0 && !state.executorSession?.sessionId) {
|
|
58
|
-
fail(
|
|
61
|
+
fail("已有执行调用但缺少执行会话编号");
|
|
59
62
|
}
|
|
60
63
|
const events = verifyJournal(journalFile);
|
|
61
64
|
|
|
62
65
|
const deliveryStatuses = new Set([
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
"local_delivery_ready",
|
|
67
|
+
"environment_validation_blocked",
|
|
68
|
+
"release_ready",
|
|
66
69
|
]);
|
|
67
70
|
if (deliveryStatuses.has(state.status)) {
|
|
68
71
|
if (!state.lastExecutorResult || !fs.existsSync(state.lastExecutorResult)) {
|
|
69
|
-
fail(
|
|
72
|
+
fail("交付状态缺少最终执行结果");
|
|
70
73
|
}
|
|
71
74
|
if (!state.lastReviewResult || !fs.existsSync(state.lastReviewResult)) {
|
|
72
|
-
fail(
|
|
75
|
+
fail("交付状态缺少最终检查结果");
|
|
73
76
|
}
|
|
74
|
-
assertInsideRun(state.lastExecutorResult, runDir,
|
|
75
|
-
assertInsideRun(state.lastReviewResult, runDir,
|
|
76
|
-
const executor = JSON.parse(
|
|
77
|
-
|
|
77
|
+
assertInsideRun(state.lastExecutorResult, runDir, "最终执行结果");
|
|
78
|
+
assertInsideRun(state.lastReviewResult, runDir, "最终检查结果");
|
|
79
|
+
const executor = JSON.parse(
|
|
80
|
+
fs.readFileSync(state.lastExecutorResult, "utf8"),
|
|
81
|
+
);
|
|
82
|
+
const review = JSON.parse(fs.readFileSync(state.lastReviewResult, "utf8"));
|
|
83
|
+
verifyAcceptanceReview(task, runDir);
|
|
78
84
|
const completion = verifyOpenSpecTasksComplete(task, executor);
|
|
79
85
|
verifyDeliveryStatus(state.status, completion);
|
|
80
86
|
if (
|
|
81
|
-
task.source ===
|
|
87
|
+
task.source === "sdd" &&
|
|
82
88
|
!review.verificationCommands?.some(
|
|
83
89
|
(item) =>
|
|
84
90
|
item.exitCode === 0 &&
|
|
85
91
|
/openspec\s+instructions\s+apply/i.test(item.command),
|
|
86
92
|
)
|
|
87
93
|
) {
|
|
88
|
-
fail(
|
|
94
|
+
fail("SDD 最终评审缺少 openspec instructions apply 命令证据");
|
|
89
95
|
}
|
|
90
|
-
if (
|
|
91
|
-
|
|
96
|
+
if (
|
|
97
|
+
review.result !== "pass" ||
|
|
98
|
+
review.findings?.some((item) => item.blocking)
|
|
99
|
+
) {
|
|
100
|
+
fail("交付状态与最终检查结论不一致");
|
|
92
101
|
}
|
|
93
102
|
verifyExecutorEvidence(task, executor, review, runDir);
|
|
94
|
-
const readyEvents = events.filter(
|
|
103
|
+
const readyEvents = events.filter(
|
|
104
|
+
(event) => event.eventType === "run.delivery_ready",
|
|
105
|
+
);
|
|
95
106
|
const reopenedEvents = events.filter(
|
|
96
|
-
(event) => event.eventType ===
|
|
107
|
+
(event) => event.eventType === "run.delivery_reopened",
|
|
97
108
|
);
|
|
98
109
|
const expectedReadyEvents = beforeReadyEvent ? 0 : 1;
|
|
99
110
|
if (readyEvents.length !== reopenedEvents.length + expectedReadyEvents) {
|
|
@@ -118,54 +129,83 @@ function verifyContract(contract) {
|
|
|
118
129
|
(!Number.isInteger(budgets.executorPhysicalStopAt) ||
|
|
119
130
|
budgets.executorPhysicalStopAt <= 0)
|
|
120
131
|
) {
|
|
121
|
-
fail(
|
|
132
|
+
fail("executorPhysicalStopAt 必须是正整数");
|
|
122
133
|
}
|
|
123
|
-
assertPositiveMax(
|
|
124
|
-
assertPositiveMax(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
134
|
+
assertPositiveMax("maxReviewRounds", budgets.maxReviewRounds, 10, true);
|
|
135
|
+
assertPositiveMax(
|
|
136
|
+
"maxExecutorInvocations",
|
|
137
|
+
budgets.maxExecutorInvocations,
|
|
138
|
+
20,
|
|
139
|
+
true,
|
|
140
|
+
);
|
|
141
|
+
assertPositiveMax(
|
|
142
|
+
"maxTotalAgentInvocations",
|
|
143
|
+
budgets.maxTotalAgentInvocations,
|
|
144
|
+
30,
|
|
145
|
+
true,
|
|
146
|
+
);
|
|
147
|
+
assertPositiveMax("maxActiveRunHours", budgets.maxActiveRunHours, 24, false);
|
|
148
|
+
assertPositiveMax(
|
|
149
|
+
"maxSingleInvocationHours",
|
|
150
|
+
budgets.maxSingleInvocationHours,
|
|
151
|
+
3,
|
|
152
|
+
false,
|
|
153
|
+
);
|
|
128
154
|
if (contract.supervisorAgent === contract.executorAgent) {
|
|
129
|
-
fail(
|
|
155
|
+
fail("监督 Agent 和执行 Agent 不能相同");
|
|
130
156
|
}
|
|
131
|
-
if (contract.supervisorExecution !==
|
|
132
|
-
fail(
|
|
157
|
+
if (contract.supervisorExecution !== "external_host") {
|
|
158
|
+
fail("监督执行模式只允许 external_host");
|
|
133
159
|
}
|
|
134
160
|
const permissions = contract.permissions ?? {};
|
|
135
|
-
for (const key of [
|
|
161
|
+
for (const key of [
|
|
162
|
+
"gitCommit",
|
|
163
|
+
"gitPush",
|
|
164
|
+
"productionWrites",
|
|
165
|
+
"bypassSandbox",
|
|
166
|
+
]) {
|
|
136
167
|
if (permissions[key] !== false) fail(`不可覆盖的权限边界被放宽: ${key}`);
|
|
137
168
|
}
|
|
138
169
|
const disclosure = permissions.externalModelDataDisclosure;
|
|
139
|
-
if (
|
|
140
|
-
|
|
170
|
+
if (
|
|
171
|
+
!disclosure?.approved ||
|
|
172
|
+
!disclosure.approvedBy ||
|
|
173
|
+
!disclosure.approvedAt
|
|
174
|
+
) {
|
|
175
|
+
fail("缺少任务级外部研发 Agent 源码披露授权");
|
|
141
176
|
}
|
|
142
177
|
if (!Array.isArray(disclosure.scope) || disclosure.scope.length === 0) {
|
|
143
|
-
fail(
|
|
178
|
+
fail("外部研发 Agent 源码披露授权缺少仓库作用域");
|
|
144
179
|
}
|
|
145
180
|
if (contract.taskPrompt) {
|
|
146
181
|
const expectedSnapshot = path.join(
|
|
147
182
|
path.resolve(contract.projectRoot),
|
|
148
|
-
|
|
149
|
-
|
|
183
|
+
".superflow",
|
|
184
|
+
"tasks",
|
|
150
185
|
contract.taskId,
|
|
151
|
-
|
|
186
|
+
"source-prompt.md",
|
|
152
187
|
);
|
|
153
188
|
if (path.resolve(contract.taskPrompt.snapshotPath) !== expectedSnapshot) {
|
|
154
|
-
fail(
|
|
189
|
+
fail("冻结 Prompt 快照路径不属于当前托管任务");
|
|
155
190
|
}
|
|
156
|
-
if (!fs.existsSync(expectedSnapshot)) fail(
|
|
157
|
-
const promptHash = crypto
|
|
158
|
-
.
|
|
159
|
-
.
|
|
160
|
-
|
|
191
|
+
if (!fs.existsSync(expectedSnapshot)) fail("冻结 Prompt 快照不存在");
|
|
192
|
+
const promptHash = crypto
|
|
193
|
+
.createHash("sha256")
|
|
194
|
+
.update(fs.readFileSync(expectedSnapshot, "utf8"))
|
|
195
|
+
.digest("hex");
|
|
196
|
+
if (promptHash !== contract.taskPrompt.sha256)
|
|
197
|
+
fail("冻结 Prompt 快照哈希不一致");
|
|
161
198
|
}
|
|
199
|
+
verifyAcceptanceContractSnapshot(contract);
|
|
162
200
|
const immutable = {
|
|
163
201
|
schemaVersion: contract.schemaVersion,
|
|
164
202
|
taskId: contract.taskId,
|
|
165
203
|
request: contract.request,
|
|
166
204
|
source: contract.source,
|
|
167
205
|
projectRoot: path.resolve(contract.projectRoot),
|
|
168
|
-
relatedProjectRoots: contract.relatedProjectRoots.map((root) =>
|
|
206
|
+
relatedProjectRoots: contract.relatedProjectRoots.map((root) =>
|
|
207
|
+
path.resolve(root),
|
|
208
|
+
),
|
|
169
209
|
profile: contract.profile,
|
|
170
210
|
...(contract.language ? { language: contract.language } : {}),
|
|
171
211
|
objective: contract.objective,
|
|
@@ -179,44 +219,133 @@ function verifyContract(contract) {
|
|
|
179
219
|
...(contract.supervisorExecution
|
|
180
220
|
? { supervisorExecution: contract.supervisorExecution }
|
|
181
221
|
: {}),
|
|
182
|
-
...(contract.executionMode
|
|
222
|
+
...(contract.executionMode
|
|
223
|
+
? { executionMode: contract.executionMode }
|
|
224
|
+
: {}),
|
|
225
|
+
...(contract.retention ? { retention: contract.retention } : {}),
|
|
226
|
+
...(contract.taskKind ? { taskKind: contract.taskKind } : {}),
|
|
227
|
+
...(contract.acceptanceContract
|
|
228
|
+
? { acceptanceContract: contract.acceptanceContract }
|
|
229
|
+
: {}),
|
|
183
230
|
permissions: contract.permissions,
|
|
184
231
|
budgets: contract.budgets,
|
|
185
232
|
};
|
|
186
|
-
const actual = crypto
|
|
187
|
-
|
|
233
|
+
const actual = crypto
|
|
234
|
+
.createHash("sha256")
|
|
235
|
+
.update(JSON.stringify(immutable))
|
|
236
|
+
.digest("hex");
|
|
237
|
+
if (actual !== contract.contractHash) fail("任务合同内容与冻结哈希不一致");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function verifyAcceptanceContractSnapshot(contract) {
|
|
241
|
+
if (!contract.acceptanceContract) return;
|
|
242
|
+
const taskDir = path.join(
|
|
243
|
+
path.resolve(contract.projectRoot),
|
|
244
|
+
".superflow",
|
|
245
|
+
"tasks",
|
|
246
|
+
contract.taskId,
|
|
247
|
+
);
|
|
248
|
+
const snapshot = path.join(taskDir, "acceptance-contract.json");
|
|
249
|
+
const projection = path.join(taskDir, "acceptance-contract.md");
|
|
250
|
+
if (!fs.existsSync(snapshot) || !fs.existsSync(projection)) {
|
|
251
|
+
fail("冻结验收合同快照或投影缺失");
|
|
252
|
+
}
|
|
253
|
+
const value = JSON.parse(fs.readFileSync(snapshot, "utf8"));
|
|
254
|
+
const acceptanceHash = crypto
|
|
255
|
+
.createHash("sha256")
|
|
256
|
+
.update(JSON.stringify(contract.acceptanceContract))
|
|
257
|
+
.digest("hex");
|
|
258
|
+
if (
|
|
259
|
+
value.schemaVersion !== 1 ||
|
|
260
|
+
value.taskId !== contract.taskId ||
|
|
261
|
+
value.contractHash !== contract.contractHash ||
|
|
262
|
+
value.acceptanceContractHash !== acceptanceHash ||
|
|
263
|
+
JSON.stringify(value.acceptanceContract) !==
|
|
264
|
+
JSON.stringify(contract.acceptanceContract)
|
|
265
|
+
) {
|
|
266
|
+
fail("冻结验收合同快照与任务合同不一致");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function verifyAcceptanceReview(task, runDir) {
|
|
271
|
+
if (!task.acceptanceContract) return;
|
|
272
|
+
const firstReviewFile = path.join(runDir, "review-result-1.json");
|
|
273
|
+
if (!fs.existsSync(firstReviewFile)) {
|
|
274
|
+
fail("冻结验收合同缺少首轮 Host 评审结果");
|
|
275
|
+
}
|
|
276
|
+
const review = JSON.parse(fs.readFileSync(firstReviewFile, "utf8"));
|
|
277
|
+
const expected = acceptanceReferences(task.acceptanceContract);
|
|
278
|
+
const reviewed = review.acceptanceCoverage?.reviewed;
|
|
279
|
+
if (!Array.isArray(reviewed) || new Set(reviewed).size !== reviewed.length) {
|
|
280
|
+
fail("最终 Host 评审缺少唯一验收合同覆盖记录");
|
|
281
|
+
}
|
|
282
|
+
const actual = new Set(reviewed);
|
|
283
|
+
if (
|
|
284
|
+
expected.length !== actual.size ||
|
|
285
|
+
expected.some((reference) => !actual.has(reference))
|
|
286
|
+
) {
|
|
287
|
+
fail("最终 Host 评审未覆盖全部冻结验收合同条目");
|
|
288
|
+
}
|
|
289
|
+
const known = new Set(expected);
|
|
290
|
+
for (const finding of review.findings ?? []) {
|
|
291
|
+
if (
|
|
292
|
+
!Array.isArray(finding.acceptanceContractRefs) ||
|
|
293
|
+
finding.acceptanceContractRefs.length === 0 ||
|
|
294
|
+
finding.acceptanceContractRefs.some((reference) => !known.has(reference))
|
|
295
|
+
) {
|
|
296
|
+
fail(`finding ${finding.id ?? "unknown"} 未映射有效验收合同条目`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function acceptanceReferences(contract) {
|
|
302
|
+
return [
|
|
303
|
+
...contract.businessInvariants.map((_, index) => `invariant:${index + 1}`),
|
|
304
|
+
...contract.sourceCoverage.map((_, index) => `source:${index + 1}`),
|
|
305
|
+
...contract.deliverables.map((_, index) => `deliverable:${index + 1}`),
|
|
306
|
+
...contract.verification.map((_, index) => `verification:${index + 1}`),
|
|
307
|
+
...contract.exclusions.map((_, index) => `exclusion:${index + 1}`),
|
|
308
|
+
];
|
|
188
309
|
}
|
|
189
310
|
|
|
190
311
|
function assertPositiveMax(name, value, max, integer) {
|
|
191
|
-
if (
|
|
312
|
+
if (
|
|
313
|
+
!Number.isFinite(value) ||
|
|
314
|
+
value <= 0 ||
|
|
315
|
+
value > max ||
|
|
316
|
+
(integer && !Number.isInteger(value))
|
|
317
|
+
) {
|
|
192
318
|
fail(`${name}=${value} 超出硬上限或格式非法`);
|
|
193
319
|
}
|
|
194
320
|
}
|
|
195
321
|
|
|
196
322
|
function verifyOpenSpecTasksComplete(task, executor) {
|
|
197
|
-
if (task.source !==
|
|
323
|
+
if (task.source !== "sdd" || !task.taskPrompt?.originalPath) {
|
|
198
324
|
return { localPending: [], environmentPending: [], releasePending: [] };
|
|
199
325
|
}
|
|
200
326
|
let current = path.dirname(path.resolve(task.taskPrompt.originalPath));
|
|
201
327
|
const projectRoot = path.resolve(task.projectRoot);
|
|
202
328
|
while (current.startsWith(projectRoot)) {
|
|
203
|
-
const tasksFile = path.join(current,
|
|
329
|
+
const tasksFile = path.join(current, "tasks.md");
|
|
204
330
|
if (fs.existsSync(tasksFile)) {
|
|
205
|
-
const tasks = fs
|
|
331
|
+
const tasks = fs
|
|
332
|
+
.readFileSync(tasksFile, "utf8")
|
|
206
333
|
.split(/\r?\n/)
|
|
207
334
|
.filter((line) => /^\s*- \[[ xX]\]/.test(line))
|
|
208
335
|
.map((line, index) => parseCompletionTask(line, index));
|
|
209
336
|
const localPending = tasks.filter(
|
|
210
|
-
(item) => !item.completed && item.category ===
|
|
337
|
+
(item) => !item.completed && item.category === "local_required",
|
|
211
338
|
);
|
|
212
339
|
const environmentPending = tasks.filter(
|
|
213
|
-
(item) => !item.completed && item.category ===
|
|
340
|
+
(item) => !item.completed && item.category === "environment_required",
|
|
214
341
|
);
|
|
215
342
|
const releasePending = tasks.filter(
|
|
216
|
-
(item) => !item.completed && item.category ===
|
|
343
|
+
(item) => !item.completed && item.category === "release_required",
|
|
217
344
|
);
|
|
218
345
|
if (localPending.length > 0) {
|
|
219
|
-
fail(
|
|
346
|
+
fail(
|
|
347
|
+
`OpenSpec tasks.md 仍有 ${localPending.length} 个本地可执行任务未完成`,
|
|
348
|
+
);
|
|
220
349
|
}
|
|
221
350
|
const evidence = new Map(
|
|
222
351
|
(executor.taskEvidence ?? []).map((item) => [item.taskId, item]),
|
|
@@ -227,11 +356,11 @@ function verifyOpenSpecTasksComplete(task, executor) {
|
|
|
227
356
|
const derivedEvidencePaths = [
|
|
228
357
|
...(executor.evidence ?? []),
|
|
229
358
|
tasksFile,
|
|
230
|
-
path.join(current,
|
|
359
|
+
path.join(current, "test-report.md"),
|
|
231
360
|
].filter((item) => taskEvidencePathExists(task, item));
|
|
232
361
|
for (const item of tasks.filter(
|
|
233
362
|
(taskItem) =>
|
|
234
|
-
taskItem.completed && taskItem.category ===
|
|
363
|
+
taskItem.completed && taskItem.category === "local_required",
|
|
235
364
|
)) {
|
|
236
365
|
const proof = evidence.get(item.taskId);
|
|
237
366
|
if (successfulCommands.length > 0 && derivedEvidencePaths.length > 0) {
|
|
@@ -241,10 +370,12 @@ function verifyOpenSpecTasksComplete(task, executor) {
|
|
|
241
370
|
fail(`已勾选本地任务 ${item.taskId} 缺少可推导的真实交付证据`);
|
|
242
371
|
}
|
|
243
372
|
if (
|
|
244
|
-
proof.owner !==
|
|
373
|
+
proof.owner !== "executor" ||
|
|
245
374
|
!proof.evidencePaths?.length ||
|
|
246
375
|
!proof.verificationCommands?.length ||
|
|
247
|
-
!proof.evidencePaths.every((item) =>
|
|
376
|
+
!proof.evidencePaths.every((item) =>
|
|
377
|
+
taskEvidencePathExists(task, item),
|
|
378
|
+
) ||
|
|
248
379
|
!proof.verificationCommands.every((command) =>
|
|
249
380
|
executor.commands?.some(
|
|
250
381
|
(item) =>
|
|
@@ -252,7 +383,9 @@ function verifyOpenSpecTasksComplete(task, executor) {
|
|
|
252
383
|
verificationCommandCovered(command, item.command),
|
|
253
384
|
),
|
|
254
385
|
) ||
|
|
255
|
-
!proof.changedFiles.every((file) =>
|
|
386
|
+
!proof.changedFiles.every((file) =>
|
|
387
|
+
executor.changedFiles?.includes(file),
|
|
388
|
+
)
|
|
256
389
|
) {
|
|
257
390
|
fail(`已勾选本地任务 ${item.taskId} 缺少结构化交付证据`);
|
|
258
391
|
}
|
|
@@ -272,7 +405,7 @@ function verifyOpenSpecTasksComplete(task, executor) {
|
|
|
272
405
|
if (parent === current) break;
|
|
273
406
|
current = parent;
|
|
274
407
|
}
|
|
275
|
-
fail(
|
|
408
|
+
fail("SDD 托管任务未找到 OpenSpec tasks.md");
|
|
276
409
|
}
|
|
277
410
|
|
|
278
411
|
function isAcceptedEvidenceCommand(command) {
|
|
@@ -302,7 +435,7 @@ function verificationCommandCovered(expected, actual) {
|
|
|
302
435
|
function mavenTestSelectors(command) {
|
|
303
436
|
if (!/(?:^|\s)(?:\.\/)?mvn(?:\s|$)/.test(command)) return [];
|
|
304
437
|
const match = command.match(/-Dtest=([^\s]+)/);
|
|
305
|
-
return match?.[1].split(
|
|
438
|
+
return match?.[1].split(",").filter(Boolean) ?? [];
|
|
306
439
|
}
|
|
307
440
|
|
|
308
441
|
function taskEvidencePathExists(task, evidencePath) {
|
|
@@ -325,7 +458,7 @@ function findTaskDirectory(task) {
|
|
|
325
458
|
(root) => path.resolve(root),
|
|
326
459
|
);
|
|
327
460
|
while (roots.some((root) => current.startsWith(root))) {
|
|
328
|
-
if (fs.existsSync(path.join(current,
|
|
461
|
+
if (fs.existsSync(path.join(current, "tasks.md"))) return current;
|
|
329
462
|
const parent = path.dirname(current);
|
|
330
463
|
if (parent === current) break;
|
|
331
464
|
current = parent;
|
|
@@ -335,61 +468,70 @@ function findTaskDirectory(task) {
|
|
|
335
468
|
|
|
336
469
|
function parseCompletionTask(line, index) {
|
|
337
470
|
const completed = /^\s*- \[[xX]\]/.test(line);
|
|
338
|
-
const text = line.replace(/^\s*- \[[ xX]\]\s*/,
|
|
471
|
+
const text = line.replace(/^\s*- \[[ xX]\]\s*/, "").trim();
|
|
339
472
|
const match = text.match(/^([A-Za-z]*\d+(?:\.\d+)*|[A-Z][A-Z0-9_-]+)\b/);
|
|
340
473
|
const category = /\[environment_required\]/i.test(text)
|
|
341
|
-
?
|
|
474
|
+
? "environment_required"
|
|
342
475
|
: /\[release_required\]/i.test(text)
|
|
343
|
-
?
|
|
344
|
-
:
|
|
476
|
+
? "release_required"
|
|
477
|
+
: "local_required";
|
|
345
478
|
return { taskId: match?.[1] ?? `task-${index + 1}`, completed, category };
|
|
346
479
|
}
|
|
347
480
|
|
|
348
481
|
function verifyDeliveryStatus(status, completion) {
|
|
349
|
-
if (
|
|
350
|
-
|
|
482
|
+
if (
|
|
483
|
+
status === "environment_validation_blocked" &&
|
|
484
|
+
completion.environmentPending.length === 0
|
|
485
|
+
) {
|
|
486
|
+
fail("environment_validation_blocked 状态没有待完成环境验收");
|
|
351
487
|
}
|
|
352
|
-
if (
|
|
353
|
-
|
|
488
|
+
if (
|
|
489
|
+
status === "local_delivery_ready" &&
|
|
490
|
+
completion.environmentPending.length > 0
|
|
491
|
+
) {
|
|
492
|
+
fail("local_delivery_ready 状态仍有环境验收未完成");
|
|
354
493
|
}
|
|
355
494
|
if (
|
|
356
|
-
status ===
|
|
495
|
+
status === "release_ready" &&
|
|
357
496
|
completion.environmentPending.length + completion.releasePending.length > 0
|
|
358
497
|
) {
|
|
359
|
-
fail(
|
|
498
|
+
fail("release_ready 状态仍有环境或发布任务未完成");
|
|
360
499
|
}
|
|
361
500
|
}
|
|
362
501
|
|
|
363
502
|
function verifyExecutorEvidence(task, result, review, runDir) {
|
|
364
|
-
if (![
|
|
365
|
-
fail(
|
|
503
|
+
if (!["ready_for_review", "blocked"].includes(result.status)) {
|
|
504
|
+
fail("最终执行结果既不可检查也不是诚实阻塞状态");
|
|
366
505
|
}
|
|
367
|
-
if (!Array.isArray(result.commands)) fail(
|
|
368
|
-
if (result.status ===
|
|
369
|
-
fail(
|
|
506
|
+
if (!Array.isArray(result.commands)) fail("最终执行结果缺少命令证据");
|
|
507
|
+
if (result.status === "blocked" && review.result !== "pass") {
|
|
508
|
+
fail("执行者阻塞结果未经监督者判定为非阻断发布前置");
|
|
370
509
|
}
|
|
371
|
-
if (result.status ===
|
|
372
|
-
fail(
|
|
510
|
+
if (result.status === "ready_for_review" && result.blockers?.length > 0) {
|
|
511
|
+
fail("可检查执行结果仍含阻塞项");
|
|
373
512
|
}
|
|
374
|
-
if (task.profile ===
|
|
375
|
-
const successful = loadExecutorCommands(runDir)
|
|
376
|
-
|
|
513
|
+
if (task.profile === "engineering" || task.profile === "sdd") {
|
|
514
|
+
const successful = loadExecutorCommands(runDir).filter(
|
|
515
|
+
(item) => item.exitCode === 0,
|
|
516
|
+
);
|
|
377
517
|
const categories = new Set(
|
|
378
518
|
successful.flatMap((item) => verificationCategories(item.command)),
|
|
379
519
|
);
|
|
380
520
|
const required = requiredVerificationCategories(projectRoot);
|
|
381
|
-
if (categories.size < required) fail(
|
|
521
|
+
if (categories.size < required) fail("工程任务缺少至少两类成功验证证据");
|
|
382
522
|
}
|
|
383
523
|
}
|
|
384
524
|
|
|
385
525
|
function requiredVerificationCategories(root) {
|
|
386
|
-
const packageFile = path.join(root,
|
|
526
|
+
const packageFile = path.join(root, "package.json");
|
|
387
527
|
if (!fs.existsSync(packageFile)) return 2;
|
|
388
528
|
try {
|
|
389
|
-
const scripts =
|
|
390
|
-
|
|
391
|
-
const
|
|
392
|
-
|
|
529
|
+
const scripts =
|
|
530
|
+
JSON.parse(fs.readFileSync(packageFile, "utf8")).scripts ?? {};
|
|
531
|
+
const hasTest = typeof scripts.test === "string";
|
|
532
|
+
const hasRunnableTarget = ["build", "start", "dev", "serve"].some(
|
|
533
|
+
(name) => typeof scripts[name] === "string",
|
|
534
|
+
);
|
|
393
535
|
return hasTest && !hasRunnableTarget ? 1 : 2;
|
|
394
536
|
} catch {
|
|
395
537
|
return 2;
|
|
@@ -397,51 +539,69 @@ function requiredVerificationCategories(root) {
|
|
|
397
539
|
}
|
|
398
540
|
|
|
399
541
|
function loadExecutorCommands(runDir) {
|
|
400
|
-
return fs
|
|
542
|
+
return fs
|
|
543
|
+
.readdirSync(runDir)
|
|
401
544
|
.filter((entry) => /^executor-result-\d+\.json$/.test(entry))
|
|
402
545
|
.flatMap((entry) => {
|
|
403
546
|
const result = JSON.parse(
|
|
404
|
-
fs.readFileSync(path.join(runDir, entry),
|
|
547
|
+
fs.readFileSync(path.join(runDir, entry), "utf8"),
|
|
405
548
|
);
|
|
406
549
|
return Array.isArray(result.commands) ? result.commands : [];
|
|
407
550
|
});
|
|
408
551
|
}
|
|
409
552
|
|
|
410
|
-
function verificationCategories(command =
|
|
553
|
+
function verificationCategories(command = "") {
|
|
411
554
|
const value = command.toLowerCase();
|
|
412
555
|
const categories = [];
|
|
413
|
-
if (
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
556
|
+
if (
|
|
557
|
+
/(^|\s)(test|vitest|jest|pytest|phpunit|go\s+test|cargo\s+test)(\s|$)/.test(
|
|
558
|
+
value,
|
|
559
|
+
) ||
|
|
560
|
+
/(?:npm|pnpm|yarn)\s+(?:run\s+)?test/.test(value) ||
|
|
561
|
+
/mvn(?:\s+[^\s]+)*\s+test/.test(value) ||
|
|
562
|
+
/gradle\w*\s+test/.test(value)
|
|
563
|
+
)
|
|
564
|
+
categories.push("test");
|
|
565
|
+
if (
|
|
566
|
+
/(?:npm|pnpm|yarn)\s+(?:run\s+)?build/.test(value) ||
|
|
567
|
+
/(?:^|[\s/])(?:vite|vue-cli-service)\s+build(?:\s|$)/.test(value) ||
|
|
568
|
+
/(^|\s)(tsc|make|cmake|compile)(\s|$)/.test(value) ||
|
|
569
|
+
/mvn(?:\s+[^\s]+)*\s+(?:compile|package|verify)/.test(value) ||
|
|
570
|
+
/gradle\w*\s+(?:build|assemble)/.test(value) ||
|
|
571
|
+
/cargo\s+build|go\s+build/.test(value)
|
|
572
|
+
)
|
|
573
|
+
categories.push("build");
|
|
574
|
+
if (
|
|
575
|
+
/(?:npm|pnpm|yarn)\s+(?:run\s+)?(?:start|dev|serve)/.test(value) ||
|
|
576
|
+
/spring-boot:run|docker\s+compose\s+up|(^|\s)(curl|wget)(\s|$)/.test(
|
|
577
|
+
value,
|
|
578
|
+
) ||
|
|
579
|
+
/health(?:check)?|integration|e2e/.test(value)
|
|
580
|
+
)
|
|
581
|
+
categories.push("runtime");
|
|
426
582
|
return categories;
|
|
427
583
|
}
|
|
428
584
|
|
|
429
585
|
function assertInsideRun(file, runDir, label) {
|
|
430
586
|
const relative = path.relative(path.resolve(runDir), path.resolve(file));
|
|
431
|
-
if (relative.startsWith(
|
|
587
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
432
588
|
fail(`${label}不在当前运行目录内`);
|
|
433
589
|
}
|
|
434
590
|
}
|
|
435
591
|
|
|
436
592
|
function verifyJournal(file) {
|
|
437
|
-
const lines = fs.readFileSync(file,
|
|
593
|
+
const lines = fs.readFileSync(file, "utf8").split(/\r?\n/).filter(Boolean);
|
|
438
594
|
let previous = null;
|
|
439
595
|
const events = lines.map((line, index) => {
|
|
440
596
|
const event = JSON.parse(line);
|
|
441
597
|
const { eventHash, ...base } = event;
|
|
442
|
-
const expected = crypto
|
|
598
|
+
const expected = crypto
|
|
599
|
+
.createHash("sha256")
|
|
600
|
+
.update(JSON.stringify(base))
|
|
601
|
+
.digest("hex");
|
|
443
602
|
if (event.sequence !== index + 1) fail(`事件序号断裂: ${event.sequence}`);
|
|
444
|
-
if (event.previousEventHash !== previous)
|
|
603
|
+
if (event.previousEventHash !== previous)
|
|
604
|
+
fail(`事件前序哈希不一致: ${event.sequence}`);
|
|
445
605
|
if (eventHash !== expected) fail(`事件哈希不一致: ${event.sequence}`);
|
|
446
606
|
previous = eventHash;
|
|
447
607
|
return event;
|