@chenmk/superflow 0.3.0 → 0.4.0
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 +20 -1
- package/README.zh-CN.md +16 -1
- package/assets/manifest.json +5 -4
- package/assets/prompts/superflow-design.md +3 -0
- package/assets/prompts/superflow-docs.md +4 -1
- package/assets/scripts/superflow-dependency-update-hook.sh +1 -1
- package/assets/scripts/superflow-managed-work-check.mjs +194 -0
- package/assets/scripts/superflow-managed-work-guard.sh +103 -0
- package/assets/skills/openspec-propose/SKILL.md +11 -0
- package/assets/skills/superflow-design/SKILL.md +18 -9
- package/assets/skills/superflow-docs/SKILL.md +16 -0
- package/assets/skills/superflow-pipeline/SKILL.md +46 -0
- package/assets/skills/superflow-pipeline/references/document-templates.md +8 -0
- package/assets/skills/superflow-pipeline/references/managed-work.md +58 -0
- package/assets/skills/superflow-pipeline/references/quality-gate.md +1 -0
- package/assets/skills/superflow-pipeline/references/superpower-technical-design-template.md +10 -0
- package/assets/skills/superflow-pipeline/scripts/superflow-archive.sh +1 -1
- package/assets/skills/superflow-pipeline/scripts/superflow-guard.sh +79 -9
- package/assets/skills-en/openspec-propose/SKILL.md +14 -0
- package/assets/skills-en/superflow-design/SKILL.md +18 -8
- package/assets/skills-en/superflow-docs/SKILL.md +16 -0
- package/assets/skills-en/superflow-pipeline/SKILL.md +48 -0
- package/assets/skills-en/superflow-pipeline/references/document-templates.md +5 -0
- package/assets/skills-en/superflow-pipeline/references/managed-work.md +34 -0
- package/assets/skills-en/superflow-pipeline/references/quality-gate.md +8 -0
- package/assets/skills-en/superflow-pipeline/references/superpower-technical-design-template.md +11 -0
- package/assets/skills-en/superflow-pipeline/scripts/superflow-archive.sh +1 -1
- package/assets/skills-en/superflow-pipeline/scripts/superflow-guard.sh +78 -9
- package/dist/app/cli.js +120 -102
- package/dist/app/cli.js.map +1 -1
- package/dist/app/commands/init.js +6 -2
- package/dist/app/commands/init.js.map +1 -1
- package/dist/app/commands/managed-service.js +5 -0
- package/dist/app/commands/managed-service.js.map +1 -0
- package/dist/app/commands/pipeline.js +123 -3
- package/dist/app/commands/pipeline.js.map +1 -1
- package/dist/app/commands/status.js +199 -77
- package/dist/app/commands/status.js.map +1 -1
- package/dist/app/commands/uninstall.js +5 -2
- package/dist/app/commands/uninstall.js.map +1 -1
- package/dist/app/commands/update.js +31 -6
- package/dist/app/commands/update.js.map +1 -1
- package/dist/domains/config/cli-help.js +103 -73
- package/dist/domains/config/cli-help.js.map +1 -1
- package/dist/domains/deps.js +2 -1
- package/dist/domains/deps.js.map +1 -1
- package/dist/domains/hook.js +71 -38
- package/dist/domains/hook.js.map +1 -1
- package/dist/domains/managed-work/contract.js +218 -0
- package/dist/domains/managed-work/contract.js.map +1 -0
- package/dist/domains/managed-work/i18n.js +12 -0
- package/dist/domains/managed-work/i18n.js.map +1 -0
- package/dist/domains/managed-work/input.js +111 -0
- package/dist/domains/managed-work/input.js.map +1 -0
- package/dist/domains/managed-work/journal.js +122 -0
- package/dist/domains/managed-work/journal.js.map +1 -0
- package/dist/domains/managed-work/lock.js +40 -0
- package/dist/domains/managed-work/lock.js.map +1 -0
- package/dist/domains/managed-work/notifications.js +53 -0
- package/dist/domains/managed-work/notifications.js.map +1 -0
- package/dist/domains/managed-work/paths.js +18 -0
- package/dist/domains/managed-work/paths.js.map +1 -0
- package/dist/domains/managed-work/prompts.js +218 -0
- package/dist/domains/managed-work/prompts.js.map +1 -0
- package/dist/domains/managed-work/runner.js +626 -0
- package/dist/domains/managed-work/runner.js.map +1 -0
- package/dist/domains/managed-work/schemas.js +85 -0
- package/dist/domains/managed-work/schemas.js.map +1 -0
- package/dist/domains/managed-work/service.js +121 -0
- package/dist/domains/managed-work/service.js.map +1 -0
- package/dist/domains/managed-work/state.js +90 -0
- package/dist/domains/managed-work/state.js.map +1 -0
- package/dist/domains/managed-work/storage.js +181 -0
- package/dist/domains/managed-work/storage.js.map +1 -0
- package/dist/domains/managed-work/types.js +16 -0
- package/dist/domains/managed-work/types.js.map +1 -0
- package/dist/domains/managed-work/wait.js +40 -0
- package/dist/domains/managed-work/wait.js.map +1 -0
- package/dist/platform/agent-process.js +308 -0
- package/dist/platform/agent-process.js.map +1 -0
- package/dist/platform/process-liveness.js +10 -0
- package/dist/platform/process-liveness.js.map +1 -0
- package/dist/platform/version-check.js +30 -9
- package/dist/platform/version-check.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { execFileSync } from "child_process";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { appendManagedEvent, readManagedEvents, verifyManagedJournal, } from "./journal.js";
|
|
6
|
+
import { acquireManagedLock } from "./lock.js";
|
|
7
|
+
import { managedRunDir } from "./paths.js";
|
|
8
|
+
import { buildExecutorPrompt, buildReviewPrompt } from "./prompts.js";
|
|
9
|
+
import { writeManagedSchemas } from "./schemas.js";
|
|
10
|
+
import { appendTaskReport, loadRegistry, loadManagedRun, loadManagedTask, saveManagedRun, saveManagedTask, upsertRegistryEntry, updateTaskReportStatus, writeJsonAtomic, } from "./storage.js";
|
|
11
|
+
import { activeRunMilliseconds, computeWorkspaceFingerprintForRoots, } from "./state.js";
|
|
12
|
+
import { ASSETS_DIR } from "../../platform/assets.js";
|
|
13
|
+
import { notifyManagedTask } from "./notifications.js";
|
|
14
|
+
import { validateManagedTaskContract, validateManagedTaskPromptSnapshot, } from "./contract.js";
|
|
15
|
+
import { managedList, managedText } from "./i18n.js";
|
|
16
|
+
export async function runManagedTask(projectRoot, taskId, invoker, env = process.env) {
|
|
17
|
+
let contract = loadManagedTask(projectRoot, taskId);
|
|
18
|
+
let state = loadManagedRun(projectRoot, taskId, registryRunId(contract, env));
|
|
19
|
+
const runDir = managedRunDir(projectRoot, taskId, state.runId);
|
|
20
|
+
mkdirSync(runDir, { recursive: true });
|
|
21
|
+
const lock = acquireManagedLock(path.join(runDir, "run.lock"), randomUUID(), contract.language);
|
|
22
|
+
let projectLocks = [];
|
|
23
|
+
try {
|
|
24
|
+
projectLocks = acquireProjectLocks(contract);
|
|
25
|
+
validateManagedTaskContract(contract);
|
|
26
|
+
validateManagedTaskPromptSnapshot(contract);
|
|
27
|
+
if (state.contractHash !== contract.contractHash) {
|
|
28
|
+
return finishBlocked(contract, state, mt(contract, "运行状态与任务合同哈希不一致", "Run state and task contract hashes do not match"), env);
|
|
29
|
+
}
|
|
30
|
+
if (!verifyManagedJournal(state)) {
|
|
31
|
+
return finishBlocked(contract, state, mt(contract, "事件账本校验失败,拒绝继续", "Event journal verification failed; refusing to continue"), env);
|
|
32
|
+
}
|
|
33
|
+
state = transition(contract, state, "running", "preflight", env);
|
|
34
|
+
appendManagedEvent(state, {
|
|
35
|
+
eventType: "run.started",
|
|
36
|
+
actor: "managed-runner",
|
|
37
|
+
role: "runner",
|
|
38
|
+
summary: mt(contract, "后台开始或恢复托管任务", "Background service started or resumed the managed task"),
|
|
39
|
+
});
|
|
40
|
+
const schemas = writeManagedSchemas(state);
|
|
41
|
+
state = reconcileCompletedReview(contract, state, env);
|
|
42
|
+
while (state.status === "running") {
|
|
43
|
+
state = enforceBudgets(contract, state, env);
|
|
44
|
+
if (state.status !== "running")
|
|
45
|
+
break;
|
|
46
|
+
if (!state.lastExecutorResult) {
|
|
47
|
+
const findings = loadRepairFindings(state);
|
|
48
|
+
state = await executeWorker(contract, state, findings, schemas.executor, invoker, env);
|
|
49
|
+
if (state.status !== "running")
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
state = await executeReview(contract, state, schemas.reviewer, invoker, env);
|
|
53
|
+
if (state.status !== "running")
|
|
54
|
+
break;
|
|
55
|
+
contract = loadManagedTask(projectRoot, taskId);
|
|
56
|
+
validateManagedTaskContract(contract);
|
|
57
|
+
}
|
|
58
|
+
return state;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const message = error.message;
|
|
62
|
+
if (state.activeSince) {
|
|
63
|
+
state.activeRunMilliseconds += Math.max(0, Date.now() - Date.parse(state.activeSince));
|
|
64
|
+
state.activeSince = null;
|
|
65
|
+
saveManagedRun(state);
|
|
66
|
+
}
|
|
67
|
+
if (isConnectivityFailure(message)) {
|
|
68
|
+
state = transition(contract, state, "waiting_for_connectivity", "connectivity_wait", env);
|
|
69
|
+
state.blocker = message;
|
|
70
|
+
saveManagedRun(state);
|
|
71
|
+
appendManagedEvent(state, {
|
|
72
|
+
eventType: "connectivity.lost",
|
|
73
|
+
actor: "managed-runner",
|
|
74
|
+
role: "system",
|
|
75
|
+
summary: mt(contract, `网络或模型服务暂时不可用:${message}`, `Network or model service is temporarily unavailable: ${message}`),
|
|
76
|
+
});
|
|
77
|
+
return state;
|
|
78
|
+
}
|
|
79
|
+
return finishBlocked(contract, state, message, env);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
for (const projectLock of projectLocks.reverse())
|
|
83
|
+
projectLock.release();
|
|
84
|
+
lock.release();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function reconcileCompletedReview(contract, state, env) {
|
|
88
|
+
if (!state.lastReviewResult || !existsSync(state.lastReviewResult))
|
|
89
|
+
return state;
|
|
90
|
+
if (!state.lastReviewResult.endsWith(`review-result-${state.reviewRound}.json`)) {
|
|
91
|
+
return state;
|
|
92
|
+
}
|
|
93
|
+
const result = JSON.parse(readFileSync(state.lastReviewResult, "utf-8"));
|
|
94
|
+
validateReviewResult(result, contract.language);
|
|
95
|
+
const endedReviews = readManagedEvents(state).filter((event) => event.eventType === "review.ended").length;
|
|
96
|
+
if (endedReviews < state.reviewInvocations) {
|
|
97
|
+
appendTaskReport(state, reviewReportSection(state, result));
|
|
98
|
+
appendManagedEvent(state, {
|
|
99
|
+
eventType: "review.ended",
|
|
100
|
+
actor: contract.supervisorAgent,
|
|
101
|
+
role: "supervisor",
|
|
102
|
+
summary: mt(contract, `恢复已落盘的检查结果:${result.summary}`, `Recovered persisted review result: ${result.summary}`),
|
|
103
|
+
evidencePaths: [state.lastReviewResult],
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (result.result === "pass") {
|
|
107
|
+
return finishAwaitingGitApproval(contract, state, result.summary, env);
|
|
108
|
+
}
|
|
109
|
+
if (result.result === "blocked") {
|
|
110
|
+
return finishBlocked(contract, state, result.summary, env);
|
|
111
|
+
}
|
|
112
|
+
if (state.reviewRound >= contract.budgets.maxReviewRounds) {
|
|
113
|
+
return finishReviewExhausted(contract, state, env);
|
|
114
|
+
}
|
|
115
|
+
if (state.executorInvocations <= state.reviewInvocations) {
|
|
116
|
+
state.lastExecutorResult = null;
|
|
117
|
+
state.currentStep = "repair_prompt_ready";
|
|
118
|
+
saveManagedRun(state);
|
|
119
|
+
appendManagedEvent(state, {
|
|
120
|
+
eventType: "recovery.checkpoint_restored",
|
|
121
|
+
actor: "managed-runner",
|
|
122
|
+
role: "runner",
|
|
123
|
+
summary: mt(contract, "根据已落盘的整改结论恢复到执行者整改步骤", "Restored the executor repair step from persisted review findings"),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return state;
|
|
127
|
+
}
|
|
128
|
+
async function executeWorker(contract, state, findings, schemaPath, invoker, env) {
|
|
129
|
+
if (state.executorInvocations >= contract.budgets.maxExecutorInvocations) {
|
|
130
|
+
return finishBudgetExhausted(contract, state, mt(contract, "执行调用次数达到上限", "Executor invocation limit reached"), env);
|
|
131
|
+
}
|
|
132
|
+
if (state.totalAgentInvocations >= contract.budgets.maxTotalAgentInvocations) {
|
|
133
|
+
return finishBudgetExhausted(contract, state, mt(contract, "总 Agent 调用次数达到上限", "Total Agent invocation limit reached"), env);
|
|
134
|
+
}
|
|
135
|
+
state.executorInvocations += 1;
|
|
136
|
+
state.totalAgentInvocations += 1;
|
|
137
|
+
state.currentStep =
|
|
138
|
+
findings.length > 0 ? "executor_repairing" : "executor_implementing";
|
|
139
|
+
state.activeSince = new Date().toISOString();
|
|
140
|
+
saveManagedRun(state);
|
|
141
|
+
appendManagedEvent(state, {
|
|
142
|
+
eventType: "executor.started",
|
|
143
|
+
actor: contract.executorAgent,
|
|
144
|
+
role: "executor",
|
|
145
|
+
summary: mt(contract, `开始第 ${state.executorInvocations} 次执行调用`, `Started executor invocation ${state.executorInvocations}`),
|
|
146
|
+
});
|
|
147
|
+
const prompt = buildExecutorPrompt(contract, state, findings);
|
|
148
|
+
const promptFile = path.join(managedRunDir(state.projectRoot, state.taskId, state.runId), findings.length > 0
|
|
149
|
+
? `repair-${state.reviewRound}.md`
|
|
150
|
+
: "executor-initial-prompt.md");
|
|
151
|
+
writeFileSync(promptFile, prompt, "utf-8");
|
|
152
|
+
state.lastRepairPrompt =
|
|
153
|
+
findings.length > 0 ? promptFile : state.lastRepairPrompt;
|
|
154
|
+
const started = Date.now();
|
|
155
|
+
const result = await invoker.invoke(invocation(contract, state, "executor", prompt, schemaPath, state.executorSession.sessionId, (sessionId) => {
|
|
156
|
+
state.executorSession = {
|
|
157
|
+
...state.executorSession,
|
|
158
|
+
sessionId,
|
|
159
|
+
createdAt: state.executorSession.createdAt ?? new Date().toISOString(),
|
|
160
|
+
status: "active",
|
|
161
|
+
lastResumedRound: state.reviewRound,
|
|
162
|
+
};
|
|
163
|
+
saveManagedRun(state);
|
|
164
|
+
}, (summary) => appendManagedEvent(state, {
|
|
165
|
+
eventType: "executor.progress",
|
|
166
|
+
actor: contract.executorAgent,
|
|
167
|
+
role: "executor",
|
|
168
|
+
summary,
|
|
169
|
+
})));
|
|
170
|
+
state = closeActiveTime(state, started);
|
|
171
|
+
state.executorSession = {
|
|
172
|
+
...state.executorSession,
|
|
173
|
+
sessionId: result.sessionId,
|
|
174
|
+
createdAt: state.executorSession.createdAt ?? new Date().toISOString(),
|
|
175
|
+
status: "active",
|
|
176
|
+
lastResumedRound: state.reviewRound,
|
|
177
|
+
};
|
|
178
|
+
validateExecutorResult(contract, result.output);
|
|
179
|
+
const resultFile = path.join(managedRunDir(state.projectRoot, state.taskId, state.runId), `executor-result-${state.executorInvocations}.json`);
|
|
180
|
+
writeJsonAtomic(resultFile, result.output);
|
|
181
|
+
const logFiles = writeInvocationLogs(state, `executor-${state.executorInvocations}`, result.stdout, result.stderr);
|
|
182
|
+
state.lastExecutorResult = resultFile;
|
|
183
|
+
state.currentStep = "delivery_evidence_checking";
|
|
184
|
+
state.workspaceFingerprint = computeWorkspaceFingerprintForRoots([
|
|
185
|
+
contract.projectRoot,
|
|
186
|
+
...contract.relatedProjectRoots,
|
|
187
|
+
]);
|
|
188
|
+
saveManagedRun(state);
|
|
189
|
+
appendTaskReport(state, executorReportSection(state, result.output));
|
|
190
|
+
appendManagedEvent(state, {
|
|
191
|
+
eventType: "executor.ended",
|
|
192
|
+
actor: contract.executorAgent,
|
|
193
|
+
role: "executor",
|
|
194
|
+
summary: result.output.summary,
|
|
195
|
+
evidencePaths: [resultFile, ...logFiles, ...result.output.evidence],
|
|
196
|
+
});
|
|
197
|
+
if (result.output.status === "ready_for_review")
|
|
198
|
+
return state;
|
|
199
|
+
return finishBlocked(contract, state, managedList(contract.language, result.output.blockers, "", "") || result.output.summary, env);
|
|
200
|
+
}
|
|
201
|
+
async function executeReview(contract, state, schemaPath, invoker, env) {
|
|
202
|
+
if (state.reviewInvocations >= contract.budgets.maxReviewRounds) {
|
|
203
|
+
return finishReviewExhausted(contract, state, env);
|
|
204
|
+
}
|
|
205
|
+
if (state.totalAgentInvocations >= contract.budgets.maxTotalAgentInvocations) {
|
|
206
|
+
return finishBudgetExhausted(contract, state, mt(contract, "总 Agent 调用次数达到上限", "Total Agent invocation limit reached"), env);
|
|
207
|
+
}
|
|
208
|
+
const fingerprint = computeWorkspaceFingerprintForRoots([
|
|
209
|
+
contract.projectRoot,
|
|
210
|
+
...contract.relatedProjectRoots,
|
|
211
|
+
]);
|
|
212
|
+
state.reviewRound += 1;
|
|
213
|
+
state.reviewInvocations += 1;
|
|
214
|
+
state.totalAgentInvocations += 1;
|
|
215
|
+
state.currentStep = "supervisor_reviewing";
|
|
216
|
+
state.activeSince = new Date().toISOString();
|
|
217
|
+
saveManagedRun(state);
|
|
218
|
+
appendManagedEvent(state, {
|
|
219
|
+
eventType: "review.started",
|
|
220
|
+
actor: contract.supervisorAgent,
|
|
221
|
+
role: "supervisor",
|
|
222
|
+
summary: mt(contract, `开始第 ${state.reviewRound} 轮正式检查`, `Started formal review round ${state.reviewRound}`),
|
|
223
|
+
});
|
|
224
|
+
const prompt = buildReviewPrompt(contract, state);
|
|
225
|
+
const started = Date.now();
|
|
226
|
+
const result = await invoker.invoke(invocation(contract, state, "supervisor", prompt, schemaPath, state.supervisorSession.sessionId, (sessionId) => {
|
|
227
|
+
state.supervisorSession = {
|
|
228
|
+
...state.supervisorSession,
|
|
229
|
+
sessionId,
|
|
230
|
+
createdAt: state.supervisorSession.createdAt ?? new Date().toISOString(),
|
|
231
|
+
status: "active",
|
|
232
|
+
lastResumedRound: state.reviewRound,
|
|
233
|
+
};
|
|
234
|
+
saveManagedRun(state);
|
|
235
|
+
}, (summary) => appendManagedEvent(state, {
|
|
236
|
+
eventType: "review.progress",
|
|
237
|
+
actor: contract.supervisorAgent,
|
|
238
|
+
role: "supervisor",
|
|
239
|
+
summary,
|
|
240
|
+
})));
|
|
241
|
+
state = closeActiveTime(state, started);
|
|
242
|
+
state.supervisorSession = {
|
|
243
|
+
...state.supervisorSession,
|
|
244
|
+
sessionId: result.sessionId,
|
|
245
|
+
createdAt: state.supervisorSession.createdAt ?? new Date().toISOString(),
|
|
246
|
+
status: "active",
|
|
247
|
+
lastResumedRound: state.reviewRound,
|
|
248
|
+
};
|
|
249
|
+
validateReviewResult(result.output, contract.language);
|
|
250
|
+
if (computeWorkspaceFingerprintForRoots([
|
|
251
|
+
contract.projectRoot,
|
|
252
|
+
...contract.relatedProjectRoots,
|
|
253
|
+
]) !== fingerprint) {
|
|
254
|
+
return finishBlocked(contract, state, mt(contract, "只读检查期间工作区发生变化", "Workspace changed during read-only review"), env);
|
|
255
|
+
}
|
|
256
|
+
const resultFile = path.join(managedRunDir(state.projectRoot, state.taskId, state.runId), `review-result-${state.reviewRound}.json`);
|
|
257
|
+
writeJsonAtomic(resultFile, result.output);
|
|
258
|
+
const logFiles = writeInvocationLogs(state, `review-${state.reviewRound}`, result.stdout, result.stderr);
|
|
259
|
+
state.lastReviewResult = resultFile;
|
|
260
|
+
saveManagedRun(state);
|
|
261
|
+
appendTaskReport(state, reviewReportSection(state, result.output));
|
|
262
|
+
appendManagedEvent(state, {
|
|
263
|
+
eventType: "review.ended",
|
|
264
|
+
actor: contract.supervisorAgent,
|
|
265
|
+
role: "supervisor",
|
|
266
|
+
summary: result.output.summary,
|
|
267
|
+
evidencePaths: [resultFile, ...logFiles],
|
|
268
|
+
});
|
|
269
|
+
if (result.output.result === "pass") {
|
|
270
|
+
return finishAwaitingGitApproval(contract, state, result.output.summary, env);
|
|
271
|
+
}
|
|
272
|
+
if (result.output.result === "blocked") {
|
|
273
|
+
return finishBlocked(contract, state, result.output.summary, env);
|
|
274
|
+
}
|
|
275
|
+
if (state.reviewRound >= contract.budgets.maxReviewRounds) {
|
|
276
|
+
return finishReviewExhausted(contract, state, env);
|
|
277
|
+
}
|
|
278
|
+
state.lastExecutorResult = null;
|
|
279
|
+
state.currentStep = "repair_prompt_ready";
|
|
280
|
+
saveManagedRun(state);
|
|
281
|
+
return state;
|
|
282
|
+
}
|
|
283
|
+
function invocation(contract, state, role, prompt, schemaPath, sessionId, onSession, onProgress) {
|
|
284
|
+
return {
|
|
285
|
+
taskId: state.taskId,
|
|
286
|
+
runId: state.runId,
|
|
287
|
+
role,
|
|
288
|
+
language: contract.language,
|
|
289
|
+
agent: role === "executor" ? contract.executorAgent : contract.supervisorAgent,
|
|
290
|
+
projectRoot: state.projectRoot,
|
|
291
|
+
writableRoots: contract.relatedProjectRoots,
|
|
292
|
+
prompt,
|
|
293
|
+
schemaPath,
|
|
294
|
+
sessionId,
|
|
295
|
+
timeout: {
|
|
296
|
+
warningMs: contract.budgets.noProgressWarningMinutes * 60_000,
|
|
297
|
+
stalledMs: contract.budgets.stalledTimeoutMinutes * 60_000,
|
|
298
|
+
hardMs: contract.budgets.maxSingleInvocationHours * 3_600_000,
|
|
299
|
+
},
|
|
300
|
+
onSession,
|
|
301
|
+
onProgress,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function transition(contract, state, status, currentStep, env) {
|
|
305
|
+
state = {
|
|
306
|
+
...state,
|
|
307
|
+
status,
|
|
308
|
+
currentStep,
|
|
309
|
+
servicePid: process.pid,
|
|
310
|
+
updatedAt: new Date().toISOString(),
|
|
311
|
+
};
|
|
312
|
+
contract.status = status;
|
|
313
|
+
saveManagedRun(state);
|
|
314
|
+
saveManagedTask(contract);
|
|
315
|
+
updateRegistry(contract, state, env);
|
|
316
|
+
return state;
|
|
317
|
+
}
|
|
318
|
+
function finishAwaitingGitApproval(contract, state, summary, env) {
|
|
319
|
+
state = transition(contract, state, "awaiting_git_approval", "awaiting_git_approval", env);
|
|
320
|
+
state.completedAt = new Date().toISOString();
|
|
321
|
+
state.workspaceFingerprint = computeWorkspaceFingerprintForRoots([
|
|
322
|
+
contract.projectRoot,
|
|
323
|
+
...contract.relatedProjectRoots,
|
|
324
|
+
]);
|
|
325
|
+
saveManagedRun(state);
|
|
326
|
+
runManagedDeliveryCheck(state, true);
|
|
327
|
+
appendTaskReport(state, mt(contract, `\n## 最终结论\n\n${summary}\n\n状态:等待用户批准 Git 提交。\n`, `\n## Final conclusion\n\n${summary}\n\nStatus: waiting for user approval to commit Git changes.\n`));
|
|
328
|
+
updateTaskReportStatus(state, mt(contract, "等待用户批准 Git 提交", "waiting for Git commit approval"));
|
|
329
|
+
appendManagedEvent(state, {
|
|
330
|
+
eventType: "run.delivery_ready",
|
|
331
|
+
actor: "managed-runner",
|
|
332
|
+
role: "runner",
|
|
333
|
+
summary: mt(contract, "任务已通过检查,等待用户批准 Git 提交", "Task passed review and is waiting for user approval to commit Git changes"),
|
|
334
|
+
});
|
|
335
|
+
notifyManagedTask({
|
|
336
|
+
taskId: state.taskId,
|
|
337
|
+
type: "delivery_ready",
|
|
338
|
+
title: mt(contract, "Superflow 任务已完成", "Superflow task completed"),
|
|
339
|
+
message: mt(contract, `${state.taskId} 已通过检查,等待 Git 提交批准`, `${state.taskId} passed review and is waiting for Git commit approval`),
|
|
340
|
+
}, env);
|
|
341
|
+
return state;
|
|
342
|
+
}
|
|
343
|
+
function finishBlocked(contract, state, blocker, env) {
|
|
344
|
+
state = transition(contract, state, "waiting_for_human", "waiting_for_human", env);
|
|
345
|
+
state.blocker = blocker;
|
|
346
|
+
state.completedAt = null;
|
|
347
|
+
saveManagedRun(state);
|
|
348
|
+
appendTaskReport(state, mt(contract, `\n## 当前阻塞\n\n${blocker}\n`, `\n## Current blocker\n\n${blocker}\n`));
|
|
349
|
+
updateTaskReportStatus(state, mt(contract, "等待人工处理", "waiting for human action"));
|
|
350
|
+
appendManagedEvent(state, {
|
|
351
|
+
eventType: "human_input.required",
|
|
352
|
+
actor: "managed-runner",
|
|
353
|
+
role: "runner",
|
|
354
|
+
summary: blocker,
|
|
355
|
+
});
|
|
356
|
+
notifyManagedTask({
|
|
357
|
+
taskId: state.taskId,
|
|
358
|
+
type: "human_required",
|
|
359
|
+
title: mt(contract, "Superflow 任务需要处理", "Superflow task needs attention"),
|
|
360
|
+
message: `${state.taskId}: ${blocker}`,
|
|
361
|
+
}, env);
|
|
362
|
+
return state;
|
|
363
|
+
}
|
|
364
|
+
function finishReviewExhausted(contract, state, env) {
|
|
365
|
+
state = transition(contract, state, "review_exhausted", "review_exhausted", env);
|
|
366
|
+
state.blocker = mt(contract, "正式检查达到五轮上限", "Formal review reached the five-round limit");
|
|
367
|
+
saveManagedRun(state);
|
|
368
|
+
updateTaskReportStatus(state, mt(contract, "检查轮次耗尽", "review rounds exhausted"));
|
|
369
|
+
appendManagedEvent(state, {
|
|
370
|
+
eventType: "budget.exhausted",
|
|
371
|
+
actor: "managed-runner",
|
|
372
|
+
role: "runner",
|
|
373
|
+
summary: state.blocker,
|
|
374
|
+
});
|
|
375
|
+
notifyManagedTask({
|
|
376
|
+
taskId: state.taskId,
|
|
377
|
+
type: "budget_exhausted",
|
|
378
|
+
title: mt(contract, "Superflow 检查轮次已耗尽", "Superflow review rounds exhausted"),
|
|
379
|
+
message: `${state.taskId}: ${state.blocker}`,
|
|
380
|
+
}, env);
|
|
381
|
+
return state;
|
|
382
|
+
}
|
|
383
|
+
function finishBudgetExhausted(contract, state, reason, env) {
|
|
384
|
+
state = transition(contract, state, "budget_exhausted", "budget_exhausted", env);
|
|
385
|
+
state.blocker = reason;
|
|
386
|
+
saveManagedRun(state);
|
|
387
|
+
updateTaskReportStatus(state, mt(contract, "调用预算耗尽", "invocation budget exhausted"));
|
|
388
|
+
appendManagedEvent(state, {
|
|
389
|
+
eventType: "budget.exhausted",
|
|
390
|
+
actor: "managed-runner",
|
|
391
|
+
role: "runner",
|
|
392
|
+
summary: reason,
|
|
393
|
+
});
|
|
394
|
+
notifyManagedTask({
|
|
395
|
+
taskId: state.taskId,
|
|
396
|
+
type: "budget_exhausted",
|
|
397
|
+
title: mt(contract, "Superflow 调用预算已耗尽", "Superflow invocation budget exhausted"),
|
|
398
|
+
message: `${state.taskId}: ${reason}`,
|
|
399
|
+
}, env);
|
|
400
|
+
return state;
|
|
401
|
+
}
|
|
402
|
+
function enforceBudgets(contract, state, env) {
|
|
403
|
+
const activeHours = activeRunMilliseconds(state) / 3_600_000;
|
|
404
|
+
if (activeHours >= contract.budgets.activeRunWarningHours &&
|
|
405
|
+
!readManagedEvents(state).some((event) => event.eventType === "deadline.warning")) {
|
|
406
|
+
appendManagedEvent(state, {
|
|
407
|
+
eventType: "deadline.warning",
|
|
408
|
+
actor: "managed-runner",
|
|
409
|
+
role: "runner",
|
|
410
|
+
summary: mt(contract, `实际工作时间已达到 ${contract.budgets.activeRunWarningHours} 小时,任务继续运行但已接近兜底上限`, `Active work time reached ${contract.budgets.activeRunWarningHours} hours; the task continues but is approaching the safety limit`),
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
if (activeHours >= contract.budgets.maxActiveRunHours) {
|
|
414
|
+
state = transition(contract, state, "deadline_exhausted", "deadline_exhausted", env);
|
|
415
|
+
state.blocker = mt(contract, "实际工作时间达到防失控上限", "Active work time reached the safety limit");
|
|
416
|
+
saveManagedRun(state);
|
|
417
|
+
appendManagedEvent(state, {
|
|
418
|
+
eventType: "deadline.exhausted",
|
|
419
|
+
actor: "managed-runner",
|
|
420
|
+
role: "runner",
|
|
421
|
+
summary: state.blocker,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
return state;
|
|
425
|
+
}
|
|
426
|
+
function closeActiveTime(state, started) {
|
|
427
|
+
state.activeRunMilliseconds += Math.max(0, Date.now() - started);
|
|
428
|
+
state.activeSince = null;
|
|
429
|
+
saveManagedRun(state);
|
|
430
|
+
return state;
|
|
431
|
+
}
|
|
432
|
+
function loadRepairFindings(state) {
|
|
433
|
+
if (!state.lastReviewResult || !existsSync(state.lastReviewResult))
|
|
434
|
+
return [];
|
|
435
|
+
const result = JSON.parse(readFileSync(state.lastReviewResult, "utf-8"));
|
|
436
|
+
return result.findings.filter((finding) => finding.blocking);
|
|
437
|
+
}
|
|
438
|
+
function validateExecutorResult(contract, result) {
|
|
439
|
+
if (!["ready_for_review", "blocked", "failed"].includes(result.status)) {
|
|
440
|
+
throw new Error(mt(contract, "执行 Agent 返回了非法状态", "Executor Agent returned an invalid status"));
|
|
441
|
+
}
|
|
442
|
+
if (!Array.isArray(result.changedFiles) ||
|
|
443
|
+
!Array.isArray(result.commands) ||
|
|
444
|
+
!Array.isArray(result.evidence) ||
|
|
445
|
+
!Array.isArray(result.blockers)) {
|
|
446
|
+
throw new Error(mt(contract, "执行 Agent 结果缺少文件、命令、证据或阻塞列表", "Executor Agent result is missing files, commands, evidence, or blockers"));
|
|
447
|
+
}
|
|
448
|
+
if (!result.summary.trim()) {
|
|
449
|
+
throw new Error(mt(contract, "执行 Agent 结果缺少摘要", "Executor Agent result is missing a summary"));
|
|
450
|
+
}
|
|
451
|
+
if (result.status !== "ready_for_review") {
|
|
452
|
+
if (result.blockers.length === 0) {
|
|
453
|
+
throw new Error(mt(contract, "执行 Agent 声明阻塞或失败,但没有提供阻塞原因", "Executor Agent reported blocked or failed without a blocker"));
|
|
454
|
+
}
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (result.blockers.length > 0) {
|
|
458
|
+
throw new Error(mt(contract, "执行 Agent 声明可检查,但仍存在阻塞项", "Executor Agent reported ready for review while blockers remain"));
|
|
459
|
+
}
|
|
460
|
+
if (result.commands.some((command) => command.exitCode !== 0)) {
|
|
461
|
+
throw new Error(mt(contract, "执行 Agent 声明可检查,但命令证据中仍有失败项", "Executor Agent reported ready for review while command evidence still contains failures"));
|
|
462
|
+
}
|
|
463
|
+
if (contract.profile === "engineering" || contract.profile === "sdd") {
|
|
464
|
+
validateEngineeringEvidence(result.commands, contract);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function validateEngineeringEvidence(commands, contract) {
|
|
468
|
+
const successful = commands.filter((command) => command.exitCode === 0);
|
|
469
|
+
const categories = new Set(successful.flatMap((command) => verificationCategories(command.command)));
|
|
470
|
+
if (categories.size < 2) {
|
|
471
|
+
throw new Error(mt(contract, "工程任务至少需要两类成功验证证据(构建、测试、启动或真实调用),只编译或只跑单测不能交付", "Engineering tasks require at least two successful evidence categories (build, tests, startup, or real calls); compilation-only or unit-test-only evidence cannot be delivered"));
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
function verificationCategories(command) {
|
|
475
|
+
const normalized = command.toLowerCase();
|
|
476
|
+
const categories = [];
|
|
477
|
+
if (/(^|\s)(test|vitest|jest|pytest|phpunit|go\s+test|cargo\s+test)(\s|$)/.test(normalized) ||
|
|
478
|
+
/(?:npm|pnpm|yarn)\s+(?:run\s+)?test/.test(normalized) ||
|
|
479
|
+
/mvn(?:\s+[^\s]+)*\s+test/.test(normalized) ||
|
|
480
|
+
/gradle\w*\s+test/.test(normalized)) {
|
|
481
|
+
categories.push("test");
|
|
482
|
+
}
|
|
483
|
+
if (/(?:npm|pnpm|yarn)\s+(?:run\s+)?build/.test(normalized) ||
|
|
484
|
+
/(^|\s)(tsc|make|cmake|compile)(\s|$)/.test(normalized) ||
|
|
485
|
+
/mvn(?:\s+[^\s]+)*\s+(?:compile|package|verify)/.test(normalized) ||
|
|
486
|
+
/gradle\w*\s+(?:build|assemble)/.test(normalized) ||
|
|
487
|
+
/cargo\s+build|go\s+build/.test(normalized)) {
|
|
488
|
+
categories.push("build");
|
|
489
|
+
}
|
|
490
|
+
if (/(?:npm|pnpm|yarn)\s+(?:run\s+)?(?:start|dev|serve)/.test(normalized) ||
|
|
491
|
+
/spring-boot:run|docker\s+compose\s+up|(^|\s)(curl|wget)(\s|$)/.test(normalized) ||
|
|
492
|
+
/health(?:check)?|integration|e2e/.test(normalized)) {
|
|
493
|
+
categories.push("runtime");
|
|
494
|
+
}
|
|
495
|
+
return categories;
|
|
496
|
+
}
|
|
497
|
+
function validateReviewResult(result, language = "zh") {
|
|
498
|
+
if (!["pass", "needs_fix", "blocked"].includes(result.result)) {
|
|
499
|
+
throw new Error(managedText(language, "监督 Agent 返回了非法状态", "Supervisor Agent returned an invalid status"));
|
|
500
|
+
}
|
|
501
|
+
if (!Array.isArray(result.findings))
|
|
502
|
+
throw new Error(managedText(language, "监督 Agent 结果缺少 findings", "Supervisor Agent result is missing findings"));
|
|
503
|
+
if (result.result === "pass" &&
|
|
504
|
+
result.findings.some((finding) => finding.blocking)) {
|
|
505
|
+
throw new Error(managedText(language, "监督结果声明通过,但仍存在阻断 finding", "Supervisor result reported pass while blocking findings remain"));
|
|
506
|
+
}
|
|
507
|
+
if (result.result === "needs_fix" &&
|
|
508
|
+
!result.findings.some((finding) => finding.blocking)) {
|
|
509
|
+
throw new Error(managedText(language, "监督结果要求整改,但没有提供阻断 finding", "Supervisor result requested fixes without a blocking finding"));
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function executorReportSection(state, result) {
|
|
513
|
+
if (state.language === "en") {
|
|
514
|
+
return [
|
|
515
|
+
`## Executor invocation ${state.executorInvocations}`,
|
|
516
|
+
"",
|
|
517
|
+
`Status: ${result.status}`,
|
|
518
|
+
`Summary: ${result.summary}`,
|
|
519
|
+
`Changed files: ${result.changedFiles.join(", ") || "none"}`,
|
|
520
|
+
`Blockers: ${result.blockers.join("; ") || "none"}`,
|
|
521
|
+
].join("\n");
|
|
522
|
+
}
|
|
523
|
+
return [
|
|
524
|
+
`## 执行调用 ${state.executorInvocations}`,
|
|
525
|
+
"",
|
|
526
|
+
`状态:${result.status}`,
|
|
527
|
+
`摘要:${result.summary}`,
|
|
528
|
+
`修改文件:${result.changedFiles.join("、") || "无"}`,
|
|
529
|
+
`阻塞:${result.blockers.join(";") || "无"}`,
|
|
530
|
+
].join("\n");
|
|
531
|
+
}
|
|
532
|
+
function reviewReportSection(state, result) {
|
|
533
|
+
if (state.language === "en") {
|
|
534
|
+
return [
|
|
535
|
+
`## Formal review R${state.reviewRound}`,
|
|
536
|
+
"",
|
|
537
|
+
`Result: ${result.result}`,
|
|
538
|
+
`Summary: ${result.summary}`,
|
|
539
|
+
`Blocking findings: ${result.findings.filter((finding) => finding.blocking).length}`,
|
|
540
|
+
].join("\n");
|
|
541
|
+
}
|
|
542
|
+
return [
|
|
543
|
+
`## 正式检查 R${state.reviewRound}`,
|
|
544
|
+
"",
|
|
545
|
+
`结果:${result.result}`,
|
|
546
|
+
`摘要:${result.summary}`,
|
|
547
|
+
`阻断问题:${result.findings.filter((finding) => finding.blocking).length}`,
|
|
548
|
+
].join("\n");
|
|
549
|
+
}
|
|
550
|
+
function updateRegistry(contract, state, env) {
|
|
551
|
+
upsertRegistryEntry({
|
|
552
|
+
taskId: contract.taskId,
|
|
553
|
+
projectRoot: contract.projectRoot,
|
|
554
|
+
status: state.status,
|
|
555
|
+
profile: state.profile,
|
|
556
|
+
language: contract.language,
|
|
557
|
+
activeRunId: state.runId,
|
|
558
|
+
createdAt: contract.createdAt,
|
|
559
|
+
updatedAt: new Date().toISOString(),
|
|
560
|
+
servicePid: state.servicePid,
|
|
561
|
+
}, env);
|
|
562
|
+
}
|
|
563
|
+
function registryRunId(contract, env) {
|
|
564
|
+
const registry = loadRegistry(env);
|
|
565
|
+
const entry = registry.tasks.find((item) => item.taskId === contract.taskId);
|
|
566
|
+
if (!entry) {
|
|
567
|
+
throw new Error(mt(contract, `全局任务索引缺少 ${contract.taskId}`, `Global task registry is missing ${contract.taskId}`));
|
|
568
|
+
}
|
|
569
|
+
return entry.activeRunId;
|
|
570
|
+
}
|
|
571
|
+
function isConnectivityFailure(message) {
|
|
572
|
+
return /(network|socket|connect(?:ion|ed|ing)?|unable to connect|server_error|timed?\s*out|rate.?limit|overloaded|busy|429|503|ECONN|连接|限流|繁忙)/i.test(message);
|
|
573
|
+
}
|
|
574
|
+
function acquireProjectLocks(contract) {
|
|
575
|
+
const acquired = [];
|
|
576
|
+
try {
|
|
577
|
+
for (const root of [
|
|
578
|
+
contract.projectRoot,
|
|
579
|
+
...contract.relatedProjectRoots,
|
|
580
|
+
].sort()) {
|
|
581
|
+
acquired.push(acquireManagedLock(path.join(root, ".superflow", "managed-project.lock"), `${contract.taskId}:${randomUUID()}`, contract.language));
|
|
582
|
+
}
|
|
583
|
+
return acquired;
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
for (const lock of acquired.reverse())
|
|
587
|
+
lock.release();
|
|
588
|
+
throw error;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function runManagedDeliveryCheck(state, beforeReadyEvent = false) {
|
|
592
|
+
const script = path.join(ASSETS_DIR, "scripts", "superflow-managed-work-check.mjs");
|
|
593
|
+
const args = [script, state.projectRoot, state.taskId];
|
|
594
|
+
if (beforeReadyEvent)
|
|
595
|
+
args.push("--before-ready-event");
|
|
596
|
+
execFileSync(process.execPath, args, {
|
|
597
|
+
cwd: state.projectRoot,
|
|
598
|
+
encoding: "utf-8",
|
|
599
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
function writeInvocationLogs(state, prefix, stdout, stderr) {
|
|
603
|
+
const runDir = managedRunDir(state.projectRoot, state.taskId, state.runId);
|
|
604
|
+
const files = [];
|
|
605
|
+
if (stdout) {
|
|
606
|
+
const file = path.join(runDir, `${prefix}-events.jsonl`);
|
|
607
|
+
writeFileSync(file, redactManagedLog(stdout), "utf-8");
|
|
608
|
+
files.push(file);
|
|
609
|
+
}
|
|
610
|
+
if (stderr) {
|
|
611
|
+
const file = path.join(runDir, `${prefix}-stderr.log`);
|
|
612
|
+
writeFileSync(file, redactManagedLog(stderr), "utf-8");
|
|
613
|
+
files.push(file);
|
|
614
|
+
}
|
|
615
|
+
return files;
|
|
616
|
+
}
|
|
617
|
+
export function redactManagedLog(value) {
|
|
618
|
+
return value
|
|
619
|
+
.replace(/(authorization\s*[:=]\s*bearer\s+)[^\s"']+/gi, "$1<redacted>")
|
|
620
|
+
.replace(/(["']?(?:token|password|secret|cookie|api[_-]?key)["']?\s*[:=]\s*["']?)[^\s"',}]+/gi, "$1<redacted>")
|
|
621
|
+
.replace(/(https?:\/\/[^:\s/@]+:)[^@\s/]+@/gi, "$1<redacted>@");
|
|
622
|
+
}
|
|
623
|
+
function mt(contract, zh, en) {
|
|
624
|
+
return managedText(contract.language, zh, en);
|
|
625
|
+
}
|
|
626
|
+
//# sourceMappingURL=runner.js.map
|