@chenmk/superflow 0.4.0 → 0.5.1
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 +58 -12
- package/INSTALL.md +62 -33
- package/README.md +181 -54
- package/README.zh-CN.md +158 -51
- package/assets/brand/managed-review-loop.png +0 -0
- package/assets/manifest.json +10 -5
- package/assets/scripts/superflow-coding-ready.mjs +135 -0
- package/assets/scripts/superflow-dependency-update-hook.sh +81 -8
- package/assets/scripts/superflow-document-audit.mjs +240 -0
- package/assets/scripts/superflow-environment-preflight.mjs +398 -0
- package/assets/scripts/superflow-hook-guard.sh +40 -1
- package/assets/scripts/superflow-managed-command-guard.py +309 -0
- package/assets/scripts/superflow-managed-executor-preflight.mjs +421 -0
- package/assets/scripts/superflow-managed-owner-verification.sh +117 -0
- package/assets/scripts/superflow-managed-work-check.mjs +287 -26
- package/assets/scripts/superflow-managed-work-guard.sh +6 -92
- package/assets/skills/superflow-clarify/SKILL.md +14 -0
- package/assets/skills/superflow-design/SKILL.md +38 -13
- package/assets/skills/superflow-docs/SKILL.md +93 -2
- package/assets/skills/superflow-pipeline/SKILL.md +167 -10
- package/assets/skills/superflow-pipeline/references/batch-prompt-template.md +17 -9
- package/assets/skills/superflow-pipeline/references/document-readiness.md +157 -0
- package/assets/skills/superflow-pipeline/references/document-templates.md +9 -5
- package/assets/skills/superflow-pipeline/references/feature-gated-workflow.md +14 -0
- package/assets/skills/superflow-pipeline/references/implementation-prompt-template.md +20 -11
- package/assets/skills/superflow-pipeline/references/managed-work.md +123 -15
- package/assets/skills/superflow-pipeline/references/orchestration.md +2 -2
- package/assets/skills/superflow-pipeline/references/p0-baseline-template.md +3 -2
- package/assets/skills/superflow-pipeline/references/quality-gate.md +7 -5
- package/assets/skills/superflow-pipeline/references/reviewer-checklist.md +7 -0
- package/assets/skills/superflow-pipeline/scripts/superflow-guard.sh +205 -9
- package/assets/skills/superflow-pipeline/scripts/superflow-handoff.sh +7 -4
- package/assets/skills/superflow-requirement-review/SKILL.md +178 -0
- package/assets/skills/superflow-requirement-review/agents/openai.yaml +4 -0
- package/assets/skills-en/superflow-clarify/SKILL.md +17 -0
- package/assets/skills-en/superflow-design/SKILL.md +31 -12
- package/assets/skills-en/superflow-docs/SKILL.md +99 -0
- package/assets/skills-en/superflow-pipeline/SKILL.md +163 -9
- package/assets/skills-en/superflow-pipeline/references/batch-prompt-template.md +4 -0
- package/assets/skills-en/superflow-pipeline/references/document-readiness.md +125 -0
- package/assets/skills-en/superflow-pipeline/references/document-templates.md +10 -0
- package/assets/skills-en/superflow-pipeline/references/feature-gated-workflow.md +7 -0
- package/assets/skills-en/superflow-pipeline/references/implementation-prompt-template.md +7 -0
- package/assets/skills-en/superflow-pipeline/references/managed-work.md +160 -10
- package/assets/skills-en/superflow-pipeline/references/quality-gate.md +12 -0
- package/assets/skills-en/superflow-pipeline/references/reviewer-checklist.md +7 -0
- package/assets/skills-en/superflow-pipeline/scripts/superflow-guard.sh +205 -9
- package/assets/skills-en/superflow-pipeline/scripts/superflow-handoff.sh +7 -4
- package/assets/skills-en/superflow-requirement-review/SKILL.md +99 -0
- package/assets/skills-en/superflow-requirement-review/agents/openai.yaml +4 -0
- package/dist/app/cli.js +64 -9
- package/dist/app/cli.js.map +1 -1
- package/dist/app/commands/change-guard.js +19 -2
- package/dist/app/commands/change-guard.js.map +1 -1
- package/dist/app/commands/check.js +107 -45
- package/dist/app/commands/check.js.map +1 -1
- package/dist/app/commands/docs.js +7 -3
- package/dist/app/commands/docs.js.map +1 -1
- package/dist/app/commands/doctor.js +174 -198
- package/dist/app/commands/doctor.js.map +1 -1
- package/dist/app/commands/eval.js +64 -0
- package/dist/app/commands/eval.js.map +1 -0
- package/dist/app/commands/implement.js +7 -3
- package/dist/app/commands/implement.js.map +1 -1
- package/dist/app/commands/init.js +12 -24
- package/dist/app/commands/init.js.map +1 -1
- package/dist/app/commands/mcp.js +167 -0
- package/dist/app/commands/mcp.js.map +1 -0
- package/dist/app/commands/pipeline.js +652 -18
- package/dist/app/commands/pipeline.js.map +1 -1
- package/dist/app/commands/requirement-review.js +6 -0
- package/dist/app/commands/requirement-review.js.map +1 -0
- package/dist/app/commands/status.js +66 -3
- package/dist/app/commands/status.js.map +1 -1
- package/dist/app/commands/uninstall.js +3 -3
- package/dist/app/commands/uninstall.js.map +1 -1
- package/dist/app/commands/update.js +83 -9
- package/dist/app/commands/update.js.map +1 -1
- package/dist/domains/agent.js +3 -11
- package/dist/domains/agent.js.map +1 -1
- package/dist/domains/config/cli-help.js +54 -12
- package/dist/domains/config/cli-help.js.map +1 -1
- package/dist/domains/config/i18n.js +6 -8
- package/dist/domains/config/i18n.js.map +1 -1
- package/dist/domains/config/manifest.js +0 -3
- package/dist/domains/config/manifest.js.map +1 -1
- package/dist/domains/deps.js +68 -7
- package/dist/domains/deps.js.map +1 -1
- package/dist/domains/managed-work/completion-policy.js +174 -0
- package/dist/domains/managed-work/completion-policy.js.map +1 -0
- package/dist/domains/managed-work/context-manifest.js +208 -0
- package/dist/domains/managed-work/context-manifest.js.map +1 -0
- package/dist/domains/managed-work/contract.js +135 -4
- package/dist/domains/managed-work/contract.js.map +1 -1
- package/dist/domains/managed-work/control-signals.js +34 -0
- package/dist/domains/managed-work/control-signals.js.map +1 -0
- package/dist/domains/managed-work/control.js +628 -0
- package/dist/domains/managed-work/control.js.map +1 -0
- package/dist/domains/managed-work/evaluation.js +497 -0
- package/dist/domains/managed-work/evaluation.js.map +1 -0
- package/dist/domains/managed-work/evidence-path.js +14 -0
- package/dist/domains/managed-work/evidence-path.js.map +1 -0
- package/dist/domains/managed-work/execution-contract.js +167 -0
- package/dist/domains/managed-work/execution-contract.js.map +1 -0
- package/dist/domains/managed-work/executor-policy.js +65 -0
- package/dist/domains/managed-work/executor-policy.js.map +1 -0
- package/dist/domains/managed-work/failure.js +65 -0
- package/dist/domains/managed-work/failure.js.map +1 -0
- package/dist/domains/managed-work/host-review.js +77 -0
- package/dist/domains/managed-work/host-review.js.map +1 -0
- package/dist/domains/managed-work/human-messages.js +31 -0
- package/dist/domains/managed-work/human-messages.js.map +1 -0
- package/dist/domains/managed-work/input.js +29 -3
- package/dist/domains/managed-work/input.js.map +1 -1
- package/dist/domains/managed-work/journal.js +77 -3
- package/dist/domains/managed-work/journal.js.map +1 -1
- package/dist/domains/managed-work/lock.js +67 -1
- package/dist/domains/managed-work/lock.js.map +1 -1
- package/dist/domains/managed-work/notifications.js +0 -15
- package/dist/domains/managed-work/notifications.js.map +1 -1
- package/dist/domains/managed-work/pair-admission.js +108 -0
- package/dist/domains/managed-work/pair-admission.js.map +1 -0
- package/dist/domains/managed-work/prompts.js +257 -22
- package/dist/domains/managed-work/prompts.js.map +1 -1
- package/dist/domains/managed-work/redaction.js +90 -0
- package/dist/domains/managed-work/redaction.js.map +1 -0
- package/dist/domains/managed-work/review-convergence.js +41 -0
- package/dist/domains/managed-work/review-convergence.js.map +1 -0
- package/dist/domains/managed-work/review-facts.js +181 -0
- package/dist/domains/managed-work/review-facts.js.map +1 -0
- package/dist/domains/managed-work/rule-selection.js +84 -0
- package/dist/domains/managed-work/rule-selection.js.map +1 -0
- package/dist/domains/managed-work/runner.js +1967 -175
- package/dist/domains/managed-work/runner.js.map +1 -1
- package/dist/domains/managed-work/schemas.js +72 -2
- package/dist/domains/managed-work/schemas.js.map +1 -1
- package/dist/domains/managed-work/service.js +82 -13
- package/dist/domains/managed-work/service.js.map +1 -1
- package/dist/domains/managed-work/state.js +195 -5
- package/dist/domains/managed-work/state.js.map +1 -1
- package/dist/domains/managed-work/storage.js +66 -24
- package/dist/domains/managed-work/storage.js.map +1 -1
- package/dist/domains/managed-work/types.js +12 -9
- package/dist/domains/managed-work/types.js.map +1 -1
- package/dist/domains/managed-work/verification-categories.js +39 -0
- package/dist/domains/managed-work/verification-categories.js.map +1 -0
- package/dist/domains/managed-work/verification-policy.js +116 -0
- package/dist/domains/managed-work/verification-policy.js.map +1 -0
- package/dist/domains/managed-work/wait.js +21 -3
- package/dist/domains/managed-work/wait.js.map +1 -1
- package/dist/domains/managed-work/workspace-binding.js +133 -0
- package/dist/domains/managed-work/workspace-binding.js.map +1 -0
- package/dist/domains/sdd-readiness.js +44 -0
- package/dist/domains/sdd-readiness.js.map +1 -0
- package/dist/domains/skill/deploy.js +0 -6
- package/dist/domains/skill/deploy.js.map +1 -1
- package/dist/domains/skill/scripts.js +0 -8
- package/dist/domains/skill/scripts.js.map +1 -1
- package/dist/domains/state.js +1 -2
- package/dist/domains/state.js.map +1 -1
- package/dist/mcp/runtime-identity.js +21 -0
- package/dist/mcp/runtime-identity.js.map +1 -0
- package/dist/mcp/server.js +369 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/platform/agent-process.js +499 -38
- package/dist/platform/agent-process.js.map +1 -1
- package/dist/platform/executable.js +28 -0
- package/dist/platform/executable.js.map +1 -0
- package/dist/platform/paths.js +1 -17
- package/dist/platform/paths.js.map +1 -1
- package/dist/platform/process-tree.js +44 -0
- package/dist/platform/process-tree.js.map +1 -0
- package/dist/platform/process.js +1 -1
- package/dist/platform/process.js.map +1 -1
- package/package.json +17 -7
- package/scripts/check-design-principles.mjs +107 -0
- package/scripts/check-managed-work-design.mjs +507 -0
- package/scripts/clean-dist.js +9 -0
- package/scripts/ensure-bin-modes.js +11 -0
- package/scripts/preinstall.js +57 -0
- package/dist/cli/index.js +0 -193
- package/dist/cli/index.js.map +0 -1
- package/dist/commands/archive.js +0 -6
- package/dist/commands/archive.js.map +0 -1
- package/dist/commands/clarify.js +0 -6
- package/dist/commands/clarify.js.map +0 -1
- package/dist/commands/design.js +0 -28
- package/dist/commands/design.js.map +0 -1
- package/dist/commands/docs.js +0 -6
- package/dist/commands/docs.js.map +0 -1
- package/dist/commands/doctor.js +0 -494
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/implement.js +0 -6
- package/dist/commands/implement.js.map +0 -1
- package/dist/commands/init.js +0 -520
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/pipeline.js +0 -6
- package/dist/commands/pipeline.js.map +0 -1
- package/dist/commands/scan.js +0 -63
- package/dist/commands/scan.js.map +0 -1
- package/dist/commands/status.js +0 -173
- package/dist/commands/status.js.map +0 -1
- package/dist/commands/uninstall.js +0 -214
- package/dist/commands/uninstall.js.map +0 -1
- package/dist/commands/update.js +0 -193
- package/dist/commands/update.js.map +0 -1
- package/dist/commands/verify.js +0 -6
- package/dist/commands/verify.js.map +0 -1
- package/dist/core/assets.js +0 -27
- package/dist/core/assets.js.map +0 -1
- package/dist/core/cli-help.js +0 -98
- package/dist/core/cli-help.js.map +0 -1
- package/dist/core/context.js +0 -123
- package/dist/core/context.js.map +0 -1
- package/dist/core/dependencies.js +0 -146
- package/dist/core/dependencies.js.map +0 -1
- package/dist/core/detect.js +0 -64
- package/dist/core/detect.js.map +0 -1
- package/dist/core/i18n.js +0 -105
- package/dist/core/i18n.js.map +0 -1
- package/dist/core/integrity.js +0 -46
- package/dist/core/integrity.js.map +0 -1
- package/dist/core/manifest.js +0 -21
- package/dist/core/manifest.js.map +0 -1
- package/dist/core/prompts.js +0 -20
- package/dist/core/prompts.js.map +0 -1
- package/dist/core/registry.js +0 -134
- package/dist/core/registry.js.map +0 -1
- package/dist/core/rules.js +0 -17
- package/dist/core/rules.js.map +0 -1
- package/dist/core/scripts.js +0 -48
- package/dist/core/scripts.js.map +0 -1
- package/dist/core/skill-check.js +0 -32
- package/dist/core/skill-check.js.map +0 -1
- package/dist/core/skills.js +0 -62
- package/dist/core/skills.js.map +0 -1
- package/dist/core/state.js +0 -54
- package/dist/core/state.js.map +0 -1
- package/dist/utils/path.js +0 -11
- package/dist/utils/path.js.map +0 -1
- package/dist/utils/shell.js +0 -29
- package/dist/utils/shell.js.map +0 -1
|
@@ -1,35 +1,69 @@
|
|
|
1
|
-
import { randomUUID } from "crypto";
|
|
1
|
+
import { createHash, randomUUID } from "crypto";
|
|
2
2
|
import { execFileSync } from "child_process";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from "fs";
|
|
4
4
|
import path from "path";
|
|
5
5
|
import { appendManagedEvent, readManagedEvents, verifyManagedJournal, } from "./journal.js";
|
|
6
|
-
import { acquireManagedLock } from "./lock.js";
|
|
6
|
+
import { acquireManagedLock, acquireManagedProjectClaim } from "./lock.js";
|
|
7
7
|
import { managedRunDir } from "./paths.js";
|
|
8
8
|
import { buildExecutorPrompt, buildReviewPrompt } from "./prompts.js";
|
|
9
|
+
import { isAcceptedEvidenceCommand } from "./execution-contract.js";
|
|
9
10
|
import { writeManagedSchemas } from "./schemas.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
11
|
+
import { verificationCategories } from "./verification-categories.js";
|
|
12
|
+
import { assessReviewConvergence } from "./review-convergence.js";
|
|
13
|
+
import { ensureManagedWorkspaceBinding, managedWorkspaceBindingFingerprint, } from "./workspace-binding.js";
|
|
14
|
+
import { appendTaskReport, resolveTaskReportBlockers, loadRegistry, loadManagedRun, loadManagedTask, saveManagedRun, saveManagedTask, upsertRegistryEntry, updateTaskReportStatus, writeJsonAtomic, } from "./storage.js";
|
|
15
|
+
import { activeRunMilliseconds, computeWorkspaceFingerprintForRoots, computeScopedWorkspaceFingerprint, effectiveExecutorInvocations, effectiveTotalAgentInvocations, snapshotChangedWorkspaceFiles, } from "./state.js";
|
|
12
16
|
import { ASSETS_DIR } from "../../platform/assets.js";
|
|
13
17
|
import { notifyManagedTask } from "./notifications.js";
|
|
14
|
-
import { validateManagedTaskContract, validateManagedTaskPromptSnapshot, } from "./contract.js";
|
|
18
|
+
import { calculateManagedContractHash, validateManagedTaskContract, validateManagedTaskPromptSnapshot, } from "./contract.js";
|
|
15
19
|
import { managedList, managedText } from "./i18n.js";
|
|
20
|
+
import { readManagedHumanMessages } from "./human-messages.js";
|
|
21
|
+
import { clearManagedControlSignal, readManagedControlSignal, } from "./control-signals.js";
|
|
22
|
+
import { evaluateCompletion, readCompletionTasks, } from "./completion-policy.js";
|
|
23
|
+
import { isCommandOutcomeConsistent, validateRiskBasedEvidence, } from "./verification-policy.js";
|
|
24
|
+
import { canonicalEvidencePath } from "./evidence-path.js";
|
|
25
|
+
import { selectManagedRuleFiles } from "./rule-selection.js";
|
|
26
|
+
import { buildExecutorPolicy, stableExecutorPolicyHash, } from "./executor-policy.js";
|
|
27
|
+
import { ensureManagedContextManifest, managedContextManifestPath, } from "./context-manifest.js";
|
|
28
|
+
import { writeManagedReviewFacts } from "./review-facts.js";
|
|
29
|
+
import { redactManagedLog, redactManagedValue } from "./redaction.js";
|
|
30
|
+
import { classifyUnresumableSessionFailure } from "../../platform/agent-process.js";
|
|
31
|
+
import { classifyManagedFailure, ManagedEnvironmentPreparationError, ManagedWorkspaceClaimError, } from "./failure.js";
|
|
16
32
|
export async function runManagedTask(projectRoot, taskId, invoker, env = process.env) {
|
|
17
|
-
|
|
33
|
+
const contract = loadManagedTask(projectRoot, taskId);
|
|
18
34
|
let state = loadManagedRun(projectRoot, taskId, registryRunId(contract, env));
|
|
19
35
|
const runDir = managedRunDir(projectRoot, taskId, state.runId);
|
|
20
36
|
mkdirSync(runDir, { recursive: true });
|
|
21
37
|
const lock = acquireManagedLock(path.join(runDir, "run.lock"), randomUUID(), contract.language);
|
|
22
38
|
let projectLocks = [];
|
|
23
39
|
try {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
40
|
+
const prepared = await prepareManagedExecution(contract);
|
|
41
|
+
projectLocks = prepared.projectLocks;
|
|
42
|
+
if (migrateLegacyDataDisclosure(contract, state)) {
|
|
43
|
+
appendManagedEvent(state, {
|
|
44
|
+
eventType: "contract.data_disclosure_migrated",
|
|
45
|
+
actor: "managed-runner",
|
|
46
|
+
role: "runner",
|
|
47
|
+
summary: mt(contract, "历史任务已执行过外部研发 Agent,迁移并冻结原仓库作用域授权", "The legacy task already used an external executor; its repository-scoped approval was migrated and frozen"),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
27
50
|
if (state.contractHash !== contract.contractHash) {
|
|
28
51
|
return finishBlocked(contract, state, mt(contract, "运行状态与任务合同哈希不一致", "Run state and task contract hashes do not match"), env);
|
|
29
52
|
}
|
|
30
53
|
if (!verifyManagedJournal(state)) {
|
|
31
54
|
return finishBlocked(contract, state, mt(contract, "事件账本校验失败,拒绝继续", "Event journal verification failed; refusing to continue"), env);
|
|
32
55
|
}
|
|
56
|
+
if (!contract.permissions.externalModelDataDisclosure.approved) {
|
|
57
|
+
return finishBlocked(contract, state, mt(contract, "尚未记录外部研发 Agent 读取和处理本地源码的任务级授权;MCP 不会绕过宿主租户 DLP", "Task-scoped approval for the external executor to read and process local source code is missing; MCP does not bypass host tenant DLP"), env);
|
|
58
|
+
}
|
|
59
|
+
if (migrateLegacySupervision(contract, state)) {
|
|
60
|
+
appendManagedEvent(state, {
|
|
61
|
+
eventType: "contract.supervision_migrated",
|
|
62
|
+
actor: "managed-runner",
|
|
63
|
+
role: "runner",
|
|
64
|
+
summary: mt(contract, "旧式后台监督已永久迁移为当前 Host 直接评审", "Legacy background supervision was permanently migrated to direct host review"),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
33
67
|
state = transition(contract, state, "running", "preflight", env);
|
|
34
68
|
appendManagedEvent(state, {
|
|
35
69
|
eventType: "run.started",
|
|
@@ -39,33 +73,180 @@ export async function runManagedTask(projectRoot, taskId, invoker, env = process
|
|
|
39
73
|
});
|
|
40
74
|
const schemas = writeManagedSchemas(state);
|
|
41
75
|
state = reconcileCompletedReview(contract, state, env);
|
|
76
|
+
if (state.executorInvocations === 0 && !state.baselineCapturedAt) {
|
|
77
|
+
state.baselineWorkspaceFiles = snapshotChangedWorkspaceFiles([
|
|
78
|
+
contract.projectRoot,
|
|
79
|
+
...contract.relatedProjectRoots,
|
|
80
|
+
]);
|
|
81
|
+
state.baselineCapturedAt = new Date().toISOString();
|
|
82
|
+
saveManagedRun(state);
|
|
83
|
+
}
|
|
42
84
|
while (state.status === "running") {
|
|
85
|
+
state = pauseAtSafeBoundary(contract, state, env);
|
|
86
|
+
if (state.status !== "running")
|
|
87
|
+
break;
|
|
43
88
|
state = enforceBudgets(contract, state, env);
|
|
44
89
|
if (state.status !== "running")
|
|
45
90
|
break;
|
|
46
|
-
if (!state.lastExecutorResult
|
|
91
|
+
if (!state.lastExecutorResult ||
|
|
92
|
+
hasUndeliveredHumanGuidance(contract, state)) {
|
|
47
93
|
const findings = loadRepairFindings(state);
|
|
48
94
|
state = await executeWorker(contract, state, findings, schemas.executor, invoker, env);
|
|
49
95
|
if (state.status !== "running")
|
|
50
96
|
break;
|
|
97
|
+
state = pauseAtSafeBoundary(contract, state, env);
|
|
98
|
+
if (state.status !== "running")
|
|
99
|
+
break;
|
|
100
|
+
if (!state.lastExecutorResult ||
|
|
101
|
+
hasUndeliveredHumanGuidance(contract, state)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
51
104
|
}
|
|
52
|
-
state =
|
|
53
|
-
|
|
54
|
-
break;
|
|
55
|
-
contract = loadManagedTask(projectRoot, taskId);
|
|
56
|
-
validateManagedTaskContract(contract);
|
|
105
|
+
state = prepareExternalHostReview(contract, state, env);
|
|
106
|
+
break;
|
|
57
107
|
}
|
|
58
108
|
return state;
|
|
59
109
|
}
|
|
60
110
|
catch (error) {
|
|
61
111
|
const message = error.message;
|
|
112
|
+
const failure = classifyManagedFailure(contract, error);
|
|
113
|
+
if (failure) {
|
|
114
|
+
state.failure = failure;
|
|
115
|
+
saveManagedRun(state);
|
|
116
|
+
appendManagedEvent(state, {
|
|
117
|
+
eventType: "run.failure_classified",
|
|
118
|
+
actor: "managed-runner",
|
|
119
|
+
role: "runner",
|
|
120
|
+
summary: `${failure.reason}: ${failure.remediation}`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
62
123
|
if (state.activeSince) {
|
|
63
124
|
state.activeRunMilliseconds += Math.max(0, Date.now() - Date.parse(state.activeSince));
|
|
64
125
|
state.activeSince = null;
|
|
65
126
|
saveManagedRun(state);
|
|
66
127
|
}
|
|
128
|
+
const pauseSignal = readManagedControlSignal(contract);
|
|
129
|
+
if (pauseSignal?.pauseRequested) {
|
|
130
|
+
clearManagedControlSignal(contract);
|
|
131
|
+
if (state.currentStep.startsWith("executor_")) {
|
|
132
|
+
state.executorInvocationCredits =
|
|
133
|
+
(state.executorInvocationCredits ?? 0) + 1;
|
|
134
|
+
state.totalAgentInvocationCredits =
|
|
135
|
+
(state.totalAgentInvocationCredits ?? 0) + 1;
|
|
136
|
+
}
|
|
137
|
+
state.runningAgentPid = null;
|
|
138
|
+
state.blocker = pauseSignal.reason;
|
|
139
|
+
state = transition(contract, state, "paused", "paused_by_user", env);
|
|
140
|
+
appendManagedEvent(state, {
|
|
141
|
+
eventType: "human.pause_applied",
|
|
142
|
+
actor: pauseSignal.actor,
|
|
143
|
+
role: "system",
|
|
144
|
+
summary: mt(contract, `已中断当前研发 Agent 并暂停任务:${pauseSignal.reason}`, `Interrupted the current executor and paused the task: ${pauseSignal.reason}`),
|
|
145
|
+
});
|
|
146
|
+
appendManagedEvent(state, {
|
|
147
|
+
eventType: "budget.interrupted_invocation_credited",
|
|
148
|
+
actor: "managed-runner",
|
|
149
|
+
role: "system",
|
|
150
|
+
summary: mt(contract, "用户暂停中断的研发调用已抵扣,不消耗有效业务预算", "The executor invocation interrupted by user pause was credited and does not consume effective business budget"),
|
|
151
|
+
});
|
|
152
|
+
return state;
|
|
153
|
+
}
|
|
154
|
+
const unresumableSession = unresumableExecutorSessionFailure(error, state);
|
|
155
|
+
if (unresumableSession) {
|
|
156
|
+
state.executorSession = retireExecutorSession(state.executorSession, unresumableSession);
|
|
157
|
+
appendManagedEvent(state, {
|
|
158
|
+
eventType: "executor.session_retired",
|
|
159
|
+
actor: "managed-runner",
|
|
160
|
+
role: "runner",
|
|
161
|
+
summary: mt(contract, `研发会话无法安全续接,已退役旧会话:${unresumableSession}`, `The executor session cannot be safely resumed and was retired: ${unresumableSession}`),
|
|
162
|
+
});
|
|
163
|
+
if (unresumableSessionRecoveryCount(state) >= 1) {
|
|
164
|
+
appendManagedEvent(state, {
|
|
165
|
+
eventType: "executor.session_recovery_circuit_open",
|
|
166
|
+
actor: "managed-runner",
|
|
167
|
+
role: "runner",
|
|
168
|
+
summary: mt(contract, "研发会话第二次无法安全续接,停止自动创建新会话并转 Host 处理", "The executor session was unresumable for a second time; fresh-session recovery stopped and the task was escalated to Host"),
|
|
169
|
+
});
|
|
170
|
+
return finishBlocked(contract, state, mt(contract, "研发会话第二次无法安全续接;已保留退役记录和原始失败证据,请由 Host 检查 Agent 或切换供应商", "The executor session was unresumable for a second time; retirement records and raw failure evidence were preserved. Ask Host to inspect the agent or switch provider"), env);
|
|
171
|
+
}
|
|
172
|
+
state.runningAgentPid = null;
|
|
173
|
+
state.blocker = null;
|
|
174
|
+
state = transition(contract, state, "queued", "executor_session_recovery_queued", env);
|
|
175
|
+
appendManagedEvent(state, {
|
|
176
|
+
eventType: "executor.session_recovery_queued",
|
|
177
|
+
actor: "managed-runner",
|
|
178
|
+
role: "runner",
|
|
179
|
+
summary: mt(contract, "已保留冻结交接包和有效证据,并仅排队一次不带旧 session 的新会话恢复", "The frozen handoff and valid evidence were preserved; exactly one fresh recovery without the old session was queued"),
|
|
180
|
+
});
|
|
181
|
+
return state;
|
|
182
|
+
}
|
|
183
|
+
if (isMaxTurnContinuationFailure(message)) {
|
|
184
|
+
const continuation = consecutiveMaxTurnFailures(state);
|
|
185
|
+
const hasWorkspaceProgress = changedWorkspaceFilesSinceBaseline(contract, state).length > 0;
|
|
186
|
+
if (!hasWorkspaceProgress && continuation >= 3) {
|
|
187
|
+
state.executorSession = rolloverSession(state.executorSession);
|
|
188
|
+
return finishBlocked(contract, state, mt(contract, "研发 Agent 连续三次因旧版或外部 max-turn 限制退出且零源码进展,已熔断;请检查执行模型能力、Prompt 规模或外部 CLI 配置", "The executor exited three times because of a legacy or external max-turn limit without workspace progress; the circuit opened. Check model capability, prompt size, or external CLI configuration"), env);
|
|
189
|
+
}
|
|
190
|
+
if (continuation < 3 && state.executorSession.sessionId) {
|
|
191
|
+
if (!hasWorkspaceProgress) {
|
|
192
|
+
state.executorSession = rolloverSession(state.executorSession);
|
|
193
|
+
}
|
|
194
|
+
state.runningAgentPid = null;
|
|
195
|
+
state.blocker = null;
|
|
196
|
+
state = transition(contract, state, "queued", "executor_continuation_queued", env);
|
|
197
|
+
appendManagedEvent(state, {
|
|
198
|
+
eventType: "executor.continuation_queued",
|
|
199
|
+
actor: "managed-runner",
|
|
200
|
+
role: "runner",
|
|
201
|
+
summary: mt(contract, hasWorkspaceProgress
|
|
202
|
+
? `研发会话受到旧版或外部 max-turn 限制,检测到源码进展,自动续接同一会话(${continuation}/3)`
|
|
203
|
+
: `研发会话受到旧版或外部 max-turn 限制且零源码进展,切换压缩短会话继续(${continuation}/3)`, hasWorkspaceProgress
|
|
204
|
+
? `Executor hit a legacy or external max-turn limit with workspace progress; automatically resuming the same session (${continuation}/3)`
|
|
205
|
+
: `Executor hit a legacy or external max-turn limit with no workspace progress; continuing in a fresh condensed session (${continuation}/3)`),
|
|
206
|
+
});
|
|
207
|
+
return state;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (isExecutorDeliveryProtocolFailure(message)) {
|
|
211
|
+
const recoveryCount = deliveryProtocolRecoveryCount(state);
|
|
212
|
+
if (recoveryCount < 1) {
|
|
213
|
+
state.executorSession = rolloverSession(state.executorSession);
|
|
214
|
+
state.runningAgentPid = null;
|
|
215
|
+
state.blocker = null;
|
|
216
|
+
state = transition(contract, state, "queued", "executor_delivery_protocol_recovery_queued", env);
|
|
217
|
+
appendManagedEvent(state, {
|
|
218
|
+
eventType: "executor.delivery_protocol_recovery_queued",
|
|
219
|
+
actor: "managed-runner",
|
|
220
|
+
role: "runner",
|
|
221
|
+
summary: mt(contract, "研发 Agent 已完成调用但未按协议返回 JSON;保留原始日志并自动创建一次新的压缩会话补齐交付与受影响验证(1/1)", "The executor completed its call but did not return protocol JSON; raw logs were preserved and one fresh condensed session was queued to complete delivery and affected verification (1/1)"),
|
|
222
|
+
});
|
|
223
|
+
return state;
|
|
224
|
+
}
|
|
225
|
+
return finishBlocked(contract, state, mt(contract, "研发 Agent 连续两次未按协议返回 JSON;已保留原始日志并停止自动恢复,请检查 Agent/适配器输出", "The executor failed to return protocol JSON twice; raw logs were preserved and automatic recovery stopped. Check the agent or adapter output"), env);
|
|
226
|
+
}
|
|
227
|
+
if (isPermanentProviderFailure(message)) {
|
|
228
|
+
state = transition(contract, state, "waiting_for_provider_change", "provider_change_required", env);
|
|
229
|
+
state.blocker = message;
|
|
230
|
+
saveManagedRun(state);
|
|
231
|
+
appendManagedEvent(state, {
|
|
232
|
+
eventType: "provider.permanent_failure",
|
|
233
|
+
actor: contract.executorAgent,
|
|
234
|
+
role: "system",
|
|
235
|
+
summary: mt(contract, `供应商额度或套餐不可用,禁止自动重试:${message}`, `Provider quota or plan is unavailable; automatic retry is forbidden: ${message}`),
|
|
236
|
+
});
|
|
237
|
+
notifyManagedTask({
|
|
238
|
+
taskId: state.taskId,
|
|
239
|
+
type: "provider_change",
|
|
240
|
+
title: mt(contract, "Superflow 需要切换模型供应商", "Superflow requires a provider change"),
|
|
241
|
+
message: `${state.taskId}: ${message}`,
|
|
242
|
+
}, env);
|
|
243
|
+
return state;
|
|
244
|
+
}
|
|
67
245
|
if (isConnectivityFailure(message)) {
|
|
68
|
-
|
|
246
|
+
const connectivityFailure = recordExecutorConnectivityFailure(contract, state, message);
|
|
247
|
+
const circuitOpen = connectivityFailure !== null &&
|
|
248
|
+
connectivityFailure.consecutiveFailures >= 3;
|
|
249
|
+
state = transition(contract, state, circuitOpen ? "waiting_for_human" : "waiting_for_connectivity", circuitOpen ? "provider_change_required" : "connectivity_wait", env);
|
|
69
250
|
state.blocker = message;
|
|
70
251
|
saveManagedRun(state);
|
|
71
252
|
appendManagedEvent(state, {
|
|
@@ -74,6 +255,22 @@ export async function runManagedTask(projectRoot, taskId, invoker, env = process
|
|
|
74
255
|
role: "system",
|
|
75
256
|
summary: mt(contract, `网络或模型服务暂时不可用:${message}`, `Network or model service is temporarily unavailable: ${message}`),
|
|
76
257
|
});
|
|
258
|
+
if (connectivityFailure?.credited) {
|
|
259
|
+
appendManagedEvent(state, {
|
|
260
|
+
eventType: "budget.transient_provider_credited",
|
|
261
|
+
actor: "managed-runner",
|
|
262
|
+
role: "system",
|
|
263
|
+
summary: mt(contract, `临时供应商错误不计入 Claude 预算:返还第 ${connectivityFailure.invocation} 次物理调用,有效执行抵扣 ${state.executorInvocationCredits} 次${connectivityFailure.stopAt === null ? "" : `,临时窗口顺延至 ${connectivityFailure.stopAt}`};连续失败 ${connectivityFailure.consecutiveFailures}/3`, `Transient provider failure excluded from the Claude budget: credited physical invocation ${connectivityFailure.invocation}, effective executor credits ${state.executorInvocationCredits}${connectivityFailure.stopAt === null ? "" : `, temporary window extended to ${connectivityFailure.stopAt}`}; consecutive failures ${connectivityFailure.consecutiveFailures}/3`),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
if (circuitOpen) {
|
|
267
|
+
appendManagedEvent(state, {
|
|
268
|
+
eventType: "connectivity.circuit_open",
|
|
269
|
+
actor: "managed-runner",
|
|
270
|
+
role: "system",
|
|
271
|
+
summary: mt(contract, "临时供应商错误已连续发生 3 次,停止自动重试并等待切换供应商", "Transient provider failures occurred 3 consecutive times; automatic retries stopped pending a provider change"),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
77
274
|
return state;
|
|
78
275
|
}
|
|
79
276
|
return finishBlocked(contract, state, message, env);
|
|
@@ -84,6 +281,33 @@ export async function runManagedTask(projectRoot, taskId, invoker, env = process
|
|
|
84
281
|
lock.release();
|
|
85
282
|
}
|
|
86
283
|
}
|
|
284
|
+
function migrateLegacyDataDisclosure(contract, state) {
|
|
285
|
+
if (contract.permissions.externalModelDataDisclosure)
|
|
286
|
+
return false;
|
|
287
|
+
contract.permissions.externalModelDataDisclosure = {
|
|
288
|
+
approved: state.executorInvocations > 0,
|
|
289
|
+
approvedBy: state.executorInvocations > 0 ? "legacy_migration" : null,
|
|
290
|
+
approvedAt: state.executorInvocations > 0 ? contract.createdAt : null,
|
|
291
|
+
scope: [contract.projectRoot, ...contract.relatedProjectRoots],
|
|
292
|
+
};
|
|
293
|
+
contract.contractHash = calculateManagedContractHash(contract);
|
|
294
|
+
state.contractHash = contract.contractHash;
|
|
295
|
+
saveManagedTask(contract);
|
|
296
|
+
saveManagedRun(state);
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
function migrateLegacySupervision(contract, state) {
|
|
300
|
+
const legacy = contract;
|
|
301
|
+
if (legacy.supervisorExecution === "external_host")
|
|
302
|
+
return false;
|
|
303
|
+
legacy.supervisorExecution = "external_host";
|
|
304
|
+
contract.contractHash = calculateManagedContractHash(contract);
|
|
305
|
+
state.contractHash = contract.contractHash;
|
|
306
|
+
state.supervisorSession = rolloverSession(state.supervisorSession);
|
|
307
|
+
saveManagedTask(contract);
|
|
308
|
+
saveManagedRun(state);
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
87
311
|
function reconcileCompletedReview(contract, state, env) {
|
|
88
312
|
if (!state.lastReviewResult || !existsSync(state.lastReviewResult))
|
|
89
313
|
return state;
|
|
@@ -104,36 +328,166 @@ function reconcileCompletedReview(contract, state, env) {
|
|
|
104
328
|
});
|
|
105
329
|
}
|
|
106
330
|
if (result.result === "pass") {
|
|
107
|
-
|
|
331
|
+
try {
|
|
332
|
+
state = promoteRevalidatedExecutorResult(contract, state);
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
return finishBlocked(contract, state, mt(contract, `Host 评审通过,但最后一次执行结果按当前规则重验仍失败:${error.message}`, `The host review passed, but the latest executor result still failed current deterministic validation: ${error.message}`), env);
|
|
336
|
+
}
|
|
337
|
+
return finishDeliveryReady(contract, state, result.summary, env);
|
|
108
338
|
}
|
|
109
339
|
if (result.result === "blocked") {
|
|
110
340
|
return finishBlocked(contract, state, result.summary, env);
|
|
111
341
|
}
|
|
112
|
-
|
|
113
|
-
|
|
342
|
+
const convergence = assessReviewConvergence(readReviewHistory(state));
|
|
343
|
+
if (!convergence.shouldContinue) {
|
|
344
|
+
state.blocker = mt(contract, `连续 ${convergence.stagnantTransitions} 次评审转换没有关闭既有阻断项,已提前止损并保留整改证据`, `${convergence.stagnantTransitions} consecutive review transitions closed no existing blocker; repair stopped early with evidence preserved`);
|
|
345
|
+
state = transition(contract, state, "repair_pending", "repair_convergence_stalled", env);
|
|
346
|
+
appendManagedEvent(state, {
|
|
347
|
+
eventType: "review.convergence_stalled",
|
|
348
|
+
actor: "managed-runner",
|
|
349
|
+
role: "runner",
|
|
350
|
+
summary: state.blocker ?? "review convergence stalled",
|
|
351
|
+
});
|
|
352
|
+
return state;
|
|
114
353
|
}
|
|
115
|
-
if (
|
|
116
|
-
state.
|
|
117
|
-
state.
|
|
118
|
-
|
|
354
|
+
if (!hasUnlimitedAgentBudget(contract) &&
|
|
355
|
+
state.reviewRound >= contract.budgets.maxReviewRounds) {
|
|
356
|
+
state.blocker = mt(contract, "仍有阻断 finding,但当前评审预算已达上限;整改结论已保留,等待用户扩展预算", "Blocking findings remain but the review budget is exhausted; the repair decision is preserved pending a user budget extension");
|
|
357
|
+
state = transition(contract, state, "repair_pending", "repair_budget_approval_required", env);
|
|
358
|
+
appendManagedEvent(state, {
|
|
359
|
+
eventType: "review.repair_pending",
|
|
360
|
+
actor: "managed-runner",
|
|
361
|
+
role: "runner",
|
|
362
|
+
summary: state.blocker ?? "repair pending",
|
|
363
|
+
});
|
|
364
|
+
return state;
|
|
365
|
+
}
|
|
366
|
+
state.lastExecutorResult = null;
|
|
367
|
+
state.currentStep = "repair_prompt_ready";
|
|
368
|
+
saveManagedRun(state);
|
|
369
|
+
appendManagedEvent(state, {
|
|
370
|
+
eventType: "recovery.checkpoint_restored",
|
|
371
|
+
actor: "managed-runner",
|
|
372
|
+
role: "runner",
|
|
373
|
+
summary: mt(contract, "根据已落盘的整改结论恢复到执行者整改步骤", "Restored the executor repair step from persisted review findings"),
|
|
374
|
+
});
|
|
375
|
+
return state;
|
|
376
|
+
}
|
|
377
|
+
function readReviewHistory(state) {
|
|
378
|
+
const runDir = managedRunDir(state.projectRoot, state.taskId, state.runId);
|
|
379
|
+
if (!existsSync(runDir))
|
|
380
|
+
return [];
|
|
381
|
+
return readdirSync(runDir)
|
|
382
|
+
.filter((name) => /^review-result-\d+\.json$/.test(name))
|
|
383
|
+
.sort((left, right) => reviewFileRound(left) - reviewFileRound(right))
|
|
384
|
+
.flatMap((name) => {
|
|
385
|
+
try {
|
|
386
|
+
return [
|
|
387
|
+
JSON.parse(readFileSync(path.join(runDir, name), "utf-8")),
|
|
388
|
+
];
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
return [];
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
function reviewFileRound(name) {
|
|
396
|
+
return Number.parseInt(name.match(/\d+/)?.[0] ?? "0", 10);
|
|
397
|
+
}
|
|
398
|
+
function promoteRevalidatedExecutorResult(contract, state) {
|
|
399
|
+
const current = state.lastExecutorResult;
|
|
400
|
+
if (!current?.endsWith("-invalid.json"))
|
|
401
|
+
return state;
|
|
402
|
+
const result = JSON.parse(readFileSync(current, "utf-8"));
|
|
403
|
+
const historical = historicalEngineeringCommands(state, true);
|
|
404
|
+
const reusableHistorical = historical.filter((command) => !isBlockingCommandFailure(command) && isCommandOutcomeConsistent(command));
|
|
405
|
+
const currentCommandCount = result.commands.length;
|
|
406
|
+
const reusableCurrent = result.commands.filter((command) => !isBlockingCommandFailure(command) && isCommandOutcomeConsistent(command));
|
|
407
|
+
result.commands = uniqueCommands([...reusableHistorical, ...reusableCurrent]);
|
|
408
|
+
promoteHostApprovedVerificationMetadata(contract, state, result, current);
|
|
409
|
+
const filteredCount = historical.length -
|
|
410
|
+
reusableHistorical.length +
|
|
411
|
+
(currentCommandCount - reusableCurrent.length);
|
|
412
|
+
if (filteredCount > 0) {
|
|
119
413
|
appendManagedEvent(state, {
|
|
120
|
-
eventType: "
|
|
414
|
+
eventType: "executor.historical_evidence_filtered",
|
|
121
415
|
actor: "managed-runner",
|
|
122
416
|
role: "runner",
|
|
123
|
-
summary: mt(contract,
|
|
417
|
+
summary: mt(contract, `按当前规则过滤 ${filteredCount} 条失效历史命令证据`, `Filtered ${filteredCount} stale historical command evidence item(s) under current rules`),
|
|
418
|
+
evidencePaths: [current],
|
|
124
419
|
});
|
|
125
420
|
}
|
|
421
|
+
validateWorkspaceSecrets(contract, state);
|
|
422
|
+
validateExecutorResult(contract, state, result, { hostApproved: true });
|
|
423
|
+
const accepted = current.replace(/-invalid\.json$/, ".json");
|
|
424
|
+
writeJsonAtomic(accepted, result);
|
|
425
|
+
state.lastExecutorResult = accepted;
|
|
426
|
+
state.deliveryProgress = evaluateCompletion(contract, result).progress;
|
|
427
|
+
saveManagedRun(state);
|
|
428
|
+
appendManagedEvent(state, {
|
|
429
|
+
eventType: "executor.result_revalidated",
|
|
430
|
+
actor: "managed-runner",
|
|
431
|
+
role: "runner",
|
|
432
|
+
summary: mt(contract, "最后一次执行结果已按当前确定性规则重验并晋升为正式证据", "The latest executor result passed current deterministic validation and was promoted to accepted evidence"),
|
|
433
|
+
evidencePaths: [current, accepted],
|
|
434
|
+
});
|
|
126
435
|
return state;
|
|
127
436
|
}
|
|
437
|
+
function promoteHostApprovedVerificationMetadata(contract, state, result, invalidResultPath) {
|
|
438
|
+
const escalated = readManagedEvents(state).some((event) => event.eventType === "executor.verification_metadata_escalated_to_host" &&
|
|
439
|
+
event.evidencePaths.includes(invalidResultPath));
|
|
440
|
+
if (!escalated)
|
|
441
|
+
return;
|
|
442
|
+
const command = result.commands.find((item) => item.exitCode === 0 &&
|
|
443
|
+
item.categories?.includes("startup") &&
|
|
444
|
+
item.categories.includes("runtime"));
|
|
445
|
+
if (!command)
|
|
446
|
+
return;
|
|
447
|
+
command.categories = uniqueCategories([
|
|
448
|
+
...(command.categories ?? []),
|
|
449
|
+
"invocation",
|
|
450
|
+
]);
|
|
451
|
+
appendManagedEvent(state, {
|
|
452
|
+
eventType: "executor.verification_metadata_host_promoted",
|
|
453
|
+
actor: contract.supervisorAgent,
|
|
454
|
+
role: "supervisor",
|
|
455
|
+
summary: mt(contract, "Host 已核对原始运行证据并批准把缺失的真实调用类别审计式晋升,未重跑研发环境", "The Host inspected the raw runtime evidence and approved audit promotion of the missing real-invocation category without rerunning the engineering environment"),
|
|
456
|
+
evidencePaths: [invalidResultPath],
|
|
457
|
+
});
|
|
458
|
+
}
|
|
128
459
|
async function executeWorker(contract, state, findings, schemaPath, invoker, env) {
|
|
129
|
-
|
|
460
|
+
const deliveryProtocolRecovery = state.currentStep === "executor_delivery_protocol_recovery_queued";
|
|
461
|
+
if (contract.budgets.executorPhysicalStopAt !== undefined &&
|
|
462
|
+
state.executorInvocations >= contract.budgets.executorPhysicalStopAt) {
|
|
463
|
+
return finishBudgetExhausted(contract, state, mt(contract, "Claude 临时调用窗口已用尽", "The temporary Claude invocation window is exhausted"), env);
|
|
464
|
+
}
|
|
465
|
+
const hasActiveExecutorWindow = contract.budgets.executorPhysicalStopAt !== undefined &&
|
|
466
|
+
state.executorInvocations < contract.budgets.executorPhysicalStopAt;
|
|
467
|
+
if (!hasActiveExecutorWindow &&
|
|
468
|
+
!hasUnlimitedAgentBudget(contract) &&
|
|
469
|
+
effectiveExecutorInvocations(state) >=
|
|
470
|
+
contract.budgets.maxExecutorInvocations) {
|
|
130
471
|
return finishBudgetExhausted(contract, state, mt(contract, "执行调用次数达到上限", "Executor invocation limit reached"), env);
|
|
131
472
|
}
|
|
132
|
-
if (
|
|
473
|
+
if (!hasActiveExecutorWindow &&
|
|
474
|
+
!hasUnlimitedAgentBudget(contract) &&
|
|
475
|
+
effectiveTotalAgentInvocations(state) >=
|
|
476
|
+
contract.budgets.maxTotalAgentInvocations) {
|
|
133
477
|
return finishBudgetExhausted(contract, state, mt(contract, "总 Agent 调用次数达到上限", "Total Agent invocation limit reached"), env);
|
|
134
478
|
}
|
|
479
|
+
const resumableSessionId = maxTurnSessionToResume(state);
|
|
480
|
+
if (!resumableSessionId) {
|
|
481
|
+
state.executorSession = rolloverSession(state.executorSession);
|
|
482
|
+
}
|
|
483
|
+
state.executorSession = {
|
|
484
|
+
...state.executorSession,
|
|
485
|
+
resumingSessionId: resumableSessionId,
|
|
486
|
+
};
|
|
135
487
|
state.executorInvocations += 1;
|
|
136
488
|
state.totalAgentInvocations += 1;
|
|
489
|
+
state.executorStage = initialExecutorStage(findings, state.executorStage);
|
|
490
|
+
state.executorActiveStage = state.executorStage;
|
|
137
491
|
state.currentStep =
|
|
138
492
|
findings.length > 0 ? "executor_repairing" : "executor_implementing";
|
|
139
493
|
state.activeSince = new Date().toISOString();
|
|
@@ -144,15 +498,26 @@ async function executeWorker(contract, state, findings, schemaPath, invoker, env
|
|
|
144
498
|
role: "executor",
|
|
145
499
|
summary: mt(contract, `开始第 ${state.executorInvocations} 次执行调用`, `Started executor invocation ${state.executorInvocations}`),
|
|
146
500
|
});
|
|
147
|
-
|
|
501
|
+
appendExecutorStageEvent(contract, state, state.executorStage);
|
|
502
|
+
const handoffFile = writeManagedExecutorHandoff(contract, state, findings);
|
|
503
|
+
const prompt = buildExecutorPrompt(contract, state, findings, handoffFile);
|
|
148
504
|
const promptFile = path.join(managedRunDir(state.projectRoot, state.taskId, state.runId), findings.length > 0
|
|
149
505
|
? `repair-${state.reviewRound}.md`
|
|
150
506
|
: "executor-initial-prompt.md");
|
|
151
507
|
writeFileSync(promptFile, prompt, "utf-8");
|
|
152
508
|
state.lastRepairPrompt =
|
|
153
509
|
findings.length > 0 ? promptFile : state.lastRepairPrompt;
|
|
510
|
+
appendManagedEvent(state, {
|
|
511
|
+
eventType: "executor.dispatch_recorded",
|
|
512
|
+
actor: "managed-runner",
|
|
513
|
+
role: "runner",
|
|
514
|
+
summary: mt(contract, `已下发第 ${state.executorInvocations} 次执行,handoff hash=${fileHash(handoffFile)}`, `Recorded executor dispatch ${state.executorInvocations}, handoff hash=${fileHash(handoffFile)}`),
|
|
515
|
+
evidencePaths: [handoffFile, promptFile],
|
|
516
|
+
});
|
|
154
517
|
const started = Date.now();
|
|
155
|
-
const result = await invoker
|
|
518
|
+
const result = await invokeWithFailureEvidence(state, invoker, invocation(contract, state, prompt, promptFile, schemaPath, resumableSessionId, deliveryProtocolRecovery, (sessionId) => {
|
|
519
|
+
if (!acceptsExecutorCallback(state))
|
|
520
|
+
return;
|
|
156
521
|
state.executorSession = {
|
|
157
522
|
...state.executorSession,
|
|
158
523
|
sessionId,
|
|
@@ -161,25 +526,83 @@ async function executeWorker(contract, state, findings, schemaPath, invoker, env
|
|
|
161
526
|
lastResumedRound: state.reviewRound,
|
|
162
527
|
};
|
|
163
528
|
saveManagedRun(state);
|
|
164
|
-
|
|
165
|
-
eventType
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
529
|
+
recordHumanGuidanceHandoffDelivery(contract, state, handoffFile, sessionId);
|
|
530
|
+
if (!readManagedEvents(state).some((event) => event.eventType === "executor.handoff_acknowledged" &&
|
|
531
|
+
event.evidencePaths.includes(handoffFile))) {
|
|
532
|
+
appendManagedEvent(state, {
|
|
533
|
+
eventType: "executor.handoff_acknowledged",
|
|
534
|
+
actor: contract.executorAgent,
|
|
535
|
+
role: "executor",
|
|
536
|
+
summary: mt(contract, `Executor 已确认第 ${state.executorInvocations} 次 handoff 并返回会话标识`, `Executor acknowledged handoff for invocation ${state.executorInvocations} with a session identifier`),
|
|
537
|
+
evidencePaths: [handoffFile],
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
}, (summary) => recordExecutorProgress(contract, state, summary)), `executor-${state.executorInvocations}-failed`);
|
|
541
|
+
state.consecutiveTransientProviderFailures = 0;
|
|
170
542
|
state = closeActiveTime(state, started);
|
|
543
|
+
state.executorUsage = mergeAgentUsage(state.executorUsage, result.usage);
|
|
544
|
+
state.runtimeTelemetry = result.telemetry ?? state.runtimeTelemetry ?? null;
|
|
545
|
+
state.runningAgentPid = null;
|
|
171
546
|
state.executorSession = {
|
|
172
547
|
...state.executorSession,
|
|
173
548
|
sessionId: result.sessionId,
|
|
174
549
|
createdAt: state.executorSession.createdAt ?? new Date().toISOString(),
|
|
175
550
|
status: "active",
|
|
176
551
|
lastResumedRound: state.reviewRound,
|
|
552
|
+
resumingSessionId: null,
|
|
177
553
|
};
|
|
178
|
-
validateExecutorResult(contract, result.output);
|
|
179
554
|
const resultFile = path.join(managedRunDir(state.projectRoot, state.taskId, state.runId), `executor-result-${state.executorInvocations}.json`);
|
|
555
|
+
let finalPreflightLogs = [];
|
|
556
|
+
appendManagedEvent(state, {
|
|
557
|
+
eventType: "executor.delivery_received",
|
|
558
|
+
actor: "managed-runner",
|
|
559
|
+
role: "runner",
|
|
560
|
+
summary: mt(contract, `Runner 已接收第 ${state.executorInvocations} 次交付,开始确定性校验`, `Runner received executor delivery ${state.executorInvocations} and started deterministic validation`),
|
|
561
|
+
});
|
|
562
|
+
try {
|
|
563
|
+
result.output = redactManagedValue(normalizeExecutorResult(contract, state, result.output));
|
|
564
|
+
validateInvocationTelemetry(contract, state, result, result.output);
|
|
565
|
+
validateWorkspaceSecrets(contract, state);
|
|
566
|
+
validateExecutorResult(contract, state, result.output);
|
|
567
|
+
finalPreflightLogs = runExecutorFinalPreflight(contract, state, result.output);
|
|
568
|
+
}
|
|
569
|
+
catch (error) {
|
|
570
|
+
const invalidFile = path.join(managedRunDir(state.projectRoot, state.taskId, state.runId), `executor-result-${state.executorInvocations}-invalid.json`);
|
|
571
|
+
writeJsonAtomic(invalidFile, result.output);
|
|
572
|
+
const invalidLogs = writeInvocationLogs(state, `executor-${state.executorInvocations}-invalid`, result.stdout, result.stderr);
|
|
573
|
+
const gateLogs = error instanceof ExecutorFinalPreflightError ? error.evidencePaths : [];
|
|
574
|
+
appendManagedEvent(state, {
|
|
575
|
+
eventType: "executor.result_rejected",
|
|
576
|
+
actor: contract.executorAgent,
|
|
577
|
+
role: "executor",
|
|
578
|
+
summary: error.message,
|
|
579
|
+
evidencePaths: [
|
|
580
|
+
invalidFile,
|
|
581
|
+
...invalidLogs,
|
|
582
|
+
...finalPreflightLogs,
|
|
583
|
+
...gateLogs,
|
|
584
|
+
],
|
|
585
|
+
});
|
|
586
|
+
if (error instanceof VerificationMetadataAmbiguityError) {
|
|
587
|
+
state.lastExecutorResult = invalidFile;
|
|
588
|
+
state.lastReviewResult = null;
|
|
589
|
+
state.currentStep = "verification_metadata_host_review_required";
|
|
590
|
+
saveManagedRun(state);
|
|
591
|
+
appendManagedEvent(state, {
|
|
592
|
+
eventType: "executor.verification_metadata_escalated_to_host",
|
|
593
|
+
actor: "managed-runner",
|
|
594
|
+
role: "runner",
|
|
595
|
+
summary: mt(contract, `运行证据类别存在结构化歧义,保留原始证据交 Host 语义判断,不启动完整 Executor 整改:${error.message}`, `Verification categories are structurally ambiguous; raw evidence was preserved for Host semantic review without starting a full Executor repair: ${error.message}`),
|
|
596
|
+
evidencePaths: [invalidFile, ...invalidLogs],
|
|
597
|
+
});
|
|
598
|
+
return state;
|
|
599
|
+
}
|
|
600
|
+
return queueExecutorSelfRepair(contract, state, error.message, invalidFile);
|
|
601
|
+
}
|
|
180
602
|
writeJsonAtomic(resultFile, result.output);
|
|
181
603
|
const logFiles = writeInvocationLogs(state, `executor-${state.executorInvocations}`, result.stdout, result.stderr);
|
|
182
604
|
state.lastExecutorResult = resultFile;
|
|
605
|
+
state.deliveryProgress = evaluateCompletion(contract, result.output).progress;
|
|
183
606
|
state.currentStep = "delivery_evidence_checking";
|
|
184
607
|
state.workspaceFingerprint = computeWorkspaceFingerprintForRoots([
|
|
185
608
|
contract.projectRoot,
|
|
@@ -188,109 +611,838 @@ async function executeWorker(contract, state, findings, schemaPath, invoker, env
|
|
|
188
611
|
saveManagedRun(state);
|
|
189
612
|
appendTaskReport(state, executorReportSection(state, result.output));
|
|
190
613
|
appendManagedEvent(state, {
|
|
191
|
-
eventType: "executor.ended",
|
|
614
|
+
eventType: "executor.ended",
|
|
615
|
+
actor: contract.executorAgent,
|
|
616
|
+
role: "executor",
|
|
617
|
+
summary: result.output.summary,
|
|
618
|
+
evidencePaths: [
|
|
619
|
+
resultFile,
|
|
620
|
+
...logFiles,
|
|
621
|
+
...finalPreflightLogs,
|
|
622
|
+
...result.output.evidence,
|
|
623
|
+
],
|
|
624
|
+
});
|
|
625
|
+
const completionGap = executorCompletionGap(contract, result.output);
|
|
626
|
+
if (result.output.status === "ready_for_review" && completionGap) {
|
|
627
|
+
return queueExecutorSelfRepair(contract, state, completionGap, resultFile);
|
|
628
|
+
}
|
|
629
|
+
if (result.output.status !== "failed")
|
|
630
|
+
return state;
|
|
631
|
+
return finishBlocked(contract, state, managedList(contract.language, result.output.blockers, "", "") ||
|
|
632
|
+
result.output.summary, env);
|
|
633
|
+
}
|
|
634
|
+
function maxTurnSessionToResume(state) {
|
|
635
|
+
if (state.executorSession.status !== "active")
|
|
636
|
+
return null;
|
|
637
|
+
if (!state.executorSession.sessionId)
|
|
638
|
+
return null;
|
|
639
|
+
if (state.executorSession.retiredSessions?.some((session) => session.sessionId === state.executorSession.sessionId)) {
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
const terminal = readManagedEvents(state)
|
|
643
|
+
.slice()
|
|
644
|
+
.reverse()
|
|
645
|
+
.find((event) => ["executor.invocation_failed", "executor.ended"].includes(event.eventType));
|
|
646
|
+
if (terminal?.eventType !== "executor.invocation_failed" ||
|
|
647
|
+
!/max[_ -]?turns|maximum number of turns/i.test(terminal.summary)) {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
return state.executorSession.sessionId;
|
|
651
|
+
}
|
|
652
|
+
function isMaxTurnContinuationFailure(message) {
|
|
653
|
+
return /max[_ -]?turns|maximum number of turns/i.test(message);
|
|
654
|
+
}
|
|
655
|
+
function unresumableExecutorSessionFailure(error, state) {
|
|
656
|
+
const sessionId = state.executorSession.resumingSessionId;
|
|
657
|
+
if (!sessionId || state.executorSession.sessionId !== sessionId)
|
|
658
|
+
return null;
|
|
659
|
+
const raw = error;
|
|
660
|
+
return classifyUnresumableSessionFailure({
|
|
661
|
+
message: raw.message,
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
function unresumableSessionRecoveryCount(state) {
|
|
665
|
+
return readManagedEvents(state).filter((event) => event.eventType === "executor.session_recovery_queued").length;
|
|
666
|
+
}
|
|
667
|
+
function isExecutorDeliveryProtocolFailure(message) {
|
|
668
|
+
return /(?:Agent 最终输出不是 JSON|Agent final output is not JSON|Agent 未返回结构化结果|Agent did not return structured output|Agent 输出中缺少可恢复 session ID|Agent output is missing a resumable session ID)/i.test(message);
|
|
669
|
+
}
|
|
670
|
+
function deliveryProtocolRecoveryCount(state) {
|
|
671
|
+
return readManagedEvents(state).filter((event) => event.eventType === "executor.delivery_protocol_recovery_queued").length;
|
|
672
|
+
}
|
|
673
|
+
function consecutiveMaxTurnFailures(state) {
|
|
674
|
+
let count = 0;
|
|
675
|
+
for (const event of readManagedEvents(state).slice().reverse()) {
|
|
676
|
+
if ([
|
|
677
|
+
"executor.ended",
|
|
678
|
+
"human.pause_applied",
|
|
679
|
+
"human.guidance_resumed",
|
|
680
|
+
"human.resume_requested",
|
|
681
|
+
"provider.permanent_failure",
|
|
682
|
+
"connectivity.lost",
|
|
683
|
+
].includes(event.eventType)) {
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
if (event.eventType === "executor.invocation_failed" &&
|
|
687
|
+
isMaxTurnContinuationFailure(event.summary)) {
|
|
688
|
+
count += 1;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return count;
|
|
692
|
+
}
|
|
693
|
+
function recordExecutorProgress(contract, state, summary) {
|
|
694
|
+
if (!acceptsExecutorCallback(state))
|
|
695
|
+
return;
|
|
696
|
+
if (summary.startsWith("superflow-supervision:")) {
|
|
697
|
+
appendSupervisionCheckpoint(contract, state, summary);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
recordExecutorRework(contract, state, summary);
|
|
701
|
+
const explicitStage = explicitExecutorStage(summary);
|
|
702
|
+
if (explicitStage && explicitStage !== state.executorActiveStage) {
|
|
703
|
+
state.executorActiveStage = explicitStage;
|
|
704
|
+
saveManagedRun(state);
|
|
705
|
+
}
|
|
706
|
+
const stage = inferExecutorStage(summary, state.executorStage);
|
|
707
|
+
if (stage && stage !== state.executorStage) {
|
|
708
|
+
state.executorStage = stage;
|
|
709
|
+
saveManagedRun(state);
|
|
710
|
+
appendExecutorStageEvent(contract, state, stage);
|
|
711
|
+
}
|
|
712
|
+
const runDir = managedRunDir(state.projectRoot, state.taskId, state.runId);
|
|
713
|
+
const evidenceFile = path.join(runDir, `executor-progress-${state.executorInvocations}.jsonl`);
|
|
714
|
+
const persistedSummary = redactManagedLog(summary);
|
|
715
|
+
appendFileSync(evidenceFile, `${JSON.stringify({ timestamp: new Date().toISOString(), summary: persistedSummary })}\n`, "utf-8");
|
|
716
|
+
appendManagedEvent(state, {
|
|
717
|
+
eventType: "executor.progress",
|
|
718
|
+
actor: contract.executorAgent,
|
|
719
|
+
role: "executor",
|
|
720
|
+
summary: persistedSummary.slice(0, 240),
|
|
721
|
+
evidencePaths: [evidenceFile],
|
|
722
|
+
});
|
|
723
|
+
const fingerprint = computeWorkspaceFingerprintForRoots([
|
|
724
|
+
contract.projectRoot,
|
|
725
|
+
...contract.relatedProjectRoots,
|
|
726
|
+
]);
|
|
727
|
+
if (fingerprint === state.workspaceFingerprint)
|
|
728
|
+
return;
|
|
729
|
+
state.workspaceFingerprint = fingerprint;
|
|
730
|
+
saveManagedRun(state);
|
|
731
|
+
const diffStats = [contract.projectRoot, ...contract.relatedProjectRoots]
|
|
732
|
+
.map((root) => `${root}\n${safeGitOutput(root, ["diff", "--stat"]) || "无差异统计"}`)
|
|
733
|
+
.join("\n");
|
|
734
|
+
const workspaceFile = path.join(runDir, `workspace-change-${Date.now()}.txt`);
|
|
735
|
+
writeFileSync(workspaceFile, `${diffStats}\n`, "utf-8");
|
|
736
|
+
appendManagedEvent(state, {
|
|
737
|
+
eventType: "workspace.changed",
|
|
738
|
+
actor: contract.executorAgent,
|
|
739
|
+
role: "executor",
|
|
740
|
+
summary: mt(contract, "检测到目标工作区内容变化,完整 diff stat 已留档", "Detected target workspace changes; the full diff stat was persisted"),
|
|
741
|
+
evidencePaths: [workspaceFile],
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
function appendSupervisionCheckpoint(contract, state, summary) {
|
|
745
|
+
const events = readManagedEvents(state);
|
|
746
|
+
const latestAttention = events
|
|
747
|
+
.slice()
|
|
748
|
+
.reverse()
|
|
749
|
+
.find((event) => event.eventType === "executor.supervision_attention_required");
|
|
750
|
+
const previous = events
|
|
751
|
+
.slice()
|
|
752
|
+
.reverse()
|
|
753
|
+
.find((event) => event.sequence > (latestAttention?.sequence ?? 0) &&
|
|
754
|
+
(event.eventType === "executor.supervision_checkpoint" ||
|
|
755
|
+
event.eventType === "executor.supervision_checkpoint_idle"));
|
|
756
|
+
const supervisionWindowStart = previous?.sequence ?? latestAttention?.sequence ?? 0;
|
|
757
|
+
const recent = events.filter((event) => event.sequence > supervisionWindowStart);
|
|
758
|
+
const stageChanges = recent.filter((event) => ["executor.stage_changed", "executor.stage_rework"].includes(event.eventType)).length;
|
|
759
|
+
const commandCompletions = recent.filter((event) => event.eventType === "executor.progress" &&
|
|
760
|
+
/(?:background command|command).*(?:completed|exit code|完成|退出码)/i.test(event.summary)).length;
|
|
761
|
+
const workspaceChanges = recent.filter((event) => event.eventType === "workspace.changed").length;
|
|
762
|
+
const raw = summary.slice("superflow-supervision:".length);
|
|
763
|
+
let telemetry = raw;
|
|
764
|
+
try {
|
|
765
|
+
telemetry = JSON.stringify(JSON.parse(raw));
|
|
766
|
+
}
|
|
767
|
+
catch {
|
|
768
|
+
// Preserve bounded raw telemetry when an older adapter emits plain text.
|
|
769
|
+
}
|
|
770
|
+
const hasEffectiveMilestone = stageChanges + commandCompletions > 0;
|
|
771
|
+
appendManagedEvent(state, {
|
|
772
|
+
eventType: hasEffectiveMilestone
|
|
773
|
+
? "executor.supervision_checkpoint"
|
|
774
|
+
: "executor.supervision_checkpoint_idle",
|
|
775
|
+
actor: contract.executorAgent,
|
|
776
|
+
role: "executor",
|
|
777
|
+
summary: mt(contract, `10 分钟监督点:当前阶段 ${state.executorActiveStage ?? state.executorStage ?? "unknown"};已达里程碑 ${state.executorStage ?? "unknown"};阶段变化 ${stageChanges};命令完成 ${commandCompletions};工作区变化 ${workspaceChanges};遥测 ${telemetry}`, `10-minute supervision checkpoint: active stage ${state.executorActiveStage ?? state.executorStage ?? "unknown"}; reached milestone ${state.executorStage ?? "unknown"}; stage changes ${stageChanges}; command completions ${commandCompletions}; workspace changes ${workspaceChanges}; telemetry ${telemetry}`).slice(0, 500),
|
|
778
|
+
evidencePaths: [],
|
|
779
|
+
});
|
|
780
|
+
if (!hasEffectiveMilestone &&
|
|
781
|
+
previous?.eventType === "executor.supervision_checkpoint_idle") {
|
|
782
|
+
appendManagedEvent(state, {
|
|
783
|
+
eventType: "executor.supervision_attention_required",
|
|
784
|
+
actor: "managed-runner",
|
|
785
|
+
role: "runner",
|
|
786
|
+
summary: mt(contract, "连续两个 10 分钟监督点没有阶段推进或权威命令完成,需要 Host 轻量判断是否偏航", "Two consecutive 10-minute checkpoints had no stage advance or authoritative command completion; the Host must perform a lightweight drift check"),
|
|
787
|
+
evidencePaths: [],
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
function acceptsExecutorCallback(state) {
|
|
792
|
+
const persisted = loadManagedRun(state.projectRoot, state.taskId, state.runId);
|
|
793
|
+
return (persisted.status === "running" &&
|
|
794
|
+
persisted.executorInvocations === state.executorInvocations);
|
|
795
|
+
}
|
|
796
|
+
function recordExecutorRework(contract, state, summary) {
|
|
797
|
+
const current = state.executorStage;
|
|
798
|
+
const explicit = explicitExecutorStage(summary);
|
|
799
|
+
if (!current || !explicit || explicit === current)
|
|
800
|
+
return;
|
|
801
|
+
const order = [
|
|
802
|
+
"source_discovery",
|
|
803
|
+
"implementation",
|
|
804
|
+
"unit_test",
|
|
805
|
+
"package",
|
|
806
|
+
"application_startup",
|
|
807
|
+
"http_e2e",
|
|
808
|
+
"cleanup",
|
|
809
|
+
"delivery_self_check",
|
|
810
|
+
];
|
|
811
|
+
if (order.indexOf(explicit) >= order.indexOf(current))
|
|
812
|
+
return;
|
|
813
|
+
const summaryText = mt(contract, `Executor 在 ${current} 后回到 ${explicit} 修复并重跑受影响验证`, `Executor returned from ${current} to ${explicit} to repair and rerun affected validation`);
|
|
814
|
+
const events = readManagedEvents(state);
|
|
815
|
+
let duplicate = false;
|
|
816
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
817
|
+
const event = events[index];
|
|
818
|
+
if (event.eventType === "executor.stage_changed")
|
|
819
|
+
break;
|
|
820
|
+
if (event.eventType === "executor.stage_rework" &&
|
|
821
|
+
event.summary === summaryText) {
|
|
822
|
+
duplicate = true;
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if (duplicate)
|
|
827
|
+
return;
|
|
828
|
+
appendManagedEvent(state, {
|
|
829
|
+
eventType: "executor.stage_rework",
|
|
830
|
+
actor: contract.executorAgent,
|
|
831
|
+
role: "executor",
|
|
832
|
+
summary: summaryText,
|
|
833
|
+
evidencePaths: [],
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
function explicitExecutorStage(summary) {
|
|
837
|
+
return summary
|
|
838
|
+
.toLowerCase()
|
|
839
|
+
.match(/superflow-stage:(source_discovery|implementation|unit_test|package|application_startup|http_e2e|cleanup|delivery_self_check)/)?.[1];
|
|
840
|
+
}
|
|
841
|
+
function inferExecutorStage(summary, current) {
|
|
842
|
+
const value = summary.toLowerCase();
|
|
843
|
+
const explicit = value.match(/superflow-stage:(source_discovery|implementation|unit_test|package|application_startup|http_e2e|cleanup|delivery_self_check)/)?.[1];
|
|
844
|
+
if (explicit)
|
|
845
|
+
return advanceExecutorStage(current, explicit);
|
|
846
|
+
if (/structuredoutput|preflight|diff --check|交付前|final result/.test(value)) {
|
|
847
|
+
return advanceExecutorStage(current, "delivery_self_check");
|
|
848
|
+
}
|
|
849
|
+
if (/drop table|redis.*(?:del|exists)|kill|sigterm|pgrep|cleanup|清理|no listener|无监听/.test(value)) {
|
|
850
|
+
return advanceExecutorStage(current, "cleanup");
|
|
851
|
+
}
|
|
852
|
+
if (/curl|http|e2e|接口调用/.test(value)) {
|
|
853
|
+
return advanceExecutorStage(current, "http_e2e");
|
|
854
|
+
}
|
|
855
|
+
if (/java -jar|spring-boot:run|app(?:lication)? startup|startup.*ports?|wait for application|启动应用/.test(value)) {
|
|
856
|
+
return advanceExecutorStage(current, "application_startup");
|
|
857
|
+
}
|
|
858
|
+
if (/mvn.*package|gradle.*build|npm.*build|package jar|打包/.test(value)) {
|
|
859
|
+
return advanceExecutorStage(current, "package");
|
|
860
|
+
}
|
|
861
|
+
if (/mvn.*test|unit test|vitest|jest|pytest|单元测试/.test(value)) {
|
|
862
|
+
return advanceExecutorStage(current, "unit_test");
|
|
863
|
+
}
|
|
864
|
+
if (/\bedit\b|\bwrite\b|workspace_writing|修改源码|编码/.test(value)) {
|
|
865
|
+
return advanceExecutorStage(current, "implementation");
|
|
866
|
+
}
|
|
867
|
+
if (/\bread\b|\brg\b|\bgrep\b|搜索|检索/.test(value)) {
|
|
868
|
+
return current ?? "source_discovery";
|
|
869
|
+
}
|
|
870
|
+
return current;
|
|
871
|
+
}
|
|
872
|
+
function initialExecutorStage(findings, current) {
|
|
873
|
+
if (findings.length === 0)
|
|
874
|
+
return "source_discovery";
|
|
875
|
+
if (findings.some((finding) => /^(?:correctness|security|maintainability|style|code_quality|implementation)$/i.test(finding.category))) {
|
|
876
|
+
return "implementation";
|
|
877
|
+
}
|
|
878
|
+
const content = findings
|
|
879
|
+
.flatMap((finding) => [
|
|
880
|
+
finding.category,
|
|
881
|
+
finding.target,
|
|
882
|
+
finding.evidence,
|
|
883
|
+
finding.requiredFix,
|
|
884
|
+
...finding.acceptanceChecks,
|
|
885
|
+
])
|
|
886
|
+
.join("\n");
|
|
887
|
+
return inferRepairStage(content, current);
|
|
888
|
+
}
|
|
889
|
+
function inferRepairStage(content, current) {
|
|
890
|
+
const value = content.toLowerCase();
|
|
891
|
+
if (/cleanup|清理|drop table|redis.*del|进程残留/.test(value)) {
|
|
892
|
+
return "cleanup";
|
|
893
|
+
}
|
|
894
|
+
if (/curl|http|e2e|接口|页面测试/.test(value))
|
|
895
|
+
return "http_e2e";
|
|
896
|
+
if (/startup|启动|端口|listener/.test(value)) {
|
|
897
|
+
return "application_startup";
|
|
898
|
+
}
|
|
899
|
+
if (/package|build|compile|构建|编译|打包/.test(value))
|
|
900
|
+
return "package";
|
|
901
|
+
if (/test|测试|覆盖率/.test(value))
|
|
902
|
+
return "unit_test";
|
|
903
|
+
if (/preflight|交付|证据|报告/.test(value))
|
|
904
|
+
return "delivery_self_check";
|
|
905
|
+
if (current && current !== "source_discovery")
|
|
906
|
+
return current;
|
|
907
|
+
return "implementation";
|
|
908
|
+
}
|
|
909
|
+
function advanceExecutorStage(current, candidate) {
|
|
910
|
+
const order = [
|
|
911
|
+
"source_discovery",
|
|
912
|
+
"implementation",
|
|
913
|
+
"unit_test",
|
|
914
|
+
"package",
|
|
915
|
+
"application_startup",
|
|
916
|
+
"http_e2e",
|
|
917
|
+
"cleanup",
|
|
918
|
+
"delivery_self_check",
|
|
919
|
+
];
|
|
920
|
+
if (!current)
|
|
921
|
+
return candidate;
|
|
922
|
+
return order.indexOf(candidate) >= order.indexOf(current)
|
|
923
|
+
? candidate
|
|
924
|
+
: current;
|
|
925
|
+
}
|
|
926
|
+
function appendExecutorStageEvent(contract, state, stage) {
|
|
927
|
+
const labels = {
|
|
928
|
+
source_discovery: ["源码与规则检索", "source and rule discovery"],
|
|
929
|
+
implementation: ["编码实现", "implementation"],
|
|
930
|
+
unit_test: ["单元测试", "unit tests"],
|
|
931
|
+
package: ["构建打包", "build and package"],
|
|
932
|
+
application_startup: ["应用启动", "application startup"],
|
|
933
|
+
http_e2e: ["真实 HTTP E2E", "real HTTP E2E"],
|
|
934
|
+
cleanup: ["环境清理", "environment cleanup"],
|
|
935
|
+
delivery_self_check: [
|
|
936
|
+
"交付前确定性自检",
|
|
937
|
+
"deterministic delivery preflight",
|
|
938
|
+
],
|
|
939
|
+
};
|
|
940
|
+
appendManagedEvent(state, {
|
|
941
|
+
eventType: "executor.stage_changed",
|
|
942
|
+
actor: contract.executorAgent,
|
|
943
|
+
role: "executor",
|
|
944
|
+
summary: mt(contract, `研发阶段:${labels[stage][0]}`, `Executor stage: ${labels[stage][1]}`),
|
|
945
|
+
evidencePaths: [],
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
function validateInvocationTelemetry(contract, state, invocationResult, result) {
|
|
949
|
+
const telemetry = invocationResult.telemetry;
|
|
950
|
+
if (!telemetry)
|
|
951
|
+
return;
|
|
952
|
+
if (contract.executorAgent === "claude") {
|
|
953
|
+
if (!telemetry.model || telemetry.tools.length === 0) {
|
|
954
|
+
throw new Error(mt(contract, "Claude system/init 未提供实际模型或工具清单", "Claude system/init did not provide the actual model or tool list"));
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
if (result.changedFiles.length > 0 &&
|
|
958
|
+
!telemetry.toolUses.some((tool) => /write|edit|apply_patch/i.test(tool))) {
|
|
959
|
+
if (!declaredFilesChangedSinceBaseline(contract, state, result.changedFiles)) {
|
|
960
|
+
throw new Error(mt(contract, "执行结果声明修改文件,但工作区基线无法证明任何对应变更", "The executor reported changed files but the workspace baseline proves no corresponding change"));
|
|
961
|
+
}
|
|
962
|
+
appendManagedEvent(state, {
|
|
963
|
+
eventType: "executor.write_telemetry_inferred",
|
|
964
|
+
actor: "managed-runner",
|
|
965
|
+
role: "runner",
|
|
966
|
+
summary: mt(contract, "流式事件缺少 Write/Edit,但冻结基线已证明交付文件真实变化;转交 Host 语义评审,不重启 Executor", "Write/Edit stream telemetry was absent, but the frozen baseline proves real delivery changes; forwarding to Host review without restarting the Executor"),
|
|
967
|
+
evidencePaths: [],
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
const claimsNoTool = [...result.blockers, result.summary].some((item) => /工具.*不可用|no tools?|tool.*unavailable/i.test(item));
|
|
971
|
+
if (claimsNoTool &&
|
|
972
|
+
telemetry.tools.length > 0 &&
|
|
973
|
+
telemetry.toolUses.length === 0) {
|
|
974
|
+
throw new Error(mt(contract, "执行者在未尝试已声明工具的情况下声称工具不可用", "The executor claimed tools were unavailable without attempting the declared tools"));
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
function declaredFilesChangedSinceBaseline(contract, state, files) {
|
|
978
|
+
const roots = [contract.projectRoot, ...contract.relatedProjectRoots].map((root) => path.resolve(root));
|
|
979
|
+
const current = snapshotChangedWorkspaceFiles(roots);
|
|
980
|
+
const baseline = state.baselineWorkspaceFiles ?? {};
|
|
981
|
+
return files.some((file) => {
|
|
982
|
+
const normalized = file.replaceAll("\\", "/");
|
|
983
|
+
return roots.some((root) => {
|
|
984
|
+
const absolute = path.isAbsolute(file)
|
|
985
|
+
? path.resolve(file)
|
|
986
|
+
: path.resolve(root, file);
|
|
987
|
+
if (absolute !== root && !absolute.startsWith(`${root}${path.sep}`)) {
|
|
988
|
+
return false;
|
|
989
|
+
}
|
|
990
|
+
const relative = path.relative(root, absolute).replaceAll("\\", "/");
|
|
991
|
+
if (relative !== normalized && path.isAbsolute(file))
|
|
992
|
+
return false;
|
|
993
|
+
const key = `${root}::${relative}`;
|
|
994
|
+
return current[key] !== undefined && baseline[key] !== current[key];
|
|
995
|
+
});
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
function prepareExternalHostReview(contract, state, env) {
|
|
999
|
+
if (!hasUnlimitedAgentBudget(contract) &&
|
|
1000
|
+
state.reviewInvocations >= contract.budgets.maxReviewRounds) {
|
|
1001
|
+
return finishReviewExhausted(contract, state, env);
|
|
1002
|
+
}
|
|
1003
|
+
state.reviewRound += 1;
|
|
1004
|
+
state.reviewInvocations += 1;
|
|
1005
|
+
const reviewedResult = state.lastExecutorResult
|
|
1006
|
+
? JSON.parse(readFileSync(state.lastExecutorResult, "utf-8"))
|
|
1007
|
+
: null;
|
|
1008
|
+
const workspacePaths = hostReviewWorkspacePaths(contract, reviewedResult);
|
|
1009
|
+
const promptPath = path.join(managedRunDir(state.projectRoot, state.taskId, state.runId), `host-review-${state.reviewRound}.md`);
|
|
1010
|
+
const reviewFacts = writeManagedReviewFacts(contract, state);
|
|
1011
|
+
state.lastReviewedWorkspaceFiles = reviewFacts.workspaceSnapshot;
|
|
1012
|
+
writeFileSync(promptPath, buildReviewPrompt(contract, state, reviewFacts.jsonPath), "utf-8");
|
|
1013
|
+
state.pendingExternalReview = {
|
|
1014
|
+
round: state.reviewRound,
|
|
1015
|
+
promptPath,
|
|
1016
|
+
factsPath: reviewFacts.jsonPath,
|
|
1017
|
+
workspaceFingerprint: computeScopedWorkspaceFingerprint([contract.projectRoot, ...contract.relatedProjectRoots], workspacePaths),
|
|
1018
|
+
requestedAt: new Date().toISOString(),
|
|
1019
|
+
workspacePaths,
|
|
1020
|
+
};
|
|
1021
|
+
const reviewMessage = mt(contract, `等待当前 Host ${contract.supervisorAgent} 提交评审结果`, `Waiting for the current ${contract.supervisorAgent} host to submit its review result`);
|
|
1022
|
+
state.blocker = null;
|
|
1023
|
+
state = transition(contract, state, "waiting_for_host_review", "external_supervisor_review_required", env);
|
|
1024
|
+
saveManagedRun(state);
|
|
1025
|
+
appendManagedEvent(state, {
|
|
1026
|
+
eventType: "review.external_requested",
|
|
1027
|
+
actor: contract.supervisorAgent,
|
|
1028
|
+
role: "supervisor",
|
|
1029
|
+
summary: reviewMessage,
|
|
1030
|
+
evidencePaths: [promptPath, reviewFacts.jsonPath, reviewFacts.markdownPath],
|
|
1031
|
+
});
|
|
1032
|
+
if (notifyManagedTask({
|
|
1033
|
+
taskId: state.taskId,
|
|
1034
|
+
type: "review_required",
|
|
1035
|
+
title: mt(contract, "Superflow 等待主 Agent 评审", "Superflow is waiting for host review"),
|
|
1036
|
+
message: mt(contract, `${state.taskId} 已完成研发执行,等待第 ${state.reviewRound} 轮全量评审`, `${state.taskId} completed executor work and awaits full review round ${state.reviewRound}`),
|
|
1037
|
+
}, env)) {
|
|
1038
|
+
appendManagedEvent(state, {
|
|
1039
|
+
eventType: "host.review_required_notified",
|
|
1040
|
+
actor: "managed-runner",
|
|
1041
|
+
role: "runner",
|
|
1042
|
+
summary: `已通知 Host 处理第 ${state.reviewRound} 轮评审`,
|
|
1043
|
+
evidencePaths: [promptPath],
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
return state;
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Host 验收会重跑已声明的验证命令,因此只允许明确 taskEvidence 指向
|
|
1050
|
+
* 的证据文件在评审期间更新;其他源码与合同文件仍必须保持不变。
|
|
1051
|
+
*/
|
|
1052
|
+
function hostReviewWorkspacePaths(contract, result) {
|
|
1053
|
+
if (!result)
|
|
1054
|
+
return [];
|
|
1055
|
+
const roots = [contract.projectRoot, ...contract.relatedProjectRoots].map((root) => path.resolve(root));
|
|
1056
|
+
const evidencePaths = new Set((result.taskEvidence ?? []).flatMap((item) => item.evidencePaths.flatMap((evidencePath) => {
|
|
1057
|
+
const canonicalPath = canonicalEvidencePath(evidencePath);
|
|
1058
|
+
return path.isAbsolute(canonicalPath)
|
|
1059
|
+
? [path.resolve(canonicalPath)]
|
|
1060
|
+
: roots.map((root) => path.resolve(root, canonicalPath));
|
|
1061
|
+
})));
|
|
1062
|
+
return result.changedFiles.filter((changedFile) => {
|
|
1063
|
+
const candidates = path.isAbsolute(changedFile)
|
|
1064
|
+
? [path.resolve(changedFile)]
|
|
1065
|
+
: roots.map((root) => path.resolve(root, changedFile));
|
|
1066
|
+
return !candidates.some((candidate) => evidencePaths.has(candidate));
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
function rolloverSession(session) {
|
|
1070
|
+
const previousSessionIds = [...(session.previousSessionIds ?? [])];
|
|
1071
|
+
if (session.sessionId && !previousSessionIds.includes(session.sessionId)) {
|
|
1072
|
+
previousSessionIds.push(session.sessionId);
|
|
1073
|
+
}
|
|
1074
|
+
return {
|
|
1075
|
+
...session,
|
|
1076
|
+
sessionId: null,
|
|
1077
|
+
createdAt: null,
|
|
1078
|
+
status: "pending",
|
|
1079
|
+
previousSessionIds,
|
|
1080
|
+
resumingSessionId: null,
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
function retireExecutorSession(session, reason) {
|
|
1084
|
+
const sessionId = session.resumingSessionId;
|
|
1085
|
+
if (!sessionId)
|
|
1086
|
+
return rolloverSession(session);
|
|
1087
|
+
const retiredSessions = [...(session.retiredSessions ?? [])];
|
|
1088
|
+
if (!retiredSessions.some((retired) => retired.sessionId === sessionId)) {
|
|
1089
|
+
retiredSessions.push({
|
|
1090
|
+
sessionId,
|
|
1091
|
+
reason,
|
|
1092
|
+
retiredAt: new Date().toISOString(),
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
return {
|
|
1096
|
+
...rolloverSession(session),
|
|
1097
|
+
retiredSessions,
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
export function writeManagedExecutorHandoff(contract, state, findings) {
|
|
1101
|
+
const runDir = managedRunDir(state.projectRoot, state.taskId, state.runId);
|
|
1102
|
+
const reportPath = path.join(runDir, "task-report.md");
|
|
1103
|
+
const report = existsSync(reportPath)
|
|
1104
|
+
? readFileSync(reportPath, "utf-8").slice(-6_000)
|
|
1105
|
+
: "";
|
|
1106
|
+
const lastResult = state.lastExecutorResult
|
|
1107
|
+
? JSON.parse(readFileSync(state.lastExecutorResult, "utf-8"))
|
|
1108
|
+
: null;
|
|
1109
|
+
const progress = readOpenSpecTaskProgress(contract, completionResultWithHistory(state, lastResult));
|
|
1110
|
+
const diffStat = safeGitOutput(contract.projectRoot, ["diff", "--stat"]);
|
|
1111
|
+
const changedFiles = changedWorkspaceFilesSinceBaseline(contract, state).map(({ root, relative }) => path.resolve(root) === path.resolve(contract.projectRoot)
|
|
1112
|
+
? relative.replaceAll("\\", "/")
|
|
1113
|
+
: path.join(root, relative).replaceAll("\\", "/"));
|
|
1114
|
+
const evidenceTasks = readCompletionTasks(contract, completionResultWithHistory(state, lastResult)).filter((task) => task.completed && task.category === "local_required");
|
|
1115
|
+
const humanMessages = undeliveredHumanGuidance(contract, state);
|
|
1116
|
+
const displayedHumanMessages = humanMessages.slice(-20);
|
|
1117
|
+
const deliveryMessageIds = humanMessages.map((message) => message.messageId);
|
|
1118
|
+
const contextManifest = ensureManagedContextManifest(contract);
|
|
1119
|
+
const ruleSelection = frozenRuleSelection(contract, contextManifest);
|
|
1120
|
+
const ruleFiles = ruleSelection.files;
|
|
1121
|
+
const preflightCommand = [
|
|
1122
|
+
process.execPath,
|
|
1123
|
+
path.join(ASSETS_DIR, "scripts", "superflow-managed-executor-preflight.mjs"),
|
|
1124
|
+
contract.projectRoot,
|
|
1125
|
+
state.taskId,
|
|
1126
|
+
].join(" ");
|
|
1127
|
+
const file = path.join(runDir, `executor-handoff-${state.executorInvocations}.md`);
|
|
1128
|
+
const machineFile = path.join(runDir, `executor-handoff-${state.executorInvocations}.json`);
|
|
1129
|
+
writeJsonAtomic(machineFile, {
|
|
1130
|
+
protocolVersion: "superflow.handoff.v2",
|
|
1131
|
+
messageType: "executor_handoff",
|
|
1132
|
+
taskId: state.taskId,
|
|
1133
|
+
runId: state.runId,
|
|
1134
|
+
invocation: state.executorInvocations,
|
|
1135
|
+
language: contract.language ?? "zh",
|
|
1136
|
+
frozenPrompt: {
|
|
1137
|
+
path: contract.taskPrompt?.snapshotPath ?? null,
|
|
1138
|
+
sha256: contract.taskPrompt?.sha256 ?? null,
|
|
1139
|
+
},
|
|
1140
|
+
workspace: { changedFiles, diffStat: diffStat || null },
|
|
1141
|
+
executionHints: {
|
|
1142
|
+
stablePolicySha256: stableExecutorPolicyHash(),
|
|
1143
|
+
contextManifest: {
|
|
1144
|
+
path: managedContextManifestPath(contract),
|
|
1145
|
+
sha256: contextManifest.manifestHash,
|
|
1146
|
+
},
|
|
1147
|
+
ruleScenarios: ruleSelection.scenarios,
|
|
1148
|
+
applicableRuleFiles: ruleFiles,
|
|
1149
|
+
mandatoryEngineeringRules: contract.mandatoryEngineeringRules ?? [],
|
|
1150
|
+
deterministicPreflightCommand: preflightCommand,
|
|
1151
|
+
},
|
|
1152
|
+
tasks: {
|
|
1153
|
+
total: progress.total,
|
|
1154
|
+
completed: progress.completed,
|
|
1155
|
+
localPending: progress.localPending,
|
|
1156
|
+
externalPending: progress.externalPending,
|
|
1157
|
+
checkedLocalTaskIds: evidenceTasks.map((task) => task.taskId),
|
|
1158
|
+
},
|
|
1159
|
+
findings,
|
|
1160
|
+
humanGuidance: {
|
|
1161
|
+
messages: humanMessages,
|
|
1162
|
+
deliveryMessageIds,
|
|
1163
|
+
},
|
|
1164
|
+
budget: {
|
|
1165
|
+
executorInvocations: state.executorInvocations,
|
|
1166
|
+
effectiveExecutorInvocations: effectiveExecutorInvocations(state),
|
|
1167
|
+
maxExecutorInvocations: contract.budgets.maxExecutorInvocations,
|
|
1168
|
+
tokenUnitsUsed: executorOutputTokenUnits(state),
|
|
1169
|
+
maxExecutorTokenUnits: contract.budgets.maxExecutorTokenUnits ?? null,
|
|
1170
|
+
outputTokensUsed: executorOutputTokenUnits(state),
|
|
1171
|
+
maxExecutorOutputTokens: contract.budgets.maxExecutorTokenUnits ?? null,
|
|
1172
|
+
costUsdUsed: state.executorUsage?.costUsd ?? null,
|
|
1173
|
+
maxExecutorCostUsd: contract.budgets.maxExecutorCostUsd ?? null,
|
|
1174
|
+
},
|
|
1175
|
+
});
|
|
1176
|
+
const lines = contract.language === "en"
|
|
1177
|
+
? [
|
|
1178
|
+
"# Condensed Executor Handoff",
|
|
1179
|
+
"",
|
|
1180
|
+
`Task: ${state.taskId}`,
|
|
1181
|
+
`Frozen prompt: ${contract.taskPrompt?.snapshotPath ?? "none"}`,
|
|
1182
|
+
`Prompt SHA-256: ${contract.taskPrompt?.sha256 ?? "none"}`,
|
|
1183
|
+
`Machine handoff: ${machineFile}`,
|
|
1184
|
+
`Applicable rule files: ${ruleFiles.join(", ") || "none"}`,
|
|
1185
|
+
`Mandatory host rules: ${contract.mandatoryEngineeringRules?.join("; ") || "none"}`,
|
|
1186
|
+
`Deterministic delivery preflight: ${preflightCommand}`,
|
|
1187
|
+
`Task progress: ${progress.completed}/${progress.total}; local pending ${progress.localPending.length}; external pending ${progress.externalPending.length}`,
|
|
1188
|
+
`Current diff stat:\n${diffStat || "none"}`,
|
|
1189
|
+
"",
|
|
1190
|
+
"## Required Complete changedFiles",
|
|
1191
|
+
"The Runner owns the authoritative changedFiles list below. The executor may report hints, but must not spend a repair round reproducing this list.",
|
|
1192
|
+
...(changedFiles.length > 0
|
|
1193
|
+
? changedFiles.map((changedFile) => `- ${changedFile}`)
|
|
1194
|
+
: ["none"]),
|
|
1195
|
+
"",
|
|
1196
|
+
"## Required Exact taskEvidence IDs",
|
|
1197
|
+
"The Runner derives baseline evidence for every exact ID below from tasks.md, workspace changes, and successful commands. Add taskEvidence only when a task needs distinct evidence; do not duplicate entries merely to satisfy formatting.",
|
|
1198
|
+
...(evidenceTasks.length > 0
|
|
1199
|
+
? evidenceTasks.map((task) => `- ${task.taskId}: ${task.text}`)
|
|
1200
|
+
: ["none"]),
|
|
1201
|
+
"",
|
|
1202
|
+
"## Current Findings",
|
|
1203
|
+
...handoffFindingLines(findings, "en"),
|
|
1204
|
+
"",
|
|
1205
|
+
"## Latest User Guidance",
|
|
1206
|
+
...(displayedHumanMessages.length > 0
|
|
1207
|
+
? displayedHumanMessages.map((message) => `- ${message.timestamp} [${message.actor}] ${message.content}`)
|
|
1208
|
+
: ["none"]),
|
|
1209
|
+
"",
|
|
1210
|
+
"## Local Pending Tasks",
|
|
1211
|
+
...(progress.localPending.length > 0
|
|
1212
|
+
? progress.localPending
|
|
1213
|
+
: ["none"]),
|
|
1214
|
+
"",
|
|
1215
|
+
"## External Prerequisites",
|
|
1216
|
+
...(progress.externalPending.length > 0
|
|
1217
|
+
? progress.externalPending
|
|
1218
|
+
: ["none"]),
|
|
1219
|
+
"",
|
|
1220
|
+
"## Recent Persisted Report",
|
|
1221
|
+
report || "none",
|
|
1222
|
+
]
|
|
1223
|
+
: [
|
|
1224
|
+
"# 研发执行压缩交接包",
|
|
1225
|
+
"",
|
|
1226
|
+
`任务:${state.taskId}`,
|
|
1227
|
+
`冻结 Prompt:${contract.taskPrompt?.snapshotPath ?? "无"}`,
|
|
1228
|
+
`Prompt SHA-256:${contract.taskPrompt?.sha256 ?? "无"}`,
|
|
1229
|
+
`机器交接协议:${machineFile}`,
|
|
1230
|
+
`适用规则文件:${ruleFiles.join("、") || "无"}`,
|
|
1231
|
+
`Host 强制规则:${contract.mandatoryEngineeringRules?.join(";") || "无"}`,
|
|
1232
|
+
`交付前确定性门禁:${preflightCommand}`,
|
|
1233
|
+
`任务进度:${progress.completed}/${progress.total};本地剩余 ${progress.localPending.length};外部前置 ${progress.externalPending.length}`,
|
|
1234
|
+
`当前差异统计:\n${diffStat || "无"}`,
|
|
1235
|
+
"",
|
|
1236
|
+
"## 必须完整上报的 changedFiles",
|
|
1237
|
+
"以下 changedFiles 由 Runner 负责生成并作为事实源;Executor 可提供提示,但不得为重复抄写本清单消耗整改轮次。",
|
|
1238
|
+
...(changedFiles.length > 0
|
|
1239
|
+
? changedFiles.map((changedFile) => `- ${changedFile}`)
|
|
1240
|
+
: ["无"]),
|
|
1241
|
+
"",
|
|
1242
|
+
"## 必须逐项上报的精确 taskEvidence ID",
|
|
1243
|
+
"Runner 会根据 tasks.md、工作区差异和成功命令为以下精确 ID 推导基础证据。只有某项需要独立证据时才补充 taskEvidence,禁止为了满足格式重复填写全部任务。",
|
|
1244
|
+
...(evidenceTasks.length > 0
|
|
1245
|
+
? evidenceTasks.map((task) => `- ${task.taskId}: ${task.text}`)
|
|
1246
|
+
: ["无"]),
|
|
1247
|
+
"",
|
|
1248
|
+
"## 本轮全部 Finding",
|
|
1249
|
+
...handoffFindingLines(findings, "zh"),
|
|
1250
|
+
"",
|
|
1251
|
+
"## 用户最新补充",
|
|
1252
|
+
...(displayedHumanMessages.length > 0
|
|
1253
|
+
? displayedHumanMessages.map((message) => `- ${message.timestamp} [${message.actor}] ${message.content}`)
|
|
1254
|
+
: ["无"]),
|
|
1255
|
+
"",
|
|
1256
|
+
"## 本地剩余任务",
|
|
1257
|
+
...(progress.localPending.length > 0
|
|
1258
|
+
? progress.localPending
|
|
1259
|
+
: ["无"]),
|
|
1260
|
+
"",
|
|
1261
|
+
"## 外部发布前置",
|
|
1262
|
+
...(progress.externalPending.length > 0
|
|
1263
|
+
? progress.externalPending
|
|
1264
|
+
: ["无"]),
|
|
1265
|
+
"",
|
|
1266
|
+
"## 最近落盘报告",
|
|
1267
|
+
report || "无",
|
|
1268
|
+
];
|
|
1269
|
+
writeFileSync(file, `${lines.join("\n")}\n`, "utf-8");
|
|
1270
|
+
appendManagedEvent(state, {
|
|
1271
|
+
eventType: "executor.handoff_created",
|
|
1272
|
+
actor: "managed-runner",
|
|
1273
|
+
role: "runner",
|
|
1274
|
+
summary: mt(contract, `已生成第 ${state.executorInvocations} 次研发执行压缩交接包`, `Created condensed handoff for executor invocation ${state.executorInvocations}`),
|
|
1275
|
+
evidencePaths: [file, machineFile],
|
|
1276
|
+
});
|
|
1277
|
+
return file;
|
|
1278
|
+
}
|
|
1279
|
+
function hasUndeliveredHumanGuidance(contract, state) {
|
|
1280
|
+
return undeliveredHumanGuidance(contract, state).length > 0;
|
|
1281
|
+
}
|
|
1282
|
+
function undeliveredHumanGuidance(contract, state) {
|
|
1283
|
+
const delivered = deliveredHumanGuidanceIds(state);
|
|
1284
|
+
return readManagedHumanMessages(contract)
|
|
1285
|
+
.filter((message) => !delivered.has(message.messageId));
|
|
1286
|
+
}
|
|
1287
|
+
function deliveredHumanGuidanceIds(state) {
|
|
1288
|
+
const runDir = managedRunDir(state.projectRoot, state.taskId, state.runId);
|
|
1289
|
+
if (!existsSync(runDir))
|
|
1290
|
+
return new Set();
|
|
1291
|
+
const delivered = new Set();
|
|
1292
|
+
for (const file of readdirSync(runDir)) {
|
|
1293
|
+
if (!/^executor-guidance-delivery-\d+\.json$/.test(file))
|
|
1294
|
+
continue;
|
|
1295
|
+
try {
|
|
1296
|
+
const receipt = JSON.parse(readFileSync(path.join(runDir, file), "utf-8"));
|
|
1297
|
+
if (!Array.isArray(receipt.messageIds))
|
|
1298
|
+
continue;
|
|
1299
|
+
for (const messageId of receipt.messageIds) {
|
|
1300
|
+
if (typeof messageId === "string")
|
|
1301
|
+
delivered.add(messageId);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
catch {
|
|
1305
|
+
// A malformed receipt is not proof of delivery and is therefore retried.
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
return delivered;
|
|
1309
|
+
}
|
|
1310
|
+
function recordHumanGuidanceHandoffDelivery(contract, state, handoffFile, sessionId) {
|
|
1311
|
+
const machineFile = handoffFile.replace(/\.md$/, ".json");
|
|
1312
|
+
const handoff = JSON.parse(readFileSync(machineFile, "utf-8"));
|
|
1313
|
+
const messageIds = handoff.humanGuidance?.deliveryMessageIds;
|
|
1314
|
+
if (!Array.isArray(messageIds) || messageIds.length === 0)
|
|
1315
|
+
return;
|
|
1316
|
+
const validIds = messageIds.filter((messageId) => typeof messageId === "string");
|
|
1317
|
+
if (validIds.length === 0)
|
|
1318
|
+
return;
|
|
1319
|
+
const runDir = managedRunDir(state.projectRoot, state.taskId, state.runId);
|
|
1320
|
+
const receiptFile = path.join(runDir, `executor-guidance-delivery-${state.executorInvocations}.json`);
|
|
1321
|
+
if (existsSync(receiptFile))
|
|
1322
|
+
return;
|
|
1323
|
+
writeJsonAtomic(receiptFile, {
|
|
1324
|
+
protocolVersion: "superflow.guidance-receipt.v1",
|
|
1325
|
+
taskId: state.taskId,
|
|
1326
|
+
runId: state.runId,
|
|
1327
|
+
invocation: state.executorInvocations,
|
|
1328
|
+
handoffFile,
|
|
1329
|
+
sessionId,
|
|
1330
|
+
messageIds: validIds,
|
|
1331
|
+
deliveredAt: new Date().toISOString(),
|
|
1332
|
+
});
|
|
1333
|
+
appendManagedEvent(state, {
|
|
1334
|
+
eventType: "executor.guidance_handoff_delivered",
|
|
192
1335
|
actor: contract.executorAgent,
|
|
193
1336
|
role: "executor",
|
|
194
|
-
summary:
|
|
195
|
-
evidencePaths: [
|
|
1337
|
+
summary: mt(contract, `Executor 已确认接收 ${validIds.length} 条运行中补充指导`, `Executor acknowledged ${validIds.length} in-flight guidance message(s)`),
|
|
1338
|
+
evidencePaths: [machineFile, receiptFile],
|
|
196
1339
|
});
|
|
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
1340
|
}
|
|
201
|
-
|
|
202
|
-
if (
|
|
203
|
-
return
|
|
1341
|
+
function handoffFindingLines(findings, language) {
|
|
1342
|
+
if (findings.length === 0)
|
|
1343
|
+
return [language === "en" ? "none" : "无"];
|
|
1344
|
+
return findings.map((finding) => language === "en"
|
|
1345
|
+
? `- ${finding.id} ${finding.target}: ${finding.requiredFix}; checks: ${finding.acceptanceChecks.join("; ")}`
|
|
1346
|
+
: `- ${finding.id} ${finding.target}:${finding.requiredFix};验收:${finding.acceptanceChecks.join(";")}`);
|
|
1347
|
+
}
|
|
1348
|
+
function executorCompletionGap(contract, result) {
|
|
1349
|
+
if (contract.source !== "sdd")
|
|
1350
|
+
return null;
|
|
1351
|
+
const decision = evaluateCompletion(contract, result);
|
|
1352
|
+
if (decision.localGaps.length > 0) {
|
|
1353
|
+
return mt(contract, `本地交付裁决未通过:${decision.localGaps.join(";")}`, `Local completion policy failed: ${decision.localGaps.join("; ")}`);
|
|
204
1354
|
}
|
|
205
|
-
|
|
206
|
-
|
|
1355
|
+
const tasksFile = findOpenSpecTasksFile(contract);
|
|
1356
|
+
if (tasksFile) {
|
|
1357
|
+
const report = path.join(path.dirname(tasksFile), "test-report.md");
|
|
1358
|
+
if (!existsSync(report)) {
|
|
1359
|
+
return mt(contract, "SDD 任务缺少 test-report.md,研发 Agent 必须填写真实测试和运行证据", "The SDD task is missing test-report.md; the executor must record real test and runtime evidence");
|
|
1360
|
+
}
|
|
207
1361
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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);
|
|
1362
|
+
return null;
|
|
1363
|
+
}
|
|
1364
|
+
function readOpenSpecTaskProgress(contract, result) {
|
|
1365
|
+
const tasks = readCompletionTasks(contract, result);
|
|
1366
|
+
if (tasks.length === 0) {
|
|
1367
|
+
return { total: 0, completed: 0, localPending: [], externalPending: [] };
|
|
255
1368
|
}
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
1369
|
+
const pending = tasks.filter((task) => !task.completed);
|
|
1370
|
+
return {
|
|
1371
|
+
total: tasks.length,
|
|
1372
|
+
completed: tasks.length - pending.length,
|
|
1373
|
+
localPending: pending
|
|
1374
|
+
.filter((task) => task.category === "local_required")
|
|
1375
|
+
.map((task) => `${task.taskId}: ${task.text}`),
|
|
1376
|
+
externalPending: pending
|
|
1377
|
+
.filter((task) => task.category !== "local_required")
|
|
1378
|
+
.map((task) => `${task.taskId}: ${task.text}`),
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
export function isExternalPendingTask(line) {
|
|
1382
|
+
return /\[(?:environment_required|release_required)\]/i.test(line);
|
|
1383
|
+
}
|
|
1384
|
+
function findOpenSpecTasksFile(contract) {
|
|
1385
|
+
if (!contract.taskPrompt?.originalPath)
|
|
1386
|
+
return null;
|
|
1387
|
+
let current = path.dirname(path.resolve(contract.taskPrompt.originalPath));
|
|
1388
|
+
const projectRoot = path.resolve(contract.projectRoot);
|
|
1389
|
+
while (current.startsWith(projectRoot)) {
|
|
1390
|
+
const file = path.join(current, "tasks.md");
|
|
1391
|
+
if (existsSync(file))
|
|
1392
|
+
return file;
|
|
1393
|
+
const parent = path.dirname(current);
|
|
1394
|
+
if (parent === current)
|
|
1395
|
+
break;
|
|
1396
|
+
current = parent;
|
|
271
1397
|
}
|
|
272
|
-
|
|
273
|
-
|
|
1398
|
+
return null;
|
|
1399
|
+
}
|
|
1400
|
+
function safeGitOutput(projectRoot, args) {
|
|
1401
|
+
try {
|
|
1402
|
+
return execFileSync("git", args, {
|
|
1403
|
+
cwd: projectRoot,
|
|
1404
|
+
encoding: "utf-8",
|
|
1405
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1406
|
+
}).trim();
|
|
274
1407
|
}
|
|
275
|
-
|
|
276
|
-
return
|
|
1408
|
+
catch {
|
|
1409
|
+
return "";
|
|
277
1410
|
}
|
|
278
|
-
state.lastExecutorResult = null;
|
|
279
|
-
state.currentStep = "repair_prompt_ready";
|
|
280
|
-
saveManagedRun(state);
|
|
281
|
-
return state;
|
|
282
1411
|
}
|
|
283
|
-
function invocation(contract, state,
|
|
1412
|
+
function invocation(contract, state, prompt, promptPath, schemaPath, sessionId, deliveryProtocolRecovery, onSession, onProgress) {
|
|
1413
|
+
const freshMaxTurnRecovery = !sessionId && consecutiveMaxTurnFailures(state) > 0;
|
|
1414
|
+
const policyPath = path.join(managedRunDir(state.projectRoot, state.taskId, state.runId), "managed-executor-policy.md");
|
|
1415
|
+
const contextManifest = ensureManagedContextManifest(contract);
|
|
1416
|
+
const ruleSelection = frozenRuleSelection(contract, contextManifest);
|
|
1417
|
+
const preflightScript = path.join(ASSETS_DIR, "scripts", "superflow-managed-executor-preflight.mjs");
|
|
1418
|
+
const ownerTemplate = path.join(ASSETS_DIR, "scripts", "superflow-managed-owner-verification.sh");
|
|
1419
|
+
const preflightCommand = [
|
|
1420
|
+
process.execPath,
|
|
1421
|
+
preflightScript,
|
|
1422
|
+
contract.projectRoot,
|
|
1423
|
+
state.taskId,
|
|
1424
|
+
].join(" ");
|
|
1425
|
+
writeFileSync(policyPath, buildExecutorPolicy({
|
|
1426
|
+
ruleSelection,
|
|
1427
|
+
toolchainFacts: managedToolchainFacts(),
|
|
1428
|
+
ownerTemplate,
|
|
1429
|
+
preflightCommand,
|
|
1430
|
+
compatibilityContinuation: Boolean(sessionId),
|
|
1431
|
+
freshRecovery: freshMaxTurnRecovery,
|
|
1432
|
+
deliveryProtocolRecovery,
|
|
1433
|
+
}), "utf-8");
|
|
284
1434
|
return {
|
|
285
1435
|
taskId: state.taskId,
|
|
286
1436
|
runId: state.runId,
|
|
287
|
-
role,
|
|
1437
|
+
role: "executor",
|
|
288
1438
|
language: contract.language,
|
|
289
|
-
agent:
|
|
1439
|
+
agent: contract.executorAgent,
|
|
290
1440
|
projectRoot: state.projectRoot,
|
|
291
1441
|
writableRoots: contract.relatedProjectRoots,
|
|
292
1442
|
prompt,
|
|
1443
|
+
promptPath,
|
|
293
1444
|
schemaPath,
|
|
1445
|
+
systemPromptPath: policyPath,
|
|
294
1446
|
sessionId,
|
|
295
1447
|
timeout: {
|
|
296
1448
|
warningMs: contract.budgets.noProgressWarningMinutes * 60_000,
|
|
@@ -299,13 +1451,111 @@ function invocation(contract, state, role, prompt, schemaPath, sessionId, onSess
|
|
|
299
1451
|
},
|
|
300
1452
|
onSession,
|
|
301
1453
|
onProgress,
|
|
1454
|
+
onTelemetry: (telemetry) => {
|
|
1455
|
+
if (!acceptsExecutorCallback(state))
|
|
1456
|
+
return;
|
|
1457
|
+
state.runtimeTelemetry = telemetry;
|
|
1458
|
+
saveManagedRun(state);
|
|
1459
|
+
},
|
|
1460
|
+
onProcess: (pid) => {
|
|
1461
|
+
if (!acceptsExecutorCallback(state))
|
|
1462
|
+
return;
|
|
1463
|
+
state.runningAgentPid = pid;
|
|
1464
|
+
saveManagedRun(state);
|
|
1465
|
+
},
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
function managedToolchainFacts() {
|
|
1469
|
+
const facts = [
|
|
1470
|
+
[
|
|
1471
|
+
"java",
|
|
1472
|
+
process.env.JAVA_HOME
|
|
1473
|
+
? path.join(process.env.JAVA_HOME, "bin", "java")
|
|
1474
|
+
: executablePath("java"),
|
|
1475
|
+
],
|
|
1476
|
+
["maven", executablePath("mvn")],
|
|
1477
|
+
["node", executablePath("node")],
|
|
1478
|
+
["npm", executablePath("npm")],
|
|
1479
|
+
]
|
|
1480
|
+
.filter((item) => Boolean(item[1]))
|
|
1481
|
+
.map(([name, executable]) => `- ${name}: ${executable}`);
|
|
1482
|
+
if (facts.length === 0)
|
|
1483
|
+
return [];
|
|
1484
|
+
return [
|
|
1485
|
+
"Superflow preflight found these executable paths. Use them directly instead of spending turns searching for the toolchain:",
|
|
1486
|
+
...facts,
|
|
1487
|
+
];
|
|
1488
|
+
}
|
|
1489
|
+
function frozenRuleSelection(contract, manifest) {
|
|
1490
|
+
const selected = selectManagedRuleFiles(contract);
|
|
1491
|
+
const frozenRules = new Set(manifest.entries
|
|
1492
|
+
.filter((entry) => entry.role === "rule")
|
|
1493
|
+
.map((entry) => path.resolve(entry.path)));
|
|
1494
|
+
return {
|
|
1495
|
+
scenarios: selected.scenarios,
|
|
1496
|
+
files: selected.files.filter((file) => frozenRules.has(path.resolve(file))),
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
function executablePath(command) {
|
|
1500
|
+
try {
|
|
1501
|
+
return execFileSync("which", [command], {
|
|
1502
|
+
encoding: "utf-8",
|
|
1503
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1504
|
+
}).trim();
|
|
1505
|
+
}
|
|
1506
|
+
catch {
|
|
1507
|
+
return "";
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
function executorOutputTokenUnits(state) {
|
|
1511
|
+
return state.executorUsage?.outputTokens ?? null;
|
|
1512
|
+
}
|
|
1513
|
+
function mergeAgentUsage(current, next) {
|
|
1514
|
+
if (!next)
|
|
1515
|
+
return current;
|
|
1516
|
+
return {
|
|
1517
|
+
inputTokens: sumNullable(current?.inputTokens, next.inputTokens),
|
|
1518
|
+
outputTokens: sumNullable(current?.outputTokens, next.outputTokens),
|
|
1519
|
+
cacheReadTokens: sumNullable(current?.cacheReadTokens, next.cacheReadTokens),
|
|
1520
|
+
cacheWriteTokens: sumNullable(current?.cacheWriteTokens, next.cacheWriteTokens),
|
|
1521
|
+
costUsd: sumNullable(current?.costUsd, next.costUsd),
|
|
302
1522
|
};
|
|
303
1523
|
}
|
|
1524
|
+
function sumNullable(left, right) {
|
|
1525
|
+
if (left == null)
|
|
1526
|
+
return right ?? null;
|
|
1527
|
+
if (right == null)
|
|
1528
|
+
return null;
|
|
1529
|
+
return left + right;
|
|
1530
|
+
}
|
|
1531
|
+
async function invokeWithFailureEvidence(state, invoker, agentInvocation, prefix) {
|
|
1532
|
+
try {
|
|
1533
|
+
return await invoker.invoke(agentInvocation);
|
|
1534
|
+
}
|
|
1535
|
+
catch (error) {
|
|
1536
|
+
const raw = error;
|
|
1537
|
+
const stdout = typeof raw.stdout === "string" ? raw.stdout : "";
|
|
1538
|
+
const stderr = typeof raw.stderr === "string" ? raw.stderr : "";
|
|
1539
|
+
const logs = stdout || stderr
|
|
1540
|
+
? writeInvocationLogs(state, prefix, stdout, stderr)
|
|
1541
|
+
: [];
|
|
1542
|
+
appendManagedEvent(state, {
|
|
1543
|
+
eventType: "executor.invocation_failed",
|
|
1544
|
+
actor: agentInvocation.agent,
|
|
1545
|
+
role: "executor",
|
|
1546
|
+
summary: raw.message,
|
|
1547
|
+
evidencePaths: logs,
|
|
1548
|
+
});
|
|
1549
|
+
throw error;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
304
1552
|
function transition(contract, state, status, currentStep, env) {
|
|
305
1553
|
state = {
|
|
306
1554
|
...state,
|
|
307
1555
|
status,
|
|
308
1556
|
currentStep,
|
|
1557
|
+
blocker: status === "running" ? null : state.blocker,
|
|
1558
|
+
failure: status === "running" ? null : state.failure ?? null,
|
|
309
1559
|
servicePid: process.pid,
|
|
310
1560
|
updatedAt: new Date().toISOString(),
|
|
311
1561
|
};
|
|
@@ -315,29 +1565,66 @@ function transition(contract, state, status, currentStep, env) {
|
|
|
315
1565
|
updateRegistry(contract, state, env);
|
|
316
1566
|
return state;
|
|
317
1567
|
}
|
|
318
|
-
function
|
|
319
|
-
|
|
320
|
-
|
|
1568
|
+
function pauseAtSafeBoundary(contract, state, env) {
|
|
1569
|
+
const signal = readManagedControlSignal(contract);
|
|
1570
|
+
if (!signal?.pauseRequested)
|
|
1571
|
+
return state;
|
|
1572
|
+
clearManagedControlSignal(contract);
|
|
1573
|
+
state.blocker = signal.reason;
|
|
1574
|
+
state.activeSince = null;
|
|
1575
|
+
state = transition(contract, state, "paused", "paused_by_user", env);
|
|
1576
|
+
appendManagedEvent(state, {
|
|
1577
|
+
eventType: "human.pause_applied",
|
|
1578
|
+
actor: signal.actor,
|
|
1579
|
+
role: "system",
|
|
1580
|
+
summary: mt(contract, `已在安全边界暂停:${signal.reason}`, `Paused at a safe boundary: ${signal.reason}`),
|
|
1581
|
+
});
|
|
1582
|
+
return state;
|
|
1583
|
+
}
|
|
1584
|
+
function finishDeliveryReady(contract, state, summary, env) {
|
|
1585
|
+
const executorResult = state.lastExecutorResult
|
|
1586
|
+
? JSON.parse(readFileSync(state.lastExecutorResult, "utf-8"))
|
|
1587
|
+
: null;
|
|
1588
|
+
const decision = evaluateCompletion(contract, completionResultWithHistory(state, executorResult));
|
|
1589
|
+
if (!decision.localReady) {
|
|
1590
|
+
return finishBlocked(contract, state, decision.localGaps.join(";"), env);
|
|
1591
|
+
}
|
|
1592
|
+
state.deliveryProgress = decision.progress;
|
|
1593
|
+
const status = !decision.environmentReady
|
|
1594
|
+
? "environment_validation_blocked"
|
|
1595
|
+
: !decision.releaseReady
|
|
1596
|
+
? "local_delivery_ready"
|
|
1597
|
+
: "release_ready";
|
|
321
1598
|
state.workspaceFingerprint = computeWorkspaceFingerprintForRoots([
|
|
322
1599
|
contract.projectRoot,
|
|
323
1600
|
...contract.relatedProjectRoots,
|
|
324
1601
|
]);
|
|
325
|
-
saveManagedRun(state);
|
|
326
1602
|
runManagedDeliveryCheck(state, true);
|
|
327
|
-
|
|
328
|
-
|
|
1603
|
+
state = transition(contract, state, status, status, env);
|
|
1604
|
+
state.completedAt = new Date().toISOString();
|
|
1605
|
+
saveManagedRun(state);
|
|
1606
|
+
appendTaskReport(state, mt(contract, `\n## 最终结论\n\n${summary}\n\n状态:${status}。\n`, `\n## Final conclusion\n\n${summary}\n\nStatus: ${status}.\n`));
|
|
1607
|
+
resolveTaskReportBlockers(state);
|
|
1608
|
+
updateTaskReportStatus(state, status);
|
|
329
1609
|
appendManagedEvent(state, {
|
|
330
1610
|
eventType: "run.delivery_ready",
|
|
331
1611
|
actor: "managed-runner",
|
|
332
1612
|
role: "runner",
|
|
333
|
-
summary: mt(contract,
|
|
1613
|
+
summary: mt(contract, `任务已通过本地检查,进入 ${status}`, `Task passed local review and entered ${status}`),
|
|
334
1614
|
});
|
|
335
|
-
notifyManagedTask({
|
|
1615
|
+
if (notifyManagedTask({
|
|
336
1616
|
taskId: state.taskId,
|
|
337
1617
|
type: "delivery_ready",
|
|
338
1618
|
title: mt(contract, "Superflow 任务已完成", "Superflow task completed"),
|
|
339
|
-
message: mt(contract, `${state.taskId}
|
|
340
|
-
}, env)
|
|
1619
|
+
message: mt(contract, `${state.taskId} 已通过本地检查并进入 ${status};Git、环境和发布动作仍需用户批准`, `${state.taskId} passed local review and entered ${status}; Git, environment, and release actions still require user approval`),
|
|
1620
|
+
}, env)) {
|
|
1621
|
+
appendManagedEvent(state, {
|
|
1622
|
+
eventType: "host.delivery_ready_notified",
|
|
1623
|
+
actor: "managed-runner",
|
|
1624
|
+
role: "runner",
|
|
1625
|
+
summary: mt(contract, "已发送可交付通知", "Recorded delivery-ready notification"),
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
341
1628
|
return state;
|
|
342
1629
|
}
|
|
343
1630
|
function finishBlocked(contract, state, blocker, env) {
|
|
@@ -353,12 +1640,19 @@ function finishBlocked(contract, state, blocker, env) {
|
|
|
353
1640
|
role: "runner",
|
|
354
1641
|
summary: blocker,
|
|
355
1642
|
});
|
|
356
|
-
notifyManagedTask({
|
|
1643
|
+
if (notifyManagedTask({
|
|
357
1644
|
taskId: state.taskId,
|
|
358
1645
|
type: "human_required",
|
|
359
1646
|
title: mt(contract, "Superflow 任务需要处理", "Superflow task needs attention"),
|
|
360
1647
|
message: `${state.taskId}: ${blocker}`,
|
|
361
|
-
}, env)
|
|
1648
|
+
}, env)) {
|
|
1649
|
+
appendManagedEvent(state, {
|
|
1650
|
+
eventType: "host.attention_notified",
|
|
1651
|
+
actor: "managed-runner",
|
|
1652
|
+
role: "runner",
|
|
1653
|
+
summary: mt(contract, "已发送人工处理通知", "Recorded attention notification"),
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
362
1656
|
return state;
|
|
363
1657
|
}
|
|
364
1658
|
function finishReviewExhausted(contract, state, env) {
|
|
@@ -400,6 +1694,21 @@ function finishBudgetExhausted(contract, state, reason, env) {
|
|
|
400
1694
|
return state;
|
|
401
1695
|
}
|
|
402
1696
|
function enforceBudgets(contract, state, env) {
|
|
1697
|
+
if (!hasUnlimitedAgentBudget(contract)) {
|
|
1698
|
+
const usage = state.executorUsage;
|
|
1699
|
+
const outputTokens = executorOutputTokenUnits(state);
|
|
1700
|
+
if (contract.budgets.maxExecutorTokenUnits !== undefined &&
|
|
1701
|
+
outputTokens !== null &&
|
|
1702
|
+
outputTokens >= contract.budgets.maxExecutorTokenUnits) {
|
|
1703
|
+
return finishBudgetExhausted(contract, state, mt(contract, `研发 Agent 输出 Token 熔断:累计 ${outputTokens},上限 ${contract.budgets.maxExecutorTokenUnits}`, `Executor output-token circuit opened at ${outputTokens}; limit ${contract.budgets.maxExecutorTokenUnits}`), env);
|
|
1704
|
+
}
|
|
1705
|
+
if (contract.budgets.maxExecutorCostUsd !== undefined &&
|
|
1706
|
+
usage?.costUsd !== null &&
|
|
1707
|
+
usage?.costUsd !== undefined &&
|
|
1708
|
+
usage.costUsd >= contract.budgets.maxExecutorCostUsd) {
|
|
1709
|
+
return finishBudgetExhausted(contract, state, mt(contract, `研发 Agent 成本熔断:累计 $${usage?.costUsd?.toFixed(2)},上限 $${contract.budgets.maxExecutorCostUsd.toFixed(2)}`, `Executor cost circuit opened at $${usage?.costUsd?.toFixed(2)}; limit $${contract.budgets.maxExecutorCostUsd.toFixed(2)}`), env);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
403
1712
|
const activeHours = activeRunMilliseconds(state) / 3_600_000;
|
|
404
1713
|
if (activeHours >= contract.budgets.activeRunWarningHours &&
|
|
405
1714
|
!readManagedEvents(state).some((event) => event.eventType === "deadline.warning")) {
|
|
@@ -423,6 +1732,9 @@ function enforceBudgets(contract, state, env) {
|
|
|
423
1732
|
}
|
|
424
1733
|
return state;
|
|
425
1734
|
}
|
|
1735
|
+
function hasUnlimitedAgentBudget(contract) {
|
|
1736
|
+
return contract.budgets.unlimitedAgentInvocations === true;
|
|
1737
|
+
}
|
|
426
1738
|
function closeActiveTime(state, started) {
|
|
427
1739
|
state.activeRunMilliseconds += Math.max(0, Date.now() - started);
|
|
428
1740
|
state.activeSince = null;
|
|
@@ -435,15 +1747,16 @@ function loadRepairFindings(state) {
|
|
|
435
1747
|
const result = JSON.parse(readFileSync(state.lastReviewResult, "utf-8"));
|
|
436
1748
|
return result.findings.filter((finding) => finding.blocking);
|
|
437
1749
|
}
|
|
438
|
-
function validateExecutorResult(contract, result) {
|
|
1750
|
+
function validateExecutorResult(contract, state, result, options = {}) {
|
|
439
1751
|
if (!["ready_for_review", "blocked", "failed"].includes(result.status)) {
|
|
440
1752
|
throw new Error(mt(contract, "执行 Agent 返回了非法状态", "Executor Agent returned an invalid status"));
|
|
441
1753
|
}
|
|
442
1754
|
if (!Array.isArray(result.changedFiles) ||
|
|
443
1755
|
!Array.isArray(result.commands) ||
|
|
444
1756
|
!Array.isArray(result.evidence) ||
|
|
1757
|
+
!Array.isArray(result.releasePrerequisites) ||
|
|
445
1758
|
!Array.isArray(result.blockers)) {
|
|
446
|
-
throw new Error(mt(contract, "执行 Agent
|
|
1759
|
+
throw new Error(mt(contract, "执行 Agent 结果缺少文件、命令、证据、发布前置条件或阻塞列表", "Executor Agent result is missing files, commands, evidence, release prerequisites, or blockers"));
|
|
447
1760
|
}
|
|
448
1761
|
if (!result.summary.trim()) {
|
|
449
1762
|
throw new Error(mt(contract, "执行 Agent 结果缺少摘要", "Executor Agent result is missing a summary"));
|
|
@@ -457,44 +1770,392 @@ function validateExecutorResult(contract, result) {
|
|
|
457
1770
|
if (result.blockers.length > 0) {
|
|
458
1771
|
throw new Error(mt(contract, "执行 Agent 声明可检查,但仍存在阻塞项", "Executor Agent reported ready for review while blockers remain"));
|
|
459
1772
|
}
|
|
460
|
-
|
|
1773
|
+
const blockingCommands = result.commands.filter(isBlockingCommandFailure);
|
|
1774
|
+
if (blockingCommands.length > 0 &&
|
|
1775
|
+
blockingCommands.every((command) => command.assertion === "negative")) {
|
|
1776
|
+
throw new Error(mt(contract, "负向命令证据不符合检查型断言协议,需要 Host 判断原始失败是否为预期行为", "Negative command evidence violates the inspection-only assertion protocol and requires Host judgment of whether the raw failure was expected"));
|
|
1777
|
+
}
|
|
1778
|
+
if (blockingCommands.length > 0) {
|
|
461
1779
|
throw new Error(mt(contract, "执行 Agent 声明可检查,但命令证据中仍有失败项", "Executor Agent reported ready for review while command evidence still contains failures"));
|
|
462
1780
|
}
|
|
463
1781
|
if (contract.profile === "engineering" || contract.profile === "sdd") {
|
|
464
|
-
|
|
1782
|
+
validateDeclaredChangedFiles(contract, state, result, options);
|
|
1783
|
+
validateEngineeringEvidence(result, contract, state);
|
|
1784
|
+
// Risk-based evidence sufficiency is intentionally semantic: after the
|
|
1785
|
+
// external Host has inspected the raw evidence and returned pass, the
|
|
1786
|
+
// Runner must not overrule that decision with command-text heuristics.
|
|
1787
|
+
// Schema, changed files, command outcomes, and engineering categories
|
|
1788
|
+
// remain deterministic gates above.
|
|
1789
|
+
if (!options.hostApproved)
|
|
1790
|
+
validateRiskBasedEvidence(contract, result);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
function normalizeExecutorResult(contract, state, result) {
|
|
1794
|
+
if (!result || typeof result !== "object")
|
|
1795
|
+
return result;
|
|
1796
|
+
result.protocolVersion = "superflow.executor.v2";
|
|
1797
|
+
result.messageType = "executor_delivery";
|
|
1798
|
+
const machineChangedFiles = changedWorkspaceFilesSinceBaseline(contract, state).map(({ root, relative }) => path.resolve(root) === path.resolve(contract.projectRoot)
|
|
1799
|
+
? relative.replaceAll("\\", "/")
|
|
1800
|
+
: path.join(root, relative).replaceAll("\\", "/"));
|
|
1801
|
+
result.changedFiles = uniqueStrings([
|
|
1802
|
+
...(Array.isArray(result.changedFiles) ? result.changedFiles : []),
|
|
1803
|
+
...machineChangedFiles,
|
|
1804
|
+
]);
|
|
1805
|
+
result.blockers = normalizedStrings(result.blockers);
|
|
1806
|
+
result.releasePrerequisites = normalizedStrings(result.releasePrerequisites);
|
|
1807
|
+
result.evidence = normalizedStrings(result.evidence);
|
|
1808
|
+
if (Array.isArray(result.commands)) {
|
|
1809
|
+
const currentCommands = result.commands.map((command) => ({
|
|
1810
|
+
...command,
|
|
1811
|
+
categories: uniqueCategories([
|
|
1812
|
+
...(command.categories ?? []),
|
|
1813
|
+
...verificationCategories(command.command, command.result),
|
|
1814
|
+
]),
|
|
1815
|
+
}));
|
|
1816
|
+
result.commands = uniqueCommands([
|
|
1817
|
+
...historicalEngineeringCommands(state, true).filter((command) => !isBlockingCommandFailure(command) &&
|
|
1818
|
+
isCommandOutcomeConsistent(command)),
|
|
1819
|
+
...currentCommands,
|
|
1820
|
+
]);
|
|
1821
|
+
}
|
|
1822
|
+
return result;
|
|
1823
|
+
}
|
|
1824
|
+
function uniqueStrings(values) {
|
|
1825
|
+
return [...new Set(values.filter(Boolean))];
|
|
1826
|
+
}
|
|
1827
|
+
function normalizedStrings(values) {
|
|
1828
|
+
if (!Array.isArray(values))
|
|
1829
|
+
return [];
|
|
1830
|
+
return uniqueStrings(values
|
|
1831
|
+
.filter((value) => typeof value === "string")
|
|
1832
|
+
.map((value) => value.trim()));
|
|
1833
|
+
}
|
|
1834
|
+
function uniqueCategories(values) {
|
|
1835
|
+
return [...new Set(values)];
|
|
1836
|
+
}
|
|
1837
|
+
function uniqueCommands(commands) {
|
|
1838
|
+
const seen = new Set();
|
|
1839
|
+
return commands.filter((command) => {
|
|
1840
|
+
const key = JSON.stringify([
|
|
1841
|
+
command.command,
|
|
1842
|
+
command.exitCode,
|
|
1843
|
+
command.result,
|
|
1844
|
+
command.assertion ?? "positive",
|
|
1845
|
+
]);
|
|
1846
|
+
if (seen.has(key))
|
|
1847
|
+
return false;
|
|
1848
|
+
seen.add(key);
|
|
1849
|
+
return true;
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
function isBlockingCommandFailure(command) {
|
|
1853
|
+
if (isAcceptedEvidenceCommand(command))
|
|
1854
|
+
return false;
|
|
1855
|
+
if (isExpectedStartupShutdown(command))
|
|
1856
|
+
return false;
|
|
1857
|
+
return true;
|
|
1858
|
+
}
|
|
1859
|
+
function isExpectedStartupShutdown(command) {
|
|
1860
|
+
if (command.exitCode !== 143 || !command.categories?.includes("startup")) {
|
|
1861
|
+
return false;
|
|
1862
|
+
}
|
|
1863
|
+
return (/(?:started|undertow.*port|application.*started)/i.test(command.result) &&
|
|
1864
|
+
/(?:sigterm|exit(?:\s*code)?\s*=?\s*143|退出码\s*=?\s*143)/i.test(command.result));
|
|
1865
|
+
}
|
|
1866
|
+
function validateWorkspaceSecrets(contract, state) {
|
|
1867
|
+
const leaked = changedWorkspaceFilesSinceBaseline(contract, state)
|
|
1868
|
+
.map(({ root, relative }) => ({
|
|
1869
|
+
relative,
|
|
1870
|
+
absolute: path.join(root, relative),
|
|
1871
|
+
}))
|
|
1872
|
+
.filter(({ absolute }) => existsSync(absolute))
|
|
1873
|
+
.filter(({ absolute }) => {
|
|
1874
|
+
try {
|
|
1875
|
+
const content = readFileSync(absolute, "utf-8");
|
|
1876
|
+
return containsPlaintextCredential(content);
|
|
1877
|
+
}
|
|
1878
|
+
catch {
|
|
1879
|
+
return false;
|
|
1880
|
+
}
|
|
1881
|
+
})
|
|
1882
|
+
.map(({ relative }) => relative.replaceAll("\\", "/"));
|
|
1883
|
+
if (leaked.length === 0)
|
|
1884
|
+
return;
|
|
1885
|
+
throw new Error(mt(contract, `工作区存在疑似明文凭据:${leaked.slice(0, 10).join("、")}`, `Workspace contains suspected plaintext credentials: ${leaked
|
|
1886
|
+
.slice(0, 10)
|
|
1887
|
+
.join(", ")}`));
|
|
1888
|
+
}
|
|
1889
|
+
export function containsPlaintextCredential(value) {
|
|
1890
|
+
const literal = /\b(?:[a-z0-9]+_)*(?:password|secret|api[_-]?key|access[_-]?token|auth[_-]?token)\b\s*[:=]\s*["']([^"']+)["']/gi;
|
|
1891
|
+
for (const match of value.matchAll(literal)) {
|
|
1892
|
+
if (isPlaintextSecretValue(match[1]))
|
|
1893
|
+
return true;
|
|
1894
|
+
}
|
|
1895
|
+
const mysqlPassword = /(?:\s-p(?:'([^']*)'|"([^"]*)"|([^\s'"-][^\s]*))|--password=(?:'([^']*)'|"([^"]*)"|([^\s]+)))/g;
|
|
1896
|
+
for (const match of value.matchAll(mysqlPassword)) {
|
|
1897
|
+
const secret = match.slice(1).find((item) => item !== undefined) ?? "";
|
|
1898
|
+
if (isPlaintextSecretValue(secret)) {
|
|
1899
|
+
return true;
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
return /authorization\s*[:=]\s*bearer\s+(?!<redacted>)[^\s"']+/i.test(value);
|
|
1903
|
+
}
|
|
1904
|
+
function isPlaintextSecretValue(value) {
|
|
1905
|
+
const normalized = value.trim().toLowerCase();
|
|
1906
|
+
return (normalized.length >= 6 &&
|
|
1907
|
+
!normalized.includes("<redacted>") &&
|
|
1908
|
+
!isDocumentationSecretPlaceholder(normalized) &&
|
|
1909
|
+
!normalized.startsWith("$") &&
|
|
1910
|
+
!isNonceDerivedTemporarySecret(normalized));
|
|
1911
|
+
}
|
|
1912
|
+
function isDocumentationSecretPlaceholder(value) {
|
|
1913
|
+
return /^<(?:pass(?:word)?|secret|api[_-]?key|access[_-]?token|auth[_-]?token)>$/i.test(value);
|
|
1914
|
+
}
|
|
1915
|
+
function isNonceDerivedTemporarySecret(value) {
|
|
1916
|
+
return /^[a-z0-9_-]*\$(?:\{[a-z0-9_]*nonce\}|[a-z0-9_]*nonce)$/.test(value);
|
|
1917
|
+
}
|
|
1918
|
+
function validateDeclaredChangedFiles(contract, state, result, options = {}) {
|
|
1919
|
+
// Host approval judges the Executor's delivery at the Host-review snapshot.
|
|
1920
|
+
// The Host may create later acceptance reports while independently verifying
|
|
1921
|
+
// the delivery; those reports are not omitted Executor changes. Normal
|
|
1922
|
+
// Executor intake still compares the live workspace against its baseline.
|
|
1923
|
+
const reviewedSnapshot = options.hostApproved && state.lastReviewedWorkspaceFiles
|
|
1924
|
+
? state.lastReviewedWorkspaceFiles
|
|
1925
|
+
: undefined;
|
|
1926
|
+
const changed = changedWorkspaceFilesSinceBaseline(contract, state, reviewedSnapshot).map(({ key }) => key);
|
|
1927
|
+
const declared = result.changedFiles.map((file) => file.replaceAll("\\", "/"));
|
|
1928
|
+
const missing = changed.filter((key) => {
|
|
1929
|
+
const [root, relative] = key.split("::");
|
|
1930
|
+
const absolute = path.join(root, relative).replaceAll("\\", "/");
|
|
1931
|
+
return (!declared.includes(relative.replaceAll("\\", "/")) &&
|
|
1932
|
+
!declared.includes(absolute));
|
|
1933
|
+
});
|
|
1934
|
+
if (missing.length > 0) {
|
|
1935
|
+
throw new Error(mt(contract, `执行结果漏报真实工作区变更:${missing.join("、")}`, `Executor result omitted real workspace changes: ${missing.join(", ")}`));
|
|
465
1936
|
}
|
|
466
1937
|
}
|
|
467
|
-
function
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
1938
|
+
function changedWorkspaceFilesSinceBaseline(contract, state, snapshot) {
|
|
1939
|
+
const current = snapshot ??
|
|
1940
|
+
snapshotChangedWorkspaceFiles([
|
|
1941
|
+
contract.projectRoot,
|
|
1942
|
+
...contract.relatedProjectRoots,
|
|
1943
|
+
]);
|
|
1944
|
+
const baseline = state.baselineWorkspaceFiles ?? {};
|
|
1945
|
+
return Object.entries(current)
|
|
1946
|
+
.filter(([key, hash]) => baseline[key] !== hash)
|
|
1947
|
+
.map(([key]) => {
|
|
1948
|
+
const separator = key.indexOf("::");
|
|
1949
|
+
return {
|
|
1950
|
+
key,
|
|
1951
|
+
root: key.slice(0, separator),
|
|
1952
|
+
relative: key.slice(separator + 2),
|
|
1953
|
+
};
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1956
|
+
function validateEngineeringEvidence(result, contract, state) {
|
|
1957
|
+
const commands = result.commands;
|
|
1958
|
+
const allCommands = [...historicalEngineeringCommands(state), ...commands];
|
|
1959
|
+
const successful = allCommands.filter((command) => command.exitCode === 0 || isExpectedStartupShutdown(command));
|
|
1960
|
+
const categories = new Set(successful.flatMap((command) => command.categories ??
|
|
1961
|
+
verificationCategories(command.command, command.result)));
|
|
1962
|
+
const requiredCategories = requiredVerificationCategories(contract.projectRoot);
|
|
1963
|
+
if (categories.size < requiredCategories) {
|
|
471
1964
|
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
1965
|
}
|
|
1966
|
+
if (isSpringBootProject(contract.projectRoot)) {
|
|
1967
|
+
const missing = [
|
|
1968
|
+
!categories.has("startup") ? "startup" : null,
|
|
1969
|
+
!categories.has("invocation") ? "invocation" : null,
|
|
1970
|
+
].filter((value) => value !== null);
|
|
1971
|
+
if (missing.length > 0) {
|
|
1972
|
+
const labels = missing.map((category) => category === "startup"
|
|
1973
|
+
? mt(contract, "启动应用", "application startup")
|
|
1974
|
+
: mt(contract, "真实 HTTP 调用", "real HTTP invocation"));
|
|
1975
|
+
const message = mt(contract, `Spring Boot 工程交付前必须完成启动应用和真实 HTTP 调用,当前缺少:${labels.join("、")}`, `Spring Boot delivery requires application startup and a real HTTP invocation; missing: ${labels.join(", ")}`);
|
|
1976
|
+
if (missing.length === 1 &&
|
|
1977
|
+
missing[0] === "invocation" &&
|
|
1978
|
+
categories.has("startup") &&
|
|
1979
|
+
categories.has("runtime") &&
|
|
1980
|
+
result.evidence.length > 0) {
|
|
1981
|
+
throw new VerificationMetadataAmbiguityError(message);
|
|
1982
|
+
}
|
|
1983
|
+
throw new Error(message);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
class VerificationMetadataAmbiguityError extends Error {
|
|
1988
|
+
}
|
|
1989
|
+
function historicalEngineeringCommands(state, includeInvalid = false) {
|
|
1990
|
+
const runDir = managedRunDir(state.projectRoot, state.taskId, state.runId);
|
|
1991
|
+
if (!existsSync(runDir))
|
|
1992
|
+
return [];
|
|
1993
|
+
const resultPattern = includeInvalid
|
|
1994
|
+
? /^executor-result-\d+(?:-invalid)?\.json$/
|
|
1995
|
+
: /^executor-result-\d+\.json$/;
|
|
1996
|
+
return readdirSync(runDir)
|
|
1997
|
+
.filter((name) => resultPattern.test(name))
|
|
1998
|
+
.flatMap((name) => {
|
|
1999
|
+
try {
|
|
2000
|
+
const result = JSON.parse(readFileSync(path.join(runDir, name), "utf-8"));
|
|
2001
|
+
return Array.isArray(result.commands) ? result.commands : [];
|
|
2002
|
+
}
|
|
2003
|
+
catch {
|
|
2004
|
+
return [];
|
|
2005
|
+
}
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
function completionResultWithHistory(state, current) {
|
|
2009
|
+
if (!current)
|
|
2010
|
+
return null;
|
|
2011
|
+
const runDir = managedRunDir(state.projectRoot, state.taskId, state.runId);
|
|
2012
|
+
const history = existsSync(runDir)
|
|
2013
|
+
? readdirSync(runDir)
|
|
2014
|
+
.filter((name) => /^executor-result-\d+(?:-invalid)?\.json$/.test(name))
|
|
2015
|
+
.flatMap((name) => {
|
|
2016
|
+
try {
|
|
2017
|
+
return [
|
|
2018
|
+
JSON.parse(readFileSync(path.join(runDir, name), "utf-8")),
|
|
2019
|
+
];
|
|
2020
|
+
}
|
|
2021
|
+
catch {
|
|
2022
|
+
return [];
|
|
2023
|
+
}
|
|
2024
|
+
})
|
|
2025
|
+
: [];
|
|
2026
|
+
const results = [...history, current];
|
|
2027
|
+
const commands = uniqueCommands(results
|
|
2028
|
+
.flatMap((result) => result.commands)
|
|
2029
|
+
.filter((command) => !isBlockingCommandFailure(command) &&
|
|
2030
|
+
isCommandOutcomeConsistent(command)));
|
|
2031
|
+
return {
|
|
2032
|
+
...current,
|
|
2033
|
+
changedFiles: [
|
|
2034
|
+
...new Set(results.flatMap((result) => result.changedFiles)),
|
|
2035
|
+
],
|
|
2036
|
+
commands,
|
|
2037
|
+
evidence: [...new Set(results.flatMap((result) => result.evidence))],
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
2040
|
+
function requiredVerificationCategories(projectRoot) {
|
|
2041
|
+
const packageFile = path.join(projectRoot, "package.json");
|
|
2042
|
+
if (!existsSync(packageFile))
|
|
2043
|
+
return 2;
|
|
2044
|
+
try {
|
|
2045
|
+
const packageJson = JSON.parse(readFileSync(packageFile, "utf-8"));
|
|
2046
|
+
const scripts = packageJson.scripts ?? {};
|
|
2047
|
+
const hasTest = typeof scripts.test === "string";
|
|
2048
|
+
const hasRunnableTarget = ["build", "start", "dev", "serve"].some((name) => typeof scripts[name] === "string");
|
|
2049
|
+
return hasTest && !hasRunnableTarget ? 1 : 2;
|
|
2050
|
+
}
|
|
2051
|
+
catch {
|
|
2052
|
+
return 2;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
function isSpringBootProject(projectRoot) {
|
|
2056
|
+
const pom = path.join(projectRoot, "pom.xml");
|
|
2057
|
+
return existsSync(pom) && /spring-boot/i.test(readFileSync(pom, "utf-8"));
|
|
2058
|
+
}
|
|
2059
|
+
function queueExecutorSelfRepair(contract, state, message, evidencePath) {
|
|
2060
|
+
const rejectionKey = executorRejectionKey(message);
|
|
2061
|
+
const rejectionCounts = state.executorRejectionCounts ?? {};
|
|
2062
|
+
const repeated = (rejectionCounts[rejectionKey] ?? 0) >= 1;
|
|
2063
|
+
const repairCount = state.executorSelfRepairCount ?? 0;
|
|
2064
|
+
const permissionBlocked = (state.runtimeTelemetry?.permissionDenials ?? 0) >= 3;
|
|
2065
|
+
const semanticHostJudgment = /疑似明文凭据|suspected plaintext credentials/i.test(message);
|
|
2066
|
+
const structuredMetadataFailure = /JSON|Expected ',' or '}'|structured (?:output|metadata)|结构化.{0,12}(?:格式|解析)/i.test(message);
|
|
2067
|
+
const protocolEvidenceFailure = /负向命令证据不符合检查型断言协议|Negative command evidence violates the inspection-only assertion protocol/i.test(message);
|
|
2068
|
+
state.executorRejectionCounts = {
|
|
2069
|
+
...rejectionCounts,
|
|
2070
|
+
[rejectionKey]: (rejectionCounts[rejectionKey] ?? 0) + 1,
|
|
2071
|
+
};
|
|
2072
|
+
if (semanticHostJudgment ||
|
|
2073
|
+
structuredMetadataFailure ||
|
|
2074
|
+
protocolEvidenceFailure ||
|
|
2075
|
+
permissionBlocked ||
|
|
2076
|
+
repeated ||
|
|
2077
|
+
repairCount >= 2) {
|
|
2078
|
+
const stopReason = semanticHostJudgment
|
|
2079
|
+
? mt(contract, "(需要语义判断)", " because semantic judgment is required")
|
|
2080
|
+
: structuredMetadataFailure
|
|
2081
|
+
? mt(contract, "(结构化元数据不得触发完整研发重跑)", " because structured metadata must not trigger a full engineering rerun")
|
|
2082
|
+
: protocolEvidenceFailure
|
|
2083
|
+
? mt(contract, "(预期失败证据需要 Host 语义判断,不得触发完整研发重跑)", " because expected-failure evidence requires Host semantic judgment and must not trigger a full engineering rerun")
|
|
2084
|
+
: repeated
|
|
2085
|
+
? mt(contract, "(同类机械退回已达上限)", " after the same rejection repeated")
|
|
2086
|
+
: permissionBlocked
|
|
2087
|
+
? mt(contract, "(本轮权限拒绝过多)", " after repeated permission denials")
|
|
2088
|
+
: mt(contract, "(自动整改已达上限)", " after automatic repairs were exhausted");
|
|
2089
|
+
state.lastExecutorResult = evidencePath;
|
|
2090
|
+
state.lastReviewResult = null;
|
|
2091
|
+
state.currentStep = "executor_rejection_escalated";
|
|
2092
|
+
saveManagedRun(state);
|
|
2093
|
+
appendManagedEvent(state, {
|
|
2094
|
+
eventType: "executor.rejection_escalated_to_host",
|
|
2095
|
+
actor: "managed-runner",
|
|
2096
|
+
role: "runner",
|
|
2097
|
+
summary: semanticHostJudgment
|
|
2098
|
+
? mt(contract, `脚本不裁决疑似语义问题${stopReason},转交 Host 判断:${message}`, `The script does not decide ambiguous semantic issues${stopReason}; escalated to the host: ${message}`)
|
|
2099
|
+
: structuredMetadataFailure
|
|
2100
|
+
? mt(contract, `结构化元数据无法由 Runner 本地恢复${stopReason},转交 Host 判断:${message}`, `The runner could not recover structured metadata locally${stopReason}; escalated to the host: ${message}`)
|
|
2101
|
+
: protocolEvidenceFailure
|
|
2102
|
+
? mt(contract, `负向命令协议违规已止损${stopReason},转交 Host 核对原始日志:${message}`, `The negative-command protocol violation was stopped${stopReason} and escalated to the Host for raw-log review: ${message}`)
|
|
2103
|
+
: mt(contract, `自动整改已止损${stopReason},转交 Host 判断:${message}`, `Automatic repair stopped${stopReason} and escalated to the host: ${message}`),
|
|
2104
|
+
evidencePaths: [evidencePath],
|
|
2105
|
+
});
|
|
2106
|
+
return state;
|
|
2107
|
+
}
|
|
2108
|
+
const result = {
|
|
2109
|
+
result: "needs_fix",
|
|
2110
|
+
summary: message,
|
|
2111
|
+
findings: [
|
|
2112
|
+
{
|
|
2113
|
+
id: `AUTO-EXECUTOR-${state.executorInvocations}`,
|
|
2114
|
+
severity: "high",
|
|
2115
|
+
blocking: true,
|
|
2116
|
+
category: "delivery_gate",
|
|
2117
|
+
target: "完整研发交付与验证证据",
|
|
2118
|
+
evidence: message,
|
|
2119
|
+
risk: mt(contract, "研发 Agent 过早返回会增加评审轮次并留下未完成任务", "An early executor return increases review rounds and leaves work incomplete"),
|
|
2120
|
+
requiredFix: mt(contract, "继续完成全部本地任务、验证和测试报告后再返回 ready_for_review", "Continue all local work, verification, and test reporting before returning ready_for_review"),
|
|
2121
|
+
acceptanceChecks: [message],
|
|
2122
|
+
},
|
|
2123
|
+
],
|
|
2124
|
+
};
|
|
2125
|
+
const file = path.join(managedRunDir(state.projectRoot, state.taskId, state.runId), `executor-self-repair-${state.executorInvocations}.json`);
|
|
2126
|
+
writeJsonAtomic(file, result);
|
|
2127
|
+
state.lastReviewResult = file;
|
|
2128
|
+
state.lastExecutorResult = null;
|
|
2129
|
+
state.executorSelfRepairCount = repairCount + 1;
|
|
2130
|
+
state.currentStep = "executor_self_repair_required";
|
|
2131
|
+
saveManagedRun(state);
|
|
2132
|
+
appendManagedEvent(state, {
|
|
2133
|
+
eventType: "executor.self_repair_queued",
|
|
2134
|
+
actor: contract.executorAgent,
|
|
2135
|
+
role: "executor",
|
|
2136
|
+
summary: message,
|
|
2137
|
+
evidencePaths: [evidencePath, file],
|
|
2138
|
+
});
|
|
2139
|
+
return state;
|
|
473
2140
|
}
|
|
474
|
-
function
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
/gradle\w*\s+test/.test(normalized)) {
|
|
481
|
-
categories.push("test");
|
|
2141
|
+
function executorRejectionKey(message) {
|
|
2142
|
+
if (/漏报真实工作区变更|omitted real workspace changes/i.test(message)) {
|
|
2143
|
+
return "changed_files";
|
|
2144
|
+
}
|
|
2145
|
+
if (/结构化证据|taskEvidence|completion policy/i.test(message)) {
|
|
2146
|
+
return "task_evidence";
|
|
482
2147
|
}
|
|
483
|
-
if (/(
|
|
484
|
-
|
|
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");
|
|
2148
|
+
if (/至少需要两类|evidence categories|启动应用|real HTTP/i.test(message)) {
|
|
2149
|
+
return "verification_categories";
|
|
489
2150
|
}
|
|
490
|
-
if (/(
|
|
491
|
-
|
|
492
|
-
/health(?:check)?|integration|e2e/.test(normalized)) {
|
|
493
|
-
categories.push("runtime");
|
|
2151
|
+
if (/命令证据中仍有失败|command evidence.*fail/i.test(message)) {
|
|
2152
|
+
return "command_failure";
|
|
494
2153
|
}
|
|
495
|
-
|
|
2154
|
+
if (/写入工具调用|write tool use/i.test(message))
|
|
2155
|
+
return "write_telemetry";
|
|
2156
|
+
return message.replace(/\d+/g, "#").slice(0, 160);
|
|
496
2157
|
}
|
|
497
|
-
function validateReviewResult(result, language = "zh") {
|
|
2158
|
+
export function validateReviewResult(result, language = "zh") {
|
|
498
2159
|
if (!["pass", "needs_fix", "blocked"].includes(result.result)) {
|
|
499
2160
|
throw new Error(managedText(language, "监督 Agent 返回了非法状态", "Supervisor Agent returned an invalid status"));
|
|
500
2161
|
}
|
|
@@ -517,7 +2178,11 @@ function executorReportSection(state, result) {
|
|
|
517
2178
|
`Status: ${result.status}`,
|
|
518
2179
|
`Summary: ${result.summary}`,
|
|
519
2180
|
`Changed files: ${result.changedFiles.join(", ") || "none"}`,
|
|
2181
|
+
`Release prerequisites: ${result.releasePrerequisites.join("; ") || "none"}`,
|
|
520
2182
|
`Blockers: ${result.blockers.join("; ") || "none"}`,
|
|
2183
|
+
`Source progress: ${formatProgress(state.deliveryProgress?.source)}`,
|
|
2184
|
+
`Environment progress: ${formatProgress(state.deliveryProgress?.environment)}`,
|
|
2185
|
+
`Release progress: ${formatProgress(state.deliveryProgress?.release)}`,
|
|
521
2186
|
].join("\n");
|
|
522
2187
|
}
|
|
523
2188
|
return [
|
|
@@ -526,9 +2191,16 @@ function executorReportSection(state, result) {
|
|
|
526
2191
|
`状态:${result.status}`,
|
|
527
2192
|
`摘要:${result.summary}`,
|
|
528
2193
|
`修改文件:${result.changedFiles.join("、") || "无"}`,
|
|
2194
|
+
`发布前置条件:${result.releasePrerequisites.join(";") || "无"}`,
|
|
529
2195
|
`阻塞:${result.blockers.join(";") || "无"}`,
|
|
2196
|
+
`源码任务进度:${formatProgress(state.deliveryProgress?.source)}`,
|
|
2197
|
+
`环境验收进度:${formatProgress(state.deliveryProgress?.environment)}`,
|
|
2198
|
+
`发布签收进度:${formatProgress(state.deliveryProgress?.release)}`,
|
|
530
2199
|
].join("\n");
|
|
531
2200
|
}
|
|
2201
|
+
function formatProgress(progress) {
|
|
2202
|
+
return `${progress?.completed ?? 0}/${progress?.total ?? 0}`;
|
|
2203
|
+
}
|
|
532
2204
|
function reviewReportSection(state, result) {
|
|
533
2205
|
if (state.language === "en") {
|
|
534
2206
|
return [
|
|
@@ -569,16 +2241,63 @@ function registryRunId(contract, env) {
|
|
|
569
2241
|
return entry.activeRunId;
|
|
570
2242
|
}
|
|
571
2243
|
function isConnectivityFailure(message) {
|
|
572
|
-
return /(network|socket|connect(?:
|
|
2244
|
+
return /(network|socket|unable to connect|connection (?:refused|reset|closed)|server_error|timed?\s*out|rate.?limit|overloaded|busy|\b(?:429|503|529)\b|\bECONN[A-Z]*\b|网络|无法连接|连接被拒绝|限流|繁忙)/i.test(message);
|
|
2245
|
+
}
|
|
2246
|
+
export function isPermanentProviderFailure(message) {
|
|
2247
|
+
return /(token plan|用量上限|套餐|quota.?exhaust|insufficient.?quota|error.?code\s*2056|\b2056\b|billing|payment required)/i.test(message);
|
|
2248
|
+
}
|
|
2249
|
+
function recordExecutorConnectivityFailure(contract, state, message) {
|
|
2250
|
+
if (!state.currentStep.startsWith("executor_"))
|
|
2251
|
+
return null;
|
|
2252
|
+
const credited = isCreditableTransientProviderFailure(message);
|
|
2253
|
+
if (credited) {
|
|
2254
|
+
state.executorInvocationCredits =
|
|
2255
|
+
(state.executorInvocationCredits ?? 0) + 1;
|
|
2256
|
+
state.totalAgentInvocationCredits =
|
|
2257
|
+
(state.totalAgentInvocationCredits ?? 0) + 1;
|
|
2258
|
+
}
|
|
2259
|
+
state.consecutiveTransientProviderFailures =
|
|
2260
|
+
(state.consecutiveTransientProviderFailures ?? 0) + 1;
|
|
2261
|
+
state.connectivityRetryCount = (state.connectivityRetryCount ?? 0) + 1;
|
|
2262
|
+
let stopAt = null;
|
|
2263
|
+
if (credited && contract.budgets.executorPhysicalStopAt !== undefined) {
|
|
2264
|
+
stopAt = contract.budgets.executorPhysicalStopAt + 1;
|
|
2265
|
+
contract.budgets = {
|
|
2266
|
+
...contract.budgets,
|
|
2267
|
+
executorPhysicalStopAt: stopAt,
|
|
2268
|
+
};
|
|
2269
|
+
contract.contractHash = calculateManagedContractHash(contract);
|
|
2270
|
+
state.contractHash = contract.contractHash;
|
|
2271
|
+
saveManagedTask(contract);
|
|
2272
|
+
}
|
|
2273
|
+
saveManagedRun(state);
|
|
2274
|
+
return {
|
|
2275
|
+
invocation: state.executorInvocations,
|
|
2276
|
+
stopAt,
|
|
2277
|
+
consecutiveFailures: state.consecutiveTransientProviderFailures,
|
|
2278
|
+
credited,
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
function isCreditableTransientProviderFailure(message) {
|
|
2282
|
+
if (/(token plan|用量上限|套餐|quota.?exhaust|insufficient.?quota|error.?code\s*2056|\b2056\b)/i.test(message)) {
|
|
2283
|
+
return false;
|
|
2284
|
+
}
|
|
2285
|
+
return /(\b429\b|\b529\b|rate.?limit|overloaded|访问量过大|暂时.*限流|temporar(?:y|ily).*(?:busy|unavailable)|connection (?:refused|reset|closed)|unable to connect|socket hang up|\bECONN[A-Z]*\b|连接(?:被拒绝|重置|关闭)|无法连接)/i.test(message);
|
|
573
2286
|
}
|
|
574
|
-
function acquireProjectLocks(contract) {
|
|
2287
|
+
async function acquireProjectLocks(contract, binding) {
|
|
575
2288
|
const acquired = [];
|
|
576
2289
|
try {
|
|
577
2290
|
for (const root of [
|
|
578
2291
|
contract.projectRoot,
|
|
579
2292
|
...contract.relatedProjectRoots,
|
|
580
2293
|
].sort()) {
|
|
581
|
-
acquired.push(
|
|
2294
|
+
acquired.push(await acquireManagedProjectClaim(path.join(root, ".superflow", "managed-project.lock"), {
|
|
2295
|
+
taskId: contract.taskId,
|
|
2296
|
+
projectRoot: root,
|
|
2297
|
+
bindingFingerprint: managedWorkspaceBindingFingerprint(binding),
|
|
2298
|
+
token: `${contract.taskId}:${randomUUID()}`,
|
|
2299
|
+
language: contract.language,
|
|
2300
|
+
}));
|
|
582
2301
|
}
|
|
583
2302
|
return acquired;
|
|
584
2303
|
}
|
|
@@ -588,6 +2307,30 @@ function acquireProjectLocks(contract) {
|
|
|
588
2307
|
throw error;
|
|
589
2308
|
}
|
|
590
2309
|
}
|
|
2310
|
+
async function prepareManagedExecution(contract) {
|
|
2311
|
+
let projectLocks = [];
|
|
2312
|
+
let stage = "workspace_binding";
|
|
2313
|
+
try {
|
|
2314
|
+
const workspaceBinding = ensureManagedWorkspaceBinding(contract);
|
|
2315
|
+
stage = "project_claim";
|
|
2316
|
+
projectLocks = await acquireProjectLocks(contract, workspaceBinding);
|
|
2317
|
+
stage = "contract_validation";
|
|
2318
|
+
validateManagedTaskContract(contract);
|
|
2319
|
+
stage = "frozen_prompt_validation";
|
|
2320
|
+
validateManagedTaskPromptSnapshot(contract);
|
|
2321
|
+
stage = "context_manifest";
|
|
2322
|
+
ensureManagedContextManifest(contract);
|
|
2323
|
+
return { workspaceBinding, projectLocks };
|
|
2324
|
+
}
|
|
2325
|
+
catch (error) {
|
|
2326
|
+
for (const projectLock of projectLocks.reverse())
|
|
2327
|
+
projectLock.release();
|
|
2328
|
+
if (error instanceof ManagedWorkspaceClaimError)
|
|
2329
|
+
throw error;
|
|
2330
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2331
|
+
throw new ManagedEnvironmentPreparationError(message, stage);
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
591
2334
|
function runManagedDeliveryCheck(state, beforeReadyEvent = false) {
|
|
592
2335
|
const script = path.join(ASSETS_DIR, "scripts", "superflow-managed-work-check.mjs");
|
|
593
2336
|
const args = [script, state.projectRoot, state.taskId];
|
|
@@ -599,27 +2342,76 @@ function runManagedDeliveryCheck(state, beforeReadyEvent = false) {
|
|
|
599
2342
|
stdio: ["ignore", "pipe", "pipe"],
|
|
600
2343
|
});
|
|
601
2344
|
}
|
|
2345
|
+
class ExecutorFinalPreflightError extends Error {
|
|
2346
|
+
evidencePaths;
|
|
2347
|
+
constructor(message, evidencePaths) {
|
|
2348
|
+
super(message);
|
|
2349
|
+
this.evidencePaths = evidencePaths;
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
function runExecutorFinalPreflight(contract, state, result) {
|
|
2353
|
+
if (result.status !== "ready_for_review" ||
|
|
2354
|
+
safeGitOutput(contract.projectRoot, [
|
|
2355
|
+
"rev-parse",
|
|
2356
|
+
"--is-inside-work-tree",
|
|
2357
|
+
]) !== "true") {
|
|
2358
|
+
return [];
|
|
2359
|
+
}
|
|
2360
|
+
const script = path.join(ASSETS_DIR, "scripts", "superflow-managed-executor-preflight.mjs");
|
|
2361
|
+
const prefix = `executor-${state.executorInvocations}-final-preflight`;
|
|
2362
|
+
try {
|
|
2363
|
+
const stdout = execFileSync(process.execPath, [script, contract.projectRoot, state.taskId], {
|
|
2364
|
+
cwd: contract.projectRoot,
|
|
2365
|
+
encoding: "utf-8",
|
|
2366
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2367
|
+
});
|
|
2368
|
+
return writeInvocationLogs(state, prefix, stdout, "");
|
|
2369
|
+
}
|
|
2370
|
+
catch (error) {
|
|
2371
|
+
const execution = error;
|
|
2372
|
+
const logs = writeInvocationLogs(state, prefix, execution.stdout ?? "", execution.stderr ?? "");
|
|
2373
|
+
const detail = (execution.stderr ?? execution.message).trim();
|
|
2374
|
+
throw new ExecutorFinalPreflightError(mt(contract, `Runner 最终交付前门禁失败:${detail}`, `Runner final delivery preflight failed: ${detail}`), logs);
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
602
2377
|
function writeInvocationLogs(state, prefix, stdout, stderr) {
|
|
603
2378
|
const runDir = managedRunDir(state.projectRoot, state.taskId, state.runId);
|
|
604
2379
|
const files = [];
|
|
605
2380
|
if (stdout) {
|
|
606
|
-
|
|
607
|
-
writeFileSync(file, redactManagedLog(stdout), "utf-8");
|
|
608
|
-
files.push(file);
|
|
2381
|
+
files.push(...writeManagedLogParts(runDir, `${prefix}-events`, "jsonl", redactManagedLog(stdout)));
|
|
609
2382
|
}
|
|
610
2383
|
if (stderr) {
|
|
611
|
-
|
|
612
|
-
writeFileSync(file, redactManagedLog(stderr), "utf-8");
|
|
613
|
-
files.push(file);
|
|
2384
|
+
files.push(...writeManagedLogParts(runDir, `${prefix}-stderr`, "log", redactManagedLog(stderr)));
|
|
614
2385
|
}
|
|
615
2386
|
return files;
|
|
616
2387
|
}
|
|
617
|
-
|
|
618
|
-
return
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
2388
|
+
function fileHash(file) {
|
|
2389
|
+
return createHash("sha256").update(readFileSync(file)).digest("hex");
|
|
2390
|
+
}
|
|
2391
|
+
const MANAGED_LOG_PART_BYTES = 8 * 1024 * 1024;
|
|
2392
|
+
export function writeManagedLogParts(directory, basename, extension, content) {
|
|
2393
|
+
const bytes = Buffer.from(content, "utf-8");
|
|
2394
|
+
if (bytes.length <= MANAGED_LOG_PART_BYTES) {
|
|
2395
|
+
const file = path.join(directory, `${basename}.${extension}`);
|
|
2396
|
+
writeFileSync(file, bytes);
|
|
2397
|
+
return [file];
|
|
2398
|
+
}
|
|
2399
|
+
const files = [];
|
|
2400
|
+
for (let offset = 0, part = 1; offset < bytes.length; part += 1) {
|
|
2401
|
+
let end = Math.min(offset + MANAGED_LOG_PART_BYTES, bytes.length);
|
|
2402
|
+
if (end < bytes.length) {
|
|
2403
|
+
const newline = bytes.lastIndexOf(10, end);
|
|
2404
|
+
if (newline > offset)
|
|
2405
|
+
end = newline + 1;
|
|
2406
|
+
}
|
|
2407
|
+
const file = path.join(directory, `${basename}-part-${String(part).padStart(3, "0")}.${extension}`);
|
|
2408
|
+
writeFileSync(file, bytes.subarray(offset, end));
|
|
2409
|
+
files.push(file);
|
|
2410
|
+
offset = end;
|
|
2411
|
+
}
|
|
2412
|
+
return files;
|
|
622
2413
|
}
|
|
2414
|
+
export { redactManagedLog, redactManagedValue } from "./redaction.js";
|
|
623
2415
|
function mt(contract, zh, en) {
|
|
624
2416
|
return managedText(contract.language, zh, en);
|
|
625
2417
|
}
|