odai-dsh-agent 0.2.6 → 0.2.8
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/LICENSE +21 -21
- package/README.md +140 -138
- package/build/bin/odai-dsh-agent.mjs +12 -12
- package/build/src/installer.d.mts +1 -1
- package/build/src/installer.d.mts.map +1 -1
- package/build/src/installer.mjs +2 -1
- package/build/src/installer.mjs.map +1 -1
- package/package.json +70 -70
- package/preset/odai/agent.cordis.yml +261 -261
- package/preset/odai/preset.yml +3 -3
- package/preset/odai/runtime/context-activation.d.mts +2 -1
- package/preset/odai/runtime/context-activation.d.mts.map +1 -1
- package/preset/odai/runtime/context-activation.mjs +4 -1
- package/preset/odai/runtime/context-activation.mjs.map +1 -1
- package/preset/odai/runtime/governance.d.mts +1 -1
- package/preset/odai/runtime/governance.d.mts.map +1 -1
- package/preset/odai/runtime/governance.mjs +1 -0
- package/preset/odai/runtime/governance.mjs.map +1 -1
- package/preset/odai/runtime/index.d.mts.map +1 -1
- package/preset/odai/runtime/index.mjs +40 -9
- package/preset/odai/runtime/index.mjs.map +1 -1
- package/preset/odai/runtime/lifecycle-runtime.d.mts +3 -1
- package/preset/odai/runtime/lifecycle-runtime.d.mts.map +1 -1
- package/preset/odai/runtime/lifecycle-runtime.mjs +132 -24
- package/preset/odai/runtime/lifecycle-runtime.mjs.map +1 -1
- package/preset/odai/runtime/prompt-runtime.d.mts +1 -0
- package/preset/odai/runtime/prompt-runtime.d.mts.map +1 -1
- package/preset/odai/runtime/prompt-runtime.mjs +19 -6
- package/preset/odai/runtime/prompt-runtime.mjs.map +1 -1
- package/preset/odai/runtime/responsibility-gap.d.mts +1 -1
- package/preset/odai/runtime/responsibility-gap.d.mts.map +1 -1
- package/preset/odai/runtime/responsibility-gap.mjs +7 -5
- package/preset/odai/runtime/responsibility-gap.mjs.map +1 -1
- package/preset/odai/runtime/responsibility-return.d.mts +21 -0
- package/preset/odai/runtime/responsibility-return.d.mts.map +1 -0
- package/preset/odai/runtime/responsibility-return.mjs +96 -0
- package/preset/odai/runtime/responsibility-return.mjs.map +1 -0
- package/preset/odai/runtime/responsibility-scope.d.mts +1 -1
- package/preset/odai/runtime/responsibility-scope.d.mts.map +1 -1
- package/preset/odai/runtime/responsibility-scope.mjs +3 -3
- package/preset/odai/runtime/responsibility-scope.mjs.map +1 -1
- package/preset/odai/runtime/role-overlays.mjs +14 -14
- package/preset/odai/runtime/role-overlays.mjs.map +1 -1
- package/preset/odai/runtime/router.d.mts +2 -1
- package/preset/odai/runtime/router.d.mts.map +1 -1
- package/preset/odai/runtime/router.mjs +23 -3
- package/preset/odai/runtime/router.mjs.map +1 -1
- package/preset/odai/runtime/routing-config.d.mts +15 -5
- package/preset/odai/runtime/routing-config.d.mts.map +1 -1
- package/preset/odai/runtime/routing-config.mjs +113 -44
- package/preset/odai/runtime/routing-config.mjs.map +1 -1
- package/preset/odai/runtime/routing-context.d.mts +17 -0
- package/preset/odai/runtime/routing-context.d.mts.map +1 -1
- package/preset/odai/runtime/routing-context.mjs +204 -48
- package/preset/odai/runtime/routing-context.mjs.map +1 -1
- package/preset/odai/runtime/runtime-config.d.mts.map +1 -1
- package/preset/odai/runtime/runtime-config.mjs +10 -1
- package/preset/odai/runtime/runtime-config.mjs.map +1 -1
- package/preset/odai/runtime/runtime-support.d.mts +3 -1
- package/preset/odai/runtime/runtime-support.d.mts.map +1 -1
- package/preset/odai/runtime/runtime-support.mjs +5 -1
- package/preset/odai/runtime/runtime-support.mjs.map +1 -1
- package/preset/odai/runtime/runtime-types.d.mts +5 -0
- package/preset/odai/runtime/runtime-types.d.mts.map +1 -1
- package/preset/odai/runtime/tool-runtime.d.mts +4 -0
- package/preset/odai/runtime/tool-runtime.d.mts.map +1 -1
- package/preset/odai/runtime/tool-runtime.mjs +49 -4
- package/preset/odai/runtime/tool-runtime.mjs.map +1 -1
- package/preset/odai/skills/odai/agents/openai.yaml +4 -4
- package/preset/odai/skills/odai/assets/claude-agents/agent.md +11 -11
- package/preset/odai/skills/odai/assets/codex-agents/config.toml +11 -11
- package/preset/odai/skills/odai/assets/codex-agents/role.toml +5 -5
- package/preset/odai/skills/odai/assets/copilot-agents/agent.md +13 -13
- package/preset/odai/skills/odai/assets/hooks-policy.example.json +24 -24
- package/preset/odai/skills/odai/assets/routing-roles/controller.md +15 -15
- package/preset/odai/skills/odai/assets/routing-roles/executor.md +11 -11
- package/preset/odai/skills/odai/assets/routing-roles/frontend.md +7 -7
- package/preset/odai/skills/odai/assets/routing-roles/planner.md +29 -29
- package/preset/odai/skills/odai/assets/routing-roles/researcher.md +6 -6
- package/preset/odai/skills/odai/assets/routing-roles/reviewer.md +11 -11
- package/preset/odai/skills/odai/assets/task-state.md +14 -14
- package/preset/odai/skills/odai/manifest.json +37 -37
- package/preset/odai/skills/odai/references/craft.md +54 -54
- package/preset/odai/skills/odai/references/dao.md +30 -30
- package/preset/odai/skills/odai/references/leverage.md +53 -53
- package/preset/odai/skills/odai/references/planning.md +82 -82
- package/preset/odai/skills/odai/references/support.md +36 -36
- package/preset/odai/skills/odai/references/verification.md +23 -23
- package/preset/odai/skills/odai/scripts/build-hooks.mjs +273 -273
- package/preset/odai/skills/odai/scripts/build-routing.mjs +274 -274
- package/preset/odai/skills/odai/scripts/install-routing.mjs +397 -397
- package/preset/odai/skills/odai/scripts/odai-hook.mjs +457 -457
- package/preset/odai/skills/odai/scripts/run-role.mjs +336 -336
- package/preset/odai/skills/odai/scripts/run-routing.mjs +1154 -1154
- package/preset/odai/skills/odai/scripts/verify-routing.mjs +103 -103
|
@@ -1,1154 +1,1154 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import process from "node:process";
|
|
7
|
-
import { spawnSync } from "node:child_process";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
|
-
|
|
10
|
-
process.on("uncaughtException", (error) => {
|
|
11
|
-
console.error(error?.message || String(error));
|
|
12
|
-
process.exitCode = 1;
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
-
const args = parseArgs(process.argv.slice(2));
|
|
17
|
-
|
|
18
|
-
if (args.help) {
|
|
19
|
-
console.log(`用已安装的 Codex odai stage 路由处理一次真实任务。一个持续总控 thread 负责目标、定路、修正与最终交付;只在规划冻结且交接有净收益时启动 executor,只在独立验收能改变放行结果时启动 reviewer。
|
|
20
|
-
|
|
21
|
-
Usage:
|
|
22
|
-
printf '%s' '<request>' | node .codex/odai-run-routing.mjs [--cwd PATH] [--output FILE]
|
|
23
|
-
[--sandbox MODE] [--manifest FILE] [--codex-bin FILE] [--evidence-dir DIR]
|
|
24
|
-
|
|
25
|
-
输入从 stdin 读取。总控必须返回 direct 或 planned,execute 与 review 由宿主机械执行。`);
|
|
26
|
-
process.exit(0);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const manifestFile = path.resolve(args.manifest || path.join(scriptDir, "odai-routing.json"));
|
|
30
|
-
const manifest = readJson(manifestFile, "路由清单");
|
|
31
|
-
if (manifest.host !== "codex") fail(`确定性规划入口当前只支持 Codex,收到:${manifest.host || "unknown"}`);
|
|
32
|
-
const routingMode = manifest.routingPolicy?.mode || "conditional";
|
|
33
|
-
if (routingMode !== "stage") fail("当前安装未启用 Codex stage 路由");
|
|
34
|
-
|
|
35
|
-
const request = readFileSync(0, "utf8").trim();
|
|
36
|
-
if (!request) fail("stdin 中没有用户请求");
|
|
37
|
-
const workdir = path.resolve(args.cwd || process.cwd());
|
|
38
|
-
const decisionRole = "controller";
|
|
39
|
-
const decisionMapping = manifest.mapping?.[decisionRole];
|
|
40
|
-
if (!decisionMapping?.model) fail(`路由清单缺少 ${decisionRole} 映射`);
|
|
41
|
-
if (routingMode === "stage" && (decisionMapping.provider || manifest.host) !== "codex") {
|
|
42
|
-
fail("stage 路由要求 controller 由 Codex 承载,以便在同一任务线程换档");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const roleRunner = path.join(scriptDir, "odai-run-role.mjs");
|
|
46
|
-
if (!existsSync(roleRunner)) fail(`缺少角色 runner:${roleRunner}`);
|
|
47
|
-
const temporary = mkdtempSync(path.join(tmpdir(), "odai-routing-run-"));
|
|
48
|
-
const evidenceDir = args.evidenceDir ? path.resolve(args.evidenceDir) : temporary;
|
|
49
|
-
mkdirSync(evidenceDir, { recursive: true });
|
|
50
|
-
const planFile = path.join(evidenceDir, `${decisionRole}-route.txt`);
|
|
51
|
-
const plannerLog = path.join(evidenceDir, `${decisionRole}-route.jsonl`);
|
|
52
|
-
const plannerEvidence = path.join(evidenceDir, `${decisionRole}-runtime.json`);
|
|
53
|
-
const outputFile = path.resolve(args.output || path.join(evidenceDir, "last-message.txt"));
|
|
54
|
-
const summaryFile = path.join(evidenceDir, "routing-run.json");
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
const runStarted = Date.now();
|
|
58
|
-
const runSummary = { version: 2, mode: routingMode, started_at: new Date().toISOString(), roles: [], stage_transitions: [] };
|
|
59
|
-
const directOwner = "controller";
|
|
60
|
-
const plannerPrompt = `这是 odai 同一总控线程的首次定路。你就是用户任务的总控,不是总控之前的另一个角色;后续可以恢复这个 thread,不创建第二份目标与全局状态。本轮只读定路:不得编辑项目、实施、运行实施后验证或创建 agent;宿主会执行你选定的后续责任。当前 cwd 就是完整项目根;调查只限于该目录内的项目文件,不遍历父目录、兄弟目录、用户目录或其他工作区,也不盘点 .git 与路由器自身的 .codex 产物。
|
|
61
|
-
|
|
62
|
-
先从完整请求判断当前责任。用户给出的原因、手段、数值或完成标准会改变路线时,必须先读取足以裁决它的最少权威原件;未经核实的前提不得冻结进 target、decision 或 accept。用户当前要求的完整结果能在回复中交付时必须 direct,其中描述的组成部分或未来实施切片不改变本轮交付类型;只有当前获授权的交付本身在最小契约核对后仍需多个相互依赖的实施产物时才 planned,不读取实现和测试来预做。一项外部动作受阻而其余结果可当轮交付时仍 direct。高后果提高证据和验收强度,但不自动制造交接。
|
|
63
|
-
|
|
64
|
-
准备 direct 写入时,按目标对象在既有项目约定与验证入口中做一次有界发现;不能只看实现文件或通用启动配置,就宣称项目没有专用契约或测试。现成验证入口须原样运行,验证需求本身不授权修改其载体。
|
|
65
|
-
|
|
66
|
-
若结果依赖尚不可用的能力,且下一步涉及安装、启用、联网、凭据或更高权限,按 odai leverage 规则和项目权威来源处理。同意前让用户看到会改变决定的真实影响、继续动作、验证和替代差异,不把关键信息只留在内部路由中。
|
|
67
|
-
|
|
68
|
-
当前请求只需回答或只读交付,并且本轮已取得充分证据时才用 direct,直接给面向用户的完整结果。当前请求需要写入时必须 planned:同一总控上下文继续实施更可靠或更省时,写 execute: controller;只有决定已经冻结、实施有界可验且新上下文有可观察净收益时,才写 execute: executor。高后果本身只提高证据和验收强度,不强制拆开实施。新证据推翻路线时回到总控,只改受影响部分。
|
|
69
|
-
|
|
70
|
-
验收已定义结果、范围与保持项时,低风险、可逆且可测试的局部实现选择由执行责任自主决定;不要因缺少唯一先例而停止。只有选择会改变业务语义、公共契约、用户取舍或高代价后果时才升级决定。
|
|
71
|
-
|
|
72
|
-
已安装能力映射:
|
|
73
|
-
${JSON.stringify(manifest.mapping, null, 2)}
|
|
74
|
-
|
|
75
|
-
用户请求:
|
|
76
|
-
${request}`;
|
|
77
|
-
const plannerRepositoryBefore = captureRepositoryState();
|
|
78
|
-
const decisionArgs = [
|
|
79
|
-
"--role", decisionRole, "--manifest", manifestFile, "--cwd", workdir,
|
|
80
|
-
"--output", planFile, "--evidence", plannerEvidence, "--sandbox", args.sandbox,
|
|
81
|
-
"--route-card",
|
|
82
|
-
];
|
|
83
|
-
if (decisionRole === "planner") decisionArgs.push("--planner-may-complete-direct");
|
|
84
|
-
const plannerResult = runProviderRole(decisionArgs, plannerPrompt, Math.min(args.timeout, 240));
|
|
85
|
-
writeFileSync(plannerLog, plannerResult.output, "utf8");
|
|
86
|
-
if (plannerResult.status !== 0) fail(`${decisionRole} 定路失败:${tail(plannerResult.output)}`);
|
|
87
|
-
const plannerObserved = readJson(plannerEvidence, `${decisionRole} 运行证据`);
|
|
88
|
-
requireObservedRole(decisionRole, plannerObserved);
|
|
89
|
-
const controllerThreadId = routingMode === "stage" ? plannerObserved.observed?.thread_id : "";
|
|
90
|
-
if (routingMode === "stage" && !controllerThreadId) fail("controller 没有形成可核实的 Codex thread ID");
|
|
91
|
-
runSummary.controller_thread_id = controllerThreadId || null;
|
|
92
|
-
const decisionRoleLabel = "controller-route";
|
|
93
|
-
runSummary.roles.push(roleSummary(decisionRoleLabel, plannerObserved));
|
|
94
|
-
runSummary.stage_transitions.push({ stage: "decision", role: decisionRole, reason: "initial-capable-first" });
|
|
95
|
-
|
|
96
|
-
let plan = existsSync(planFile) ? normalizePlanCard(readFileSync(planFile, "utf8").trim()) : "";
|
|
97
|
-
let planState = validatePlan(plan, directOwner);
|
|
98
|
-
const plannerRepositoryAfter = captureRepositoryState();
|
|
99
|
-
if (planState.mode === "planned" && repositoryStateChanged(plannerRepositoryBefore, plannerRepositoryAfter)) {
|
|
100
|
-
fail(`${decisionRole} 选择 planned 却提前修改了项目;复杂任务只能交付方案,不能预做实现`);
|
|
101
|
-
}
|
|
102
|
-
let activePlannerObserved = plannerObserved;
|
|
103
|
-
const initialExecute = planState.execute;
|
|
104
|
-
const initialReviewIds = [...planState.reviewIds];
|
|
105
|
-
let executionResult;
|
|
106
|
-
if (planState.mode === "direct") {
|
|
107
|
-
executionResult = captureDirectCompletion({
|
|
108
|
-
plan, planState, plannerObserved: activePlannerObserved,
|
|
109
|
-
repositoryBefore: plannerRepositoryBefore, repositoryAfter: plannerRepositoryAfter,
|
|
110
|
-
directRole: directOwner,
|
|
111
|
-
});
|
|
112
|
-
} else {
|
|
113
|
-
try {
|
|
114
|
-
executionResult = runSelectedExecution({
|
|
115
|
-
plan, planState, plannerObserved: activePlannerObserved, runSummary, suffix: "",
|
|
116
|
-
sessionId: planState.execute === "controller" ? controllerThreadId : "",
|
|
117
|
-
});
|
|
118
|
-
} catch (error) {
|
|
119
|
-
if (routingMode !== "stage" || planState.execute !== "executor" || !isRecoverableExecutionError(error)) throw error;
|
|
120
|
-
executionResult = runControllerRecovery({
|
|
121
|
-
plan, planState, plannerObserved: activePlannerObserved, runSummary,
|
|
122
|
-
sessionId: controllerThreadId, failure: error?.message || String(error),
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
let rawOutput = executionResult.output;
|
|
127
|
-
let reviewState = null;
|
|
128
|
-
if (planState.reviewIds.length > 0) {
|
|
129
|
-
reviewState = runReview({
|
|
130
|
-
request, plan, planState, executionResult, plannerObserved: activePlannerObserved, runSummary, round: 1,
|
|
131
|
-
});
|
|
132
|
-
if (reviewState.route === "execution") {
|
|
133
|
-
executionResult = runExecutionCorrection({
|
|
134
|
-
request, plan, planState, reviewState, plannerObserved: activePlannerObserved, runSummary,
|
|
135
|
-
sessionId: executionResult.observed?.observed?.thread_id || "",
|
|
136
|
-
});
|
|
137
|
-
rawOutput = executionResult.output;
|
|
138
|
-
reviewState = runReview({
|
|
139
|
-
request, plan, planState, executionResult, plannerObserved: activePlannerObserved, runSummary, round: 2,
|
|
140
|
-
});
|
|
141
|
-
} else if (reviewState.route === "planning") {
|
|
142
|
-
const replanned = runIncrementalReplan({
|
|
143
|
-
request, plan, planState, reviewState, plannerObserved: activePlannerObserved, runSummary,
|
|
144
|
-
sessionId: controllerThreadId, role: routingMode === "stage" ? "controller" : "planner",
|
|
145
|
-
directOwner,
|
|
146
|
-
});
|
|
147
|
-
plan = replanned.plan;
|
|
148
|
-
planState = replanned.planState;
|
|
149
|
-
activePlannerObserved = replanned.observed;
|
|
150
|
-
if (planState.reviewIds.length === 0) {
|
|
151
|
-
fail("增量重规划必须保留 finding 所涉验收 ID 的独立复验");
|
|
152
|
-
}
|
|
153
|
-
executionResult = runSelectedExecution({
|
|
154
|
-
plan, planState, plannerObserved: activePlannerObserved, runSummary, suffix: "-recovery",
|
|
155
|
-
sessionId: planState.execute === "controller" ? controllerThreadId : "",
|
|
156
|
-
});
|
|
157
|
-
rawOutput = executionResult.output;
|
|
158
|
-
reviewState = runReview({
|
|
159
|
-
request, plan, planState, executionResult, plannerObserved: activePlannerObserved, runSummary, round: 2,
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
const output = planState.mode === "direct"
|
|
164
|
-
? `${String(rawOutput || "").trim()}\n`
|
|
165
|
-
: renderDelivery(rawOutput, planState.acceptance, reviewState, planState.reviewIds);
|
|
166
|
-
writeFileSync(outputFile, output, "utf8");
|
|
167
|
-
runSummary.completed_at = new Date().toISOString();
|
|
168
|
-
runSummary.initial_execute = initialExecute;
|
|
169
|
-
runSummary.execute = planState.execute;
|
|
170
|
-
runSummary.initial_review_ids = initialReviewIds;
|
|
171
|
-
runSummary.review_ids = planState.reviewIds;
|
|
172
|
-
runSummary.execution_context = planState.mode === "direct"
|
|
173
|
-
? `${directOwner}-direct-same-context`
|
|
174
|
-
: planState.execute === "controller"
|
|
175
|
-
? "controller-resumed-same-context"
|
|
176
|
-
: "bounded-fresh-execution-context";
|
|
177
|
-
runSummary.controller_context = routingMode === "stage" ? "persistent-task-thread" : "host-owned-no-model-call";
|
|
178
|
-
runSummary.review_status = reviewState?.status || "not-requested";
|
|
179
|
-
runSummary.review_route = reviewState?.route || "none";
|
|
180
|
-
runSummary.duration_ms = Date.now() - runStarted;
|
|
181
|
-
runSummary.totals = summarizeRoles(runSummary.roles);
|
|
182
|
-
writeFileSync(summaryFile, `${JSON.stringify(runSummary, null, 2)}\n`, "utf8");
|
|
183
|
-
process.stdout.write(output);
|
|
184
|
-
if (!output.endsWith("\n")) process.stdout.write("\n");
|
|
185
|
-
if (reviewState?.status === "fail") process.exitCode = 1;
|
|
186
|
-
else if (reviewState?.status === "unresolved") process.exitCode = 2;
|
|
187
|
-
} finally {
|
|
188
|
-
if (!args.evidenceDir) rmSync(temporary, { recursive: true, force: true });
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function runProviderRole(commandArgs, input, timeout = args.timeout) {
|
|
192
|
-
if (args.codexBin) commandArgs.push("--codex-bin", args.codexBin);
|
|
193
|
-
commandArgs.push("--timeout", String(timeout));
|
|
194
|
-
const result = spawnSync(process.execPath, [roleRunner, ...commandArgs], {
|
|
195
|
-
cwd: workdir,
|
|
196
|
-
input,
|
|
197
|
-
env: { ...process.env, ODAI_ROUTING_ACTIVE: "1" },
|
|
198
|
-
encoding: "utf8",
|
|
199
|
-
windowsHide: true,
|
|
200
|
-
timeout: timeout * 1000,
|
|
201
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
202
|
-
});
|
|
203
|
-
if (result.error) fail(result.error.message);
|
|
204
|
-
return { status: result.status, output: `${result.stdout || ""}${result.stderr || ""}` };
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function executionContext() {
|
|
208
|
-
return `原始用户请求由总控、planner 与 reviewer 保管,用于防遗漏、修正路由和最终验收;受托执行者只接收已经覆盖该请求的冻结方案,不重新解释或扩写需求。`;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function buildExecutionPrompt(role, plan, planState, plannerObserved) {
|
|
212
|
-
return `按 odai 宿主已经核实的规划承担一次 ${role} 执行切片。只在规划的允许范围内形成真实产物,不重新规划、不扩大目标、不批准最终交付。
|
|
213
|
-
|
|
214
|
-
${executionContext()}
|
|
215
|
-
|
|
216
|
-
已核实规划(这是决定与边界的 owner,不要求重新证明):
|
|
217
|
-
${plan}
|
|
218
|
-
|
|
219
|
-
planner 已核实的来源指针如下。不要为了复述规划而重读;只读取完成实际产物仍必需的实现原件:
|
|
220
|
-
${formatCapturedEvidence(plannerObserved, plan)}
|
|
221
|
-
|
|
222
|
-
${planState.reviewIds.length > 0
|
|
223
|
-
? `独立 reviewer 负责 ${planState.reviewIds.join(", ")} 的最终判定。不要先跑基线检查;完成改动后,每项必要验证只运行一次。只有检查失败并完成针对性修正后才可重跑。`
|
|
224
|
-
: "不要先跑基线检查;完成改动后,每项闭合验收所需的检查只运行一次,失败并修正后才可重跑。"}
|
|
225
|
-
决定性验证成功且没有新的失败证据时立即停止分析与检索,直接形成回交和 closeout;不为再次解释、安心复核或等待 reviewer 继续推理。
|
|
226
|
-
回交时点名每份改变决定的权威来源路径;宿主会把未修改的来源原件纳入证据包,不能只说“已查阅”。执行中从权威来源取得的、会改变复验结果的准确对象、条件、输入或位置须保留,不能被规划中的宽泛说法或简洁摘要稀释。需要验证不自动授权修改测试、基准、文档或其他证据源;项目已点名现成验证入口时先原样运行,不能为了产生更漂亮或更完整的证据改变它。用户当前就要审阅或使用的正文、判断或其他内容必须在回交中自足给出;除非契约明确只交文件,文件路径不能替代实际内容。写入持久文件必须有请求、项目契约或规划对该载体的明确需要,不为暂存内容或证明完成自造文件。
|
|
227
|
-
新建或替换持久 owner 时,规划捕获的既有 owner 查找结果必须进入对应验收项与最终回交;没有可追溯证据才补最小查找,不重复已经捕获的调查。
|
|
228
|
-
指定验证器因当前入口或依赖失败时,先尝试用现有兼容入口运行同一验证器;不得把自写近似检查称为等价。权威项目原件已明确某项环境、入口或数据不存在,且当前授权不包含补建时,不再加载外部能力、安装工具或广泛搜索来重复证明缺失;准确保留未决与继续条件。规划中执行后仍成立的 stop 必须保留在面向用户的回交中。
|
|
229
|
-
完成后按宿主提供的结构化输出契约回交:delivery 是面向用户的简洁结果;acceptance 覆盖全部验收 ID:${planState.acceptance.map((item) => item.id).join(", ")},每项给 status、实际 evidence 与 next。verified 的 next 必须为空,未决或失败必须给继续条件;未来仍需观察的属性写 unresolved。不得把 reviewer 尚未作出的判断写成独立验收通过。`;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function captureDirectCompletion({ plan, planState, plannerObserved, repositoryBefore, repositoryAfter, directRole = "planner" }) {
|
|
233
|
-
const output = directExecutionOutput(plan);
|
|
234
|
-
const packet = buildEvidencePacket({
|
|
235
|
-
plan,
|
|
236
|
-
planState,
|
|
237
|
-
plannerObserved,
|
|
238
|
-
executionObserved: plannerObserved,
|
|
239
|
-
executionOutput: output,
|
|
240
|
-
repositoryBefore,
|
|
241
|
-
repositoryAfter,
|
|
242
|
-
});
|
|
243
|
-
const packetFile = path.join(evidenceDir, "evidence-packet-direct.json");
|
|
244
|
-
writeFileSync(packetFile, `${JSON.stringify(packet, null, 2)}\n`, "utf8");
|
|
245
|
-
return {
|
|
246
|
-
role: directRole,
|
|
247
|
-
output,
|
|
248
|
-
evidence: plannerEvidence,
|
|
249
|
-
observed: plannerObserved,
|
|
250
|
-
packet,
|
|
251
|
-
packetFile,
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function directExecutionOutput(plan) {
|
|
256
|
-
const lines = String(plan || "").trim().split(/\r?\n/);
|
|
257
|
-
const first = lines.findIndex((line) => line.trim());
|
|
258
|
-
if (first < 0 || !/^\s*mode\s*[::]\s*direct\s*$/i.test(lines[first])) {
|
|
259
|
-
fail("direct 交付的首个非空行必须且只能是 mode: direct");
|
|
260
|
-
}
|
|
261
|
-
const output = lines.slice(first + 1).join("\n").trim();
|
|
262
|
-
if (!output) fail("direct 没有形成面向用户的完整交付");
|
|
263
|
-
if (/<odai_(?:delivery|closeout|plan)>/i.test(output)) {
|
|
264
|
-
fail("direct 交付不得包含内部路由标签");
|
|
265
|
-
}
|
|
266
|
-
return output;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function runSelectedExecution({ plan, planState, plannerObserved, runSummary, suffix, sessionId = "" }) {
|
|
270
|
-
const role = planState.execute;
|
|
271
|
-
const roleOutput = path.join(evidenceDir, `${role}-result${suffix}.txt`);
|
|
272
|
-
const roleEvidence = path.join(evidenceDir, `${role}-runtime${suffix}.json`);
|
|
273
|
-
const roleLog = path.join(evidenceDir, `${role}${suffix}.log`);
|
|
274
|
-
const before = captureRepositoryState();
|
|
275
|
-
const rolePrompt = buildExecutionPrompt(role, plan, planState, plannerObserved);
|
|
276
|
-
const command = [
|
|
277
|
-
"--role", role, "--manifest", manifestFile, "--cwd", workdir,
|
|
278
|
-
"--output", roleOutput, "--evidence", roleEvidence, "--sandbox", args.sandbox,
|
|
279
|
-
"--closeout-ids", planState.acceptance.map((item) => item.id).join(","),
|
|
280
|
-
];
|
|
281
|
-
if (sessionId) command.push("--session-id", sessionId);
|
|
282
|
-
const executed = runProviderRole(command, rolePrompt);
|
|
283
|
-
writeFileSync(roleLog, executed.output, "utf8");
|
|
284
|
-
if (executed.status !== 0) fail(`${role} 执行失败:${tail(executed.output)}`);
|
|
285
|
-
const observed = readJson(roleEvidence, `${role} 运行证据`);
|
|
286
|
-
requireObservedRole(role, observed);
|
|
287
|
-
if (sessionId) requireSameThread(sessionId, observed, role);
|
|
288
|
-
runSummary.roles.push(roleSummary(`${role}${suffix}`, observed));
|
|
289
|
-
if (routingMode === "stage") {
|
|
290
|
-
runSummary.stage_transitions.push({
|
|
291
|
-
stage: "implementation",
|
|
292
|
-
role,
|
|
293
|
-
reason: sessionId
|
|
294
|
-
? role === "controller" ? "same-controller-continued-after-read-only-route" : "finding-correction-in-original-execution-context"
|
|
295
|
-
: "frozen-bounded-work-in-fresh-context",
|
|
296
|
-
thread_id: observed.observed?.thread_id || null,
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
const output = existsSync(roleOutput) ? readFileSync(roleOutput, "utf8").trim() : executed.output.trim();
|
|
300
|
-
validateCloseout(output, planState.acceptance, `${role} 回交`);
|
|
301
|
-
const packet = buildEvidencePacket({
|
|
302
|
-
plan,
|
|
303
|
-
planState,
|
|
304
|
-
plannerObserved,
|
|
305
|
-
executionObserved: observed,
|
|
306
|
-
executionOutput: output,
|
|
307
|
-
repositoryBefore: before,
|
|
308
|
-
repositoryAfter: captureRepositoryState(),
|
|
309
|
-
});
|
|
310
|
-
const packetFile = path.join(evidenceDir, `evidence-packet${suffix}.json`);
|
|
311
|
-
writeFileSync(packetFile, `${JSON.stringify(packet, null, 2)}\n`, "utf8");
|
|
312
|
-
return {
|
|
313
|
-
role,
|
|
314
|
-
output,
|
|
315
|
-
evidence: roleEvidence,
|
|
316
|
-
observed,
|
|
317
|
-
packet,
|
|
318
|
-
packetFile,
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function runControllerRecovery({ plan, planState, plannerObserved, runSummary, sessionId, failure }) {
|
|
323
|
-
const roleOutput = path.join(evidenceDir, "controller-recovery-result.txt");
|
|
324
|
-
const roleEvidence = path.join(evidenceDir, "controller-recovery-runtime.json");
|
|
325
|
-
const roleLog = path.join(evidenceDir, "controller-recovery.log");
|
|
326
|
-
const before = captureRepositoryState();
|
|
327
|
-
const prompt = `执行阶段出现了可观察失败,宿主现已把同一任务线程升回 controller 档。你仍是原总控;不要重做完整调查,也不要为失败扩大目标。
|
|
328
|
-
|
|
329
|
-
冻结路线:
|
|
330
|
-
${plan}
|
|
331
|
-
|
|
332
|
-
执行失败:
|
|
333
|
-
${failure}
|
|
334
|
-
|
|
335
|
-
只检查失败实际暴露的缺口。路线仍成立时,在原边界内完成最小修正与必要验证;新证据推翻决定时,只修正受影响决定后继续完成。不得丢失验收 ID:${planState.acceptance.map((item) => item.id).join(", ")}。
|
|
336
|
-
按宿主提供的结构化输出契约回交,delivery 给面向用户的简洁结果,acceptance 覆盖全部验收 ID;每项包含 status、实际 evidence 与 next,verified 的 next 为空,未决或失败时给真实继续条件。`;
|
|
337
|
-
const recovered = runProviderRole([
|
|
338
|
-
"--role", "controller", "--manifest", manifestFile, "--cwd", workdir,
|
|
339
|
-
"--output", roleOutput, "--evidence", roleEvidence, "--sandbox", args.sandbox,
|
|
340
|
-
"--closeout-ids", planState.acceptance.map((item) => item.id).join(","),
|
|
341
|
-
"--session-id", sessionId,
|
|
342
|
-
], prompt);
|
|
343
|
-
writeFileSync(roleLog, recovered.output, "utf8");
|
|
344
|
-
if (recovered.status !== 0) fail(`controller 升档恢复失败:${tail(recovered.output)}`);
|
|
345
|
-
const observed = readJson(roleEvidence, "controller 升档恢复证据");
|
|
346
|
-
requireObservedRole("controller", observed);
|
|
347
|
-
requireSameThread(sessionId, observed, "controller");
|
|
348
|
-
runSummary.roles.push(roleSummary("controller-recovery", observed));
|
|
349
|
-
runSummary.stage_transitions.push({ stage: "recovery", role: "controller", reason: "execution-failure", thread_id: sessionId });
|
|
350
|
-
const output = existsSync(roleOutput) ? readFileSync(roleOutput, "utf8").trim() : recovered.output.trim();
|
|
351
|
-
validateCloseout(output, planState.acceptance, "controller 恢复回交");
|
|
352
|
-
const packet = buildEvidencePacket({
|
|
353
|
-
plan,
|
|
354
|
-
planState,
|
|
355
|
-
plannerObserved,
|
|
356
|
-
executionObserved: observed,
|
|
357
|
-
executionOutput: output,
|
|
358
|
-
repositoryBefore: before,
|
|
359
|
-
repositoryAfter: captureRepositoryState(),
|
|
360
|
-
executionFailure: failure,
|
|
361
|
-
});
|
|
362
|
-
const packetFile = path.join(evidenceDir, "evidence-packet-controller-recovery.json");
|
|
363
|
-
writeFileSync(packetFile, `${JSON.stringify(packet, null, 2)}\n`, "utf8");
|
|
364
|
-
return { role: "controller", output, evidence: roleEvidence, observed, packet, packetFile };
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function isRecoverableExecutionError(error) {
|
|
368
|
-
const message = String(error?.message || error || "");
|
|
369
|
-
return !/(实际模型不匹配|恢复了错误线程|没有恢复总控线程|路由清单|托管安装)/.test(message);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function runReview({ request: userRequest, plan, planState, executionResult, plannerObserved, runSummary, round }) {
|
|
373
|
-
const suffix = round === 1 ? "" : `-${round}`;
|
|
374
|
-
const resultFile = path.join(evidenceDir, `reviewer-result${suffix}.txt`);
|
|
375
|
-
const evidenceFile = path.join(evidenceDir, `reviewer-runtime${suffix}.json`);
|
|
376
|
-
const logFile = path.join(evidenceDir, `reviewer${suffix}.log`);
|
|
377
|
-
const reviewPrompt = `独立验收当前实际产物。宿主已经提供自足的不可变证据包;不得调用工具、读取仓库或重复运行已经成功的确定性检查。先核对完整 accept 是否覆盖原始请求中每个会改变结果的要求;遗漏属于方案或验收设计缺陷,使用 route: planning。覆盖成立后,只检查定路中点名的这些 ID:${planState.reviewIds.join(", ")}。
|
|
378
|
-
|
|
379
|
-
宿主不可变证据包(${executionResult.packetFile},其中用户请求、规划与执行回交各只有一份):
|
|
380
|
-
${JSON.stringify(executionResult.packet, null, 2)}
|
|
381
|
-
|
|
382
|
-
只依据以上方案、回交与证据包作出判断。执行输出和执行者新写的验证记录都只是主张;命令、退出码与输出只认 execution.observed.verification_evidence,且命令本身必须能产生所声称的观察。证据缺失或互相冲突时,不自行重新取证:方案或验收设计有误用 route: planning,实施偏差用 route: execution,缺用户决定或外部条件用 unresolved 的相应 route。finding 不授权新增证明文件,只指出原产物的最小修正或应取得的真实观察。首行只写 pass、fail 或 unresolved;第二行按角色契约只写 route。随后逐项给出证据、最小修正或继续条件。不得自行修复。`;
|
|
383
|
-
const reviewed = runProviderRole([
|
|
384
|
-
"--role", "reviewer", "--manifest", manifestFile, "--cwd", workdir,
|
|
385
|
-
"--output", resultFile, "--evidence", evidenceFile,
|
|
386
|
-
], reviewPrompt);
|
|
387
|
-
writeFileSync(logFile, reviewed.output, "utf8");
|
|
388
|
-
if (reviewed.status !== 0) fail(`reviewer 验收失败:${tail(reviewed.output)}`);
|
|
389
|
-
const observed = readJson(evidenceFile, "reviewer 运行证据");
|
|
390
|
-
requireObservedRole("reviewer", observed);
|
|
391
|
-
if (observed.observed?.tool_use_detected === true) {
|
|
392
|
-
fail("reviewer 已收到完整宿主证据包却仍调用工具,拒绝把重复取证计作有效验收");
|
|
393
|
-
}
|
|
394
|
-
runSummary.roles.push(roleSummary(`reviewer-${round}`, observed));
|
|
395
|
-
runSummary.stage_transitions.push({ stage: "review", role: "reviewer", reason: "independent-acceptance-can-change-release" });
|
|
396
|
-
const output = existsSync(resultFile) ? readFileSync(resultFile, "utf8").trim() : reviewed.output.trim();
|
|
397
|
-
return { ...parseReviewDecision(output), output, round };
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function runIncrementalReplan({ request: userRequest, plan, planState, reviewState, plannerObserved, runSummary, sessionId = "", role = "planner", directOwner = "planner" }) {
|
|
401
|
-
const resultFile = path.join(evidenceDir, `${role}-replan.txt`);
|
|
402
|
-
const evidenceFile = path.join(evidenceDir, `${role}-replan-runtime.json`);
|
|
403
|
-
const logFile = path.join(evidenceDir, `${role}-replan.log`);
|
|
404
|
-
const prompt = `这是 reviewer 已判定为 route: planning 的增量重规划。只处理 finding 推翻的决定;不要重做首次调查、编辑文件或实施。
|
|
405
|
-
|
|
406
|
-
用户请求:
|
|
407
|
-
${userRequest}
|
|
408
|
-
|
|
409
|
-
当前规划:
|
|
410
|
-
${plan}
|
|
411
|
-
|
|
412
|
-
首次规划已取得的证据:
|
|
413
|
-
${formatCapturedEvidence(plannerObserved, plan)}
|
|
414
|
-
|
|
415
|
-
独立验收 finding:
|
|
416
|
-
${reviewState.output}
|
|
417
|
-
|
|
418
|
-
只利用已有证据做增量修正,不重复首次调查;仅当 finding 明确指出证据包缺少一个会改变路线的事实时,才补取该事实。返回同一格式的完整替换路由。保留未受影响验收 ID;为 finding 涉及且现已可取得证据的属性继续点名 reviewer。`;
|
|
419
|
-
const command = [
|
|
420
|
-
"--role", role, "--manifest", manifestFile, "--cwd", workdir,
|
|
421
|
-
"--output", resultFile, "--evidence", evidenceFile,
|
|
422
|
-
];
|
|
423
|
-
if (sessionId) command.push("--session-id", sessionId);
|
|
424
|
-
else if (role === "planner") command.push("--planner-may-complete-direct");
|
|
425
|
-
const replanned = runProviderRole(command, prompt);
|
|
426
|
-
writeFileSync(logFile, replanned.output, "utf8");
|
|
427
|
-
if (replanned.status !== 0) fail(`${role} 增量重规划失败:${tail(replanned.output)}`);
|
|
428
|
-
const observed = readJson(evidenceFile, `${role} 增量重规划证据`);
|
|
429
|
-
requireObservedRole(role, observed);
|
|
430
|
-
if (sessionId) requireSameThread(sessionId, observed, role);
|
|
431
|
-
runSummary.roles.push(roleSummary(`${role}-replan`, observed));
|
|
432
|
-
if (sessionId) runSummary.stage_transitions.push({ stage: "decision", role, reason: "review-invalidated-route", thread_id: sessionId });
|
|
433
|
-
const nextPlan = existsSync(resultFile) ? readFileSync(resultFile, "utf8").trim() : replanned.output.trim();
|
|
434
|
-
const nextState = validatePlan(nextPlan, directOwner);
|
|
435
|
-
const nextIds = new Set(nextState.acceptance.map((item) => item.id));
|
|
436
|
-
const lost = planState.reviewIds.filter((id) => !nextIds.has(id));
|
|
437
|
-
if (lost.length > 0) fail(`增量重规划丢失 finding 所涉验收 ID:${lost.join(", ")}`);
|
|
438
|
-
return { plan: nextPlan, planState: nextState, observed };
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function runExecutionCorrection({ plan, planState, reviewState, plannerObserved, runSummary, sessionId = "" }) {
|
|
442
|
-
const role = planState.execute;
|
|
443
|
-
const resultFile = path.join(evidenceDir, `${role}-repair-result.txt`);
|
|
444
|
-
const evidenceFile = path.join(evidenceDir, `${role}-repair-runtime.json`);
|
|
445
|
-
const logFile = path.join(evidenceDir, `${role}-repair.log`);
|
|
446
|
-
const prompt = `reviewer 已将 finding 判定为实施偏差,原规划仍成立。你是原执行承载 ${role};只修正 finding 指向的实际产物,不重新规划、不扩大范围、不改写验收源,也不批准最终交付。
|
|
447
|
-
|
|
448
|
-
已核实规划:
|
|
449
|
-
${plan}
|
|
450
|
-
|
|
451
|
-
[首次规划证据]
|
|
452
|
-
${formatCapturedEvidence(plannerObserved, plan)}
|
|
453
|
-
|
|
454
|
-
[独立验收 finding]
|
|
455
|
-
${reviewState.output}
|
|
456
|
-
|
|
457
|
-
finding 只授权修正原产物,不授权新建验证报告、检查日志或其他交付外文件。需要补证时运行能真实判定该属性的现有检查并回交宿主捕获的原始命令结果;检查不存在或当前不可运行就保留未决,不能用注释、示例输出、自写记录或叙述替代执行证据。
|
|
458
|
-
|
|
459
|
-
不要重复首次调查或已经成功且未受 finding 影响的检查。完成最小修正与相称验证后,按宿主提供的结构化输出契约回交:delivery 给面向用户的简洁结果,acceptance 覆盖 ${planState.acceptance.map((item) => item.id).join(", ")};每项含 status、实际 evidence 与 next,verified 的 next 为空,未闭合项给真实继续条件。宿主与 reviewer 负责最终收口。`;
|
|
460
|
-
const before = captureRepositoryState();
|
|
461
|
-
const command = [
|
|
462
|
-
"--role", role, "--manifest", manifestFile, "--cwd", workdir,
|
|
463
|
-
"--output", resultFile, "--evidence", evidenceFile, "--sandbox", args.sandbox,
|
|
464
|
-
"--closeout-ids", planState.acceptance.map((item) => item.id).join(","),
|
|
465
|
-
];
|
|
466
|
-
if (sessionId) command.push("--session-id", sessionId);
|
|
467
|
-
const repaired = runProviderRole(command, prompt);
|
|
468
|
-
writeFileSync(logFile, repaired.output, "utf8");
|
|
469
|
-
if (repaired.status !== 0) fail(`${role} 修正失败:${tail(repaired.output)}`);
|
|
470
|
-
const observed = readJson(evidenceFile, `${role} 修正证据`);
|
|
471
|
-
requireObservedRole(role, observed);
|
|
472
|
-
if (sessionId) requireSameThread(sessionId, observed, role);
|
|
473
|
-
runSummary.roles.push(roleSummary(`${role}-repair`, observed));
|
|
474
|
-
if (sessionId) runSummary.stage_transitions.push({ stage: "implementation", role, reason: "review-found-execution-defect", thread_id: sessionId });
|
|
475
|
-
const output = existsSync(resultFile) ? readFileSync(resultFile, "utf8").trim() : repaired.output.trim();
|
|
476
|
-
validateCloseout(output, planState.acceptance, `${role} 修正回交`);
|
|
477
|
-
const packet = buildEvidencePacket({
|
|
478
|
-
plan,
|
|
479
|
-
planState,
|
|
480
|
-
plannerObserved,
|
|
481
|
-
executionObserved: observed,
|
|
482
|
-
executionOutput: output,
|
|
483
|
-
repositoryBefore: before,
|
|
484
|
-
repositoryAfter: captureRepositoryState(),
|
|
485
|
-
reviewFinding: reviewState.output,
|
|
486
|
-
});
|
|
487
|
-
const packetFile = path.join(evidenceDir, "evidence-packet-repair.json");
|
|
488
|
-
writeFileSync(packetFile, `${JSON.stringify(packet, null, 2)}\n`, "utf8");
|
|
489
|
-
return {
|
|
490
|
-
role,
|
|
491
|
-
output,
|
|
492
|
-
evidence: evidenceFile,
|
|
493
|
-
observed,
|
|
494
|
-
packet,
|
|
495
|
-
packetFile,
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
function formatCapturedEvidence(evidence, plan) {
|
|
500
|
-
const observed = evidence?.observed || {};
|
|
501
|
-
const compact = {
|
|
502
|
-
provider: observed.provider || evidence?.requested?.provider || null,
|
|
503
|
-
models: observed.models || [],
|
|
504
|
-
cited_paths: plannerEvidencePointers(plan),
|
|
505
|
-
};
|
|
506
|
-
return JSON.stringify(compact, null, 2);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
function plannerEvidencePointers(plan) {
|
|
510
|
-
const seen = new Set();
|
|
511
|
-
const pointers = [];
|
|
512
|
-
const source = String(plan || "");
|
|
513
|
-
const candidates = [
|
|
514
|
-
...[...source.matchAll(/`([^`\r\n]+)`/g)].map((match) => match[1]),
|
|
515
|
-
...[...source.matchAll(/\[[^\]\r\n]*\]\(([^)\r\n]+)\)/g)].map((match) => match[1]),
|
|
516
|
-
...[...source.matchAll(/\b(?:[A-Za-z0-9_.-]+\/)+[A-Za-z0-9_.-]+\b/g)].map((match) => match[0]),
|
|
517
|
-
];
|
|
518
|
-
for (const rawCandidate of candidates) {
|
|
519
|
-
const raw = String(rawCandidate || "").trim().replace(/(?::\d+)?(?:#.*)?$/, "");
|
|
520
|
-
if (!raw || raw.includes(" ") || raw.includes("*") || /^[a-z]+:\/\//i.test(raw)) continue;
|
|
521
|
-
const file = path.resolve(workdir, raw.replace(/^\.\//, ""));
|
|
522
|
-
if (file !== workdir && !file.startsWith(`${workdir}${path.sep}`)) continue;
|
|
523
|
-
if (!existsSync(file)) continue;
|
|
524
|
-
const candidate = path.relative(workdir, file) || ".";
|
|
525
|
-
if (seen.has(candidate)) continue;
|
|
526
|
-
seen.add(candidate);
|
|
527
|
-
pointers.push(candidate);
|
|
528
|
-
}
|
|
529
|
-
return pointers;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
function captureCitedSources(value, repositoryBefore, repositoryAfter) {
|
|
533
|
-
const changed = repositoryChangedPaths(repositoryBefore, repositoryAfter);
|
|
534
|
-
let remaining = 50000;
|
|
535
|
-
return plannerEvidencePointers(value).flatMap((relativePath) => {
|
|
536
|
-
if (changed.has(relativePath)) return [];
|
|
537
|
-
if (remaining <= 0) return [];
|
|
538
|
-
try {
|
|
539
|
-
const data = readFileSync(path.resolve(workdir, relativePath));
|
|
540
|
-
if (data.includes(0)) return [{ path: relativePath, content: "[binary file omitted]" }];
|
|
541
|
-
const content = clip(data.toString("utf8"), Math.min(remaining, 20000));
|
|
542
|
-
remaining -= content.length;
|
|
543
|
-
return [{ path: relativePath, content }];
|
|
544
|
-
} catch {
|
|
545
|
-
return [{ path: relativePath, content: "[unreadable file]" }];
|
|
546
|
-
}
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
function repositoryChangedPaths(before, after) {
|
|
551
|
-
const candidates = new Set();
|
|
552
|
-
for (const state of [before, after]) {
|
|
553
|
-
for (const item of state?.untracked_files || []) candidates.add(item.path);
|
|
554
|
-
for (const line of String(state?.status?.output || "").split(/\r?\n/)) {
|
|
555
|
-
if (line.length < 4) continue;
|
|
556
|
-
const pathText = line.slice(3).trim();
|
|
557
|
-
const parts = pathText.includes(" -> ") ? pathText.split(" -> ") : [pathText];
|
|
558
|
-
for (const item of parts) if (item) candidates.add(item.replace(/^"|"$/g, ""));
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
return new Set([...candidates].filter((relativePath) => repositoryPathSignature(before, relativePath) !== repositoryPathSignature(after, relativePath)));
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
function repositoryPathSignature(state, relativePath) {
|
|
565
|
-
const statusLines = String(state?.status?.output || "").split(/\r?\n/)
|
|
566
|
-
.filter((line) => line.slice(3).trim().split(" -> ").some((item) => item.replace(/^"|"$/g, "") === relativePath));
|
|
567
|
-
const diffBlocks = [state?.unstaged_diff?.output, state?.staged_diff?.output]
|
|
568
|
-
.flatMap((value) => String(value || "").split(/(?=^diff --git )/m))
|
|
569
|
-
.filter((block) => block.startsWith("diff --git ") && block.split(/\r?\n/, 1)[0].includes(` b/${relativePath}`));
|
|
570
|
-
const untracked = (state?.untracked_files || []).find((item) => item.path === relativePath) || null;
|
|
571
|
-
return JSON.stringify({ statusLines, diffBlocks, untracked });
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
function buildEvidencePacket({
|
|
575
|
-
plan,
|
|
576
|
-
planState,
|
|
577
|
-
plannerObserved,
|
|
578
|
-
executionObserved,
|
|
579
|
-
executionOutput,
|
|
580
|
-
repositoryBefore,
|
|
581
|
-
repositoryAfter,
|
|
582
|
-
reviewFinding = null,
|
|
583
|
-
executionFailure = null,
|
|
584
|
-
}) {
|
|
585
|
-
return {
|
|
586
|
-
version: 1,
|
|
587
|
-
immutable: true,
|
|
588
|
-
request,
|
|
589
|
-
plan,
|
|
590
|
-
acceptance: planState.acceptance,
|
|
591
|
-
review_ids: planState.reviewIds,
|
|
592
|
-
planner: {
|
|
593
|
-
requested: plannerObserved?.requested || null,
|
|
594
|
-
observed: pickRoleIdentityEvidence(plannerObserved?.observed),
|
|
595
|
-
},
|
|
596
|
-
execution: {
|
|
597
|
-
requested: executionObserved?.requested || null,
|
|
598
|
-
observed: {
|
|
599
|
-
...pickRoleIdentityEvidence(executionObserved?.observed),
|
|
600
|
-
verification_evidence: pickExecutionVerificationEvidence(executionObserved?.observed?.tool_evidence || []),
|
|
601
|
-
cited_sources: captureCitedSources(executionOutput, repositoryBefore, repositoryAfter),
|
|
602
|
-
},
|
|
603
|
-
output: executionOutput,
|
|
604
|
-
},
|
|
605
|
-
repository: {
|
|
606
|
-
before: repositoryBefore,
|
|
607
|
-
after: repositoryAfter,
|
|
608
|
-
},
|
|
609
|
-
prior_review_finding: reviewFinding,
|
|
610
|
-
prior_execution_failure: executionFailure,
|
|
611
|
-
};
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
function pickRoleIdentityEvidence(observed = {}) {
|
|
615
|
-
return {
|
|
616
|
-
provider: observed.provider || null,
|
|
617
|
-
models: observed.models || [],
|
|
618
|
-
model_verified: observed.model_verified === true,
|
|
619
|
-
reasoning_efforts: observed.reasoning_efforts || [],
|
|
620
|
-
tool_use_detected: observed.tool_use_detected ?? null,
|
|
621
|
-
usage: observed.usage || null,
|
|
622
|
-
cost_usd: observed.cost_usd ?? null,
|
|
623
|
-
duration_ms: observed.duration_ms ?? null,
|
|
624
|
-
exit_code: observed.exit_code ?? null,
|
|
625
|
-
};
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
function pickExecutionVerificationEvidence(items) {
|
|
629
|
-
const lastChange = items.reduce((latest, item, index) => item?.type === "file_change" ? index : latest, -1);
|
|
630
|
-
return items.flatMap((item, index) => {
|
|
631
|
-
if (item?.type !== "command_execution" || item.status !== "completed") return [];
|
|
632
|
-
const command = String(item.command || "").trim();
|
|
633
|
-
if (!isVerificationCommand(command)) return [];
|
|
634
|
-
return [{
|
|
635
|
-
sequence: index,
|
|
636
|
-
after_last_file_change: index > lastChange,
|
|
637
|
-
command,
|
|
638
|
-
status: item.status,
|
|
639
|
-
exit_code: item.exit_code,
|
|
640
|
-
output: clip(String(item.output || ""), 30000),
|
|
641
|
-
}];
|
|
642
|
-
});
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
function isVerificationCommand(command) {
|
|
646
|
-
if (!command) return false;
|
|
647
|
-
return /\bgit\s+(?:diff\s+--check|status\b)|\b(?:npm|pnpm|yarn|bun)\s+(?:test|run\s+(?:test|check|verify|lint|build|typecheck))\b|\b(?:pytest|go\s+test|cargo\s+test|make\s+(?:test|check|verify|lint|build))\b|\bnode\s+(?:--check|tests?\/|[^\s;|&]*?(?:test|check|verify|validate)[^\s;|&]*\.m?js\b)|\b(?:python3?|py\s+-3)\b[^\n]*(?:test|check|verify|validate)|\bassert(?:\.|\s|\()/i.test(command);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
function captureRepositoryState() {
|
|
651
|
-
const status = runGit(["status", "--short", "--untracked-files=all"]);
|
|
652
|
-
return {
|
|
653
|
-
status,
|
|
654
|
-
unstaged_diff: runGit(["diff", "--no-ext-diff", "--binary", "--"]),
|
|
655
|
-
staged_diff: runGit(["diff", "--cached", "--no-ext-diff", "--binary", "--"]),
|
|
656
|
-
untracked_files: captureUntrackedFiles(),
|
|
657
|
-
};
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
function captureUntrackedFiles() {
|
|
661
|
-
const result = spawnSync("git", ["ls-files", "--others", "--exclude-standard", "-z", "--"], {
|
|
662
|
-
cwd: workdir,
|
|
663
|
-
encoding: "buffer",
|
|
664
|
-
windowsHide: true,
|
|
665
|
-
timeout: Math.min(args.timeout, 60) * 1000,
|
|
666
|
-
maxBuffer: 16 * 1024 * 1024,
|
|
667
|
-
});
|
|
668
|
-
if (result.status !== 0) return [];
|
|
669
|
-
const paths = result.stdout.toString("utf8").split("\0").filter(Boolean);
|
|
670
|
-
let remaining = 100000;
|
|
671
|
-
return paths.flatMap((relativePath) => {
|
|
672
|
-
if (remaining <= 0) return [];
|
|
673
|
-
const absolutePath = path.resolve(workdir, relativePath);
|
|
674
|
-
if (absolutePath !== workdir && !absolutePath.startsWith(`${workdir}${path.sep}`)) return [];
|
|
675
|
-
try {
|
|
676
|
-
const data = readFileSync(absolutePath);
|
|
677
|
-
if (data.includes(0)) return [{ path: relativePath, content: "[binary file omitted]" }];
|
|
678
|
-
const content = clip(data.toString("utf8"), Math.min(remaining, 50000));
|
|
679
|
-
remaining -= content.length;
|
|
680
|
-
return [{ path: relativePath, content }];
|
|
681
|
-
} catch {
|
|
682
|
-
return [{ path: relativePath, content: "[unreadable file]" }];
|
|
683
|
-
}
|
|
684
|
-
});
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
function repositoryStateChanged(before, after) {
|
|
688
|
-
return JSON.stringify(before || {}) !== JSON.stringify(after || {});
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
function runGit(commandArgs) {
|
|
692
|
-
const result = spawnSync("git", commandArgs, {
|
|
693
|
-
cwd: workdir,
|
|
694
|
-
encoding: "utf8",
|
|
695
|
-
windowsHide: true,
|
|
696
|
-
timeout: Math.min(args.timeout, 60) * 1000,
|
|
697
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
698
|
-
});
|
|
699
|
-
return {
|
|
700
|
-
command: `git ${commandArgs.join(" ")}`,
|
|
701
|
-
exit_code: result.status,
|
|
702
|
-
output: clip(`${result.stdout || ""}${result.stderr || ""}`, 100000),
|
|
703
|
-
};
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
function validateCloseout(value, acceptance, label) {
|
|
707
|
-
const parsed = parseCloseout(value, label);
|
|
708
|
-
const expected = new Set(acceptance.map((item) => item.id));
|
|
709
|
-
const unknown = Object.keys(parsed).filter((id) => !expected.has(id));
|
|
710
|
-
if (unknown.length > 0) fail(`${label} 的 closeout 含未知验收 ID:${unknown.join(", ")}`);
|
|
711
|
-
for (const item of acceptance) {
|
|
712
|
-
const report = parsed[item.id];
|
|
713
|
-
if (!report || typeof report !== "object" || Array.isArray(report)) {
|
|
714
|
-
fail(`${label} 的 closeout 缺少 ${item.id}`);
|
|
715
|
-
}
|
|
716
|
-
const unknownFields = Object.keys(report).filter((name) => !new Set(["status", "evidence", "next"]).has(name));
|
|
717
|
-
if (unknownFields.length > 0) {
|
|
718
|
-
fail(`${label} 的 closeout 中 ${item.id} 含未知字段:${unknownFields.join(", ")}`);
|
|
719
|
-
}
|
|
720
|
-
if (!new Set(["verified", "unresolved", "failed"]).has(report.status)) {
|
|
721
|
-
fail(`${label} 的 closeout 中 ${item.id} 状态无效`);
|
|
722
|
-
}
|
|
723
|
-
if (!cleanInline(report.evidence)) fail(`${label} 的 closeout 中 ${item.id} 缺少实际证据`);
|
|
724
|
-
const next = cleanInline(report.next);
|
|
725
|
-
if (report.status === "verified" && next) {
|
|
726
|
-
fail(`${label} 的 closeout 中 ${item.id} 已写 verified 却仍有继续条件`);
|
|
727
|
-
}
|
|
728
|
-
if (report.status !== "verified" && !next) {
|
|
729
|
-
fail(`${label} 的 closeout 中 ${item.id} 未闭合却缺少继续条件`);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
return parsed;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
function parseCloseout(value, label = "交付") {
|
|
736
|
-
const blocks = [...String(value || "").matchAll(/<odai_closeout>\s*([\s\S]*?)\s*<\/odai_closeout>/gi)];
|
|
737
|
-
if (blocks.length !== 1) fail(`${label} 必须且只能包含一个 <odai_closeout>`);
|
|
738
|
-
try {
|
|
739
|
-
const parsed = parseCloseoutJson(blocks[0][1]);
|
|
740
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("根节点不是对象");
|
|
741
|
-
return parsed;
|
|
742
|
-
} catch (error) {
|
|
743
|
-
fail(`${label} 的 closeout 不是有效 JSON:${error.message}`);
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
function parseCloseoutJson(value) {
|
|
748
|
-
const text = String(value || "").trim();
|
|
749
|
-
try {
|
|
750
|
-
return JSON.parse(text);
|
|
751
|
-
} catch (originalError) {
|
|
752
|
-
if (!text.startsWith("{")) throw originalError;
|
|
753
|
-
let depth = 0;
|
|
754
|
-
let inString = false;
|
|
755
|
-
let escaped = false;
|
|
756
|
-
for (let index = 0; index < text.length; index += 1) {
|
|
757
|
-
const character = text[index];
|
|
758
|
-
if (inString) {
|
|
759
|
-
if (escaped) escaped = false;
|
|
760
|
-
else if (character === "\\") escaped = true;
|
|
761
|
-
else if (character === '"') inString = false;
|
|
762
|
-
continue;
|
|
763
|
-
}
|
|
764
|
-
if (character === '"') inString = true;
|
|
765
|
-
else if (character === "{") depth += 1;
|
|
766
|
-
else if (character === "}") depth -= 1;
|
|
767
|
-
if (depth !== 0) continue;
|
|
768
|
-
const trailing = text.slice(index + 1).trim();
|
|
769
|
-
if (!trailing || !/^}+$/.test(trailing)) throw originalError;
|
|
770
|
-
return JSON.parse(text.slice(0, index + 1));
|
|
771
|
-
}
|
|
772
|
-
throw originalError;
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
function clip(value, limit) {
|
|
777
|
-
const text = String(value || "");
|
|
778
|
-
if (text.length <= limit) return text;
|
|
779
|
-
return `${text.slice(0, limit)}\n...[truncated ${text.length - limit} chars]`;
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
function parseReviewDecision(value) {
|
|
783
|
-
const lines = String(value || "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
784
|
-
const first = lines[0] || "";
|
|
785
|
-
const match = /^(pass|fail|unresolved)\b/i.exec(first);
|
|
786
|
-
if (!match) fail("reviewer 回执首行必须是 pass、fail 或 unresolved");
|
|
787
|
-
const routeMatch = /^route\s*:\s*(none|execution|planning|user|blocked)\s*$/i.exec(lines[1] || "");
|
|
788
|
-
if (!routeMatch) fail("reviewer 回执第二行必须是 route: none、execution、planning、user 或 blocked");
|
|
789
|
-
const status = match[1].toLowerCase();
|
|
790
|
-
const route = routeMatch[1].toLowerCase();
|
|
791
|
-
const allowed = {
|
|
792
|
-
pass: new Set(["none"]),
|
|
793
|
-
fail: new Set(["execution", "planning"]),
|
|
794
|
-
unresolved: new Set(["planning", "user", "blocked"]),
|
|
795
|
-
};
|
|
796
|
-
if (!allowed[status].has(route)) fail(`reviewer 回执组合无效:${status} / ${route}`);
|
|
797
|
-
return { status, route };
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
function requireObservedRole(role, evidence) {
|
|
801
|
-
const expected = manifest.mapping?.[role];
|
|
802
|
-
const observed = evidence?.observed || {};
|
|
803
|
-
if (!observed.model_verified || !Array.isArray(observed.models) || observed.models.length === 0) {
|
|
804
|
-
fail(`${role} 没有形成可核实的实际模型证据`);
|
|
805
|
-
}
|
|
806
|
-
if ((expected?.provider || manifest.host) === "codex" && !observed.models.includes(expected.model)) {
|
|
807
|
-
fail(`${role} 实际模型不匹配:期望 ${expected.model},实际 ${observed.models.join(", ")}`);
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
function requireSameThread(expected, evidence, role) {
|
|
812
|
-
const actual = String(evidence?.observed?.thread_id || "");
|
|
813
|
-
if (!actual || actual !== expected) {
|
|
814
|
-
fail(`${role} 没有恢复总控线程:期望 ${expected},实际 ${actual || "unknown"}`);
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
function roleSummary(role, evidence) {
|
|
819
|
-
return {
|
|
820
|
-
role,
|
|
821
|
-
requested: evidence.requested || null,
|
|
822
|
-
observed: evidence.observed || null,
|
|
823
|
-
};
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
function summarizeRoles(roles) {
|
|
827
|
-
const total = {
|
|
828
|
-
calls: roles.length,
|
|
829
|
-
duration_ms: 0,
|
|
830
|
-
cost_usd_known: 0,
|
|
831
|
-
cost_usd_complete: true,
|
|
832
|
-
usage: {},
|
|
833
|
-
models: [],
|
|
834
|
-
};
|
|
835
|
-
const models = new Set();
|
|
836
|
-
for (const item of roles) {
|
|
837
|
-
const observed = item?.observed || {};
|
|
838
|
-
total.duration_ms += Number(observed.duration_ms) || 0;
|
|
839
|
-
if (Number.isFinite(Number(observed.cost_usd))) total.cost_usd_known += Number(observed.cost_usd);
|
|
840
|
-
else total.cost_usd_complete = false;
|
|
841
|
-
for (const model of observed.models || []) models.add(model);
|
|
842
|
-
for (const [key, value] of Object.entries(observed.usage || {})) {
|
|
843
|
-
if (Number.isFinite(Number(value))) total.usage[key] = (total.usage[key] || 0) + Number(value);
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
total.models = [...models];
|
|
847
|
-
total.cost_usd_known = Number(total.cost_usd_known.toFixed(6));
|
|
848
|
-
return total;
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
function normalizePlanCard(value) {
|
|
852
|
-
const source = String(value || "").trim();
|
|
853
|
-
const wrapped = /<odai_(delivery|plan)(?:\s+[^>]*)?>\s*([\s\S]*?)\s*<\/odai_\1>/i.exec(source);
|
|
854
|
-
if (!wrapped) return normalizeAcceptanceIds(normalizeReviewField(source));
|
|
855
|
-
let parsed;
|
|
856
|
-
try {
|
|
857
|
-
parsed = JSON.parse(wrapped[2]);
|
|
858
|
-
} catch {
|
|
859
|
-
return normalizeAcceptanceIds(source);
|
|
860
|
-
}
|
|
861
|
-
const declaredMode = String(parsed?.status || parsed?.mode || parsed?.route
|
|
862
|
-
|| (wrapped[1].toLowerCase() === "plan" ? "planned" : "")).toLowerCase();
|
|
863
|
-
if (declaredMode !== "planned") return source;
|
|
864
|
-
const required = ["target", "evidence", "scope", "decision", "execute", "review", "accept", "stop"];
|
|
865
|
-
if (required.some((key) => parsed[key] == null)) return source;
|
|
866
|
-
const list = (value) => Array.isArray(value) ? value : value == null ? [] : [value];
|
|
867
|
-
const scalar = (value) => typeof value === "string" ? value : JSON.stringify(value);
|
|
868
|
-
const rawReview = parsed.review && typeof parsed.review === "object" && !Array.isArray(parsed.review)
|
|
869
|
-
? parsed.review.accept_ids ?? parsed.review.ids ?? []
|
|
870
|
-
: parsed.review;
|
|
871
|
-
const reviewIds = list(rawReview).flatMap((value) => String(value).match(/\b[A-Za-z][A-Za-z0-9_-]*\d[A-Za-z0-9_-]*\b/g) || []);
|
|
872
|
-
const acceptance = parsed.accept && typeof parsed.accept === "object" && !Array.isArray(parsed.accept)
|
|
873
|
-
? Object.entries(parsed.accept)
|
|
874
|
-
: list(parsed.accept).map((condition, index) => [`A${index + 1}`, condition]);
|
|
875
|
-
const steps = list(parsed.steps ?? parsed.slices);
|
|
876
|
-
return normalizeAcceptanceIds(normalizeReviewField([
|
|
877
|
-
"mode: planned",
|
|
878
|
-
`target: ${scalar(parsed.target)}`,
|
|
879
|
-
`evidence: ${scalar(parsed.evidence)}`,
|
|
880
|
-
`scope: ${scalar(parsed.scope)}`,
|
|
881
|
-
`decision: ${scalar(parsed.decision)}`,
|
|
882
|
-
`execute: ${scalar(parsed.execute)}`,
|
|
883
|
-
`review: ${reviewIds.length > 0 ? `reviewer ${reviewIds.join(" ")}` : "none"}`,
|
|
884
|
-
"accept:",
|
|
885
|
-
...acceptance.map(([id, condition]) => `- ${id}: ${scalar(condition)}`),
|
|
886
|
-
`stop: ${scalar(parsed.stop)}`,
|
|
887
|
-
...(steps.length > 0 ? ["steps:", ...steps.map((item) => `- ${scalar(item)}`)] : []),
|
|
888
|
-
].join("\n")));
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
function normalizeReviewField(value) {
|
|
892
|
-
const source = String(value || "").trim();
|
|
893
|
-
if (field(source, "mode").toLowerCase() !== "planned") return source;
|
|
894
|
-
const lines = source.split(/\r?\n/);
|
|
895
|
-
const start = lines.findIndex((line) => topLevelFieldLine("review").test(line));
|
|
896
|
-
if (start < 0) return source;
|
|
897
|
-
const inline = /^\s*(?:#{1,6}\s*)?(?:[-*]\s*)?review\s*[::]\s*(\S[\s\S]*)?\s*$/i.exec(lines[start]);
|
|
898
|
-
if (inline?.[1]) return source;
|
|
899
|
-
let end = lines.length;
|
|
900
|
-
for (let index = start + 1; index < lines.length; index += 1) {
|
|
901
|
-
if (["target", "evidence", "scope", "decision", "execute", "accept", "stop", "steps?", "slices?"]
|
|
902
|
-
.some((name) => topLevelFieldLine(name).test(lines[index]))) {
|
|
903
|
-
end = index;
|
|
904
|
-
break;
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
const section = lines.slice(start + 1, end).join("\n");
|
|
908
|
-
if (/^\s*(?:[-*]\s*)?none\s*$/im.test(section)) {
|
|
909
|
-
lines.splice(start, end - start, "review: none");
|
|
910
|
-
return lines.join("\n");
|
|
911
|
-
}
|
|
912
|
-
const ids = [...new Set(expandReviewIds(section))];
|
|
913
|
-
if (!/\breviewer\b/i.test(section) || ids.length === 0) return source;
|
|
914
|
-
lines.splice(start, end - start, "review: reviewer " + ids.join(" "));
|
|
915
|
-
return lines.join("\n");
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
function normalizeAcceptanceIds(value) {
|
|
919
|
-
const source = String(value || "").trim();
|
|
920
|
-
if (field(source, "mode").toLowerCase() !== "planned") return source;
|
|
921
|
-
const lines = source.split(/\r?\n/);
|
|
922
|
-
const start = lines.findIndex((line) => topLevelFieldLine("accept").test(line));
|
|
923
|
-
if (start < 0) return source;
|
|
924
|
-
let end = lines.length;
|
|
925
|
-
for (let index = start + 1; index < lines.length; index += 1) {
|
|
926
|
-
if (["stop", "steps?", "slices?"].some((name) => topLevelFieldLine(name).test(lines[index]))) {
|
|
927
|
-
end = index;
|
|
928
|
-
break;
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
const explicit = new Set();
|
|
932
|
-
for (const line of lines.slice(start + 1, end)) {
|
|
933
|
-
const match = /^\s*(?:[-*]\s*)?([A-Za-z][A-Za-z0-9_-]*\d[A-Za-z0-9_-]*)(?:\s*[::]\s*|\s+)(\S[\s\S]*)$/i.exec(line);
|
|
934
|
-
if (match) explicit.add(match[1].toUpperCase());
|
|
935
|
-
}
|
|
936
|
-
let next = 1;
|
|
937
|
-
const generatedId = () => {
|
|
938
|
-
while (explicit.has(`A${next}`)) next += 1;
|
|
939
|
-
const id = `A${next}`;
|
|
940
|
-
explicit.add(id);
|
|
941
|
-
next += 1;
|
|
942
|
-
return id;
|
|
943
|
-
};
|
|
944
|
-
for (let index = start + 1; index < end; index += 1) {
|
|
945
|
-
if (/^\s*(?:[-*]\s*)?([A-Za-z][A-Za-z0-9_-]*\d[A-Za-z0-9_-]*)(?:\s*[::]\s*|\s+)\S/i.test(lines[index])) continue;
|
|
946
|
-
const implicit = /^\s*(?:[-*]\s+|\d+[.)]\s+)(\S[\s\S]*)$/.exec(lines[index]);
|
|
947
|
-
if (implicit) lines[index] = `- ${generatedId()}: ${implicit[1].trim()}`;
|
|
948
|
-
}
|
|
949
|
-
return lines.join("\n");
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
function stripInternalBlocks(value) {
|
|
953
|
-
return String(value || "")
|
|
954
|
-
.replace(/<odai_(?:plan|closeout)(?:\s+[^>]*)?>[\s\S]*?<\/odai_(?:plan|closeout)>/gi, "")
|
|
955
|
-
.trim();
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
function validatePlan(value, directOwner = "planner") {
|
|
959
|
-
if (!value) fail("planner 没有形成路由或交付");
|
|
960
|
-
const first = String(value).split(/\r?\n/).find((line) => line.trim()) || "";
|
|
961
|
-
if (!/^\s*mode\s*[::]\s*(direct|planned)\s*$/i.test(first)) {
|
|
962
|
-
fail("planner 规划路由无效:首个非空行必须且只能是 mode: direct 或 mode: planned");
|
|
963
|
-
}
|
|
964
|
-
const mode = field(value, "mode").toLowerCase();
|
|
965
|
-
if (mode === "direct") {
|
|
966
|
-
if (countTopLevelFields(value, "mode") !== 1) fail("direct 交付的 mode 不得重复");
|
|
967
|
-
directExecutionOutput(value);
|
|
968
|
-
return { mode, acceptance: [], execute: directOwner, reviewIds: [] };
|
|
969
|
-
}
|
|
970
|
-
const requiredFields = ["mode", "target", "evidence", "scope", "decision", "execute", "review", "accept", "stop"];
|
|
971
|
-
const missing = requiredFields.filter((name) => !hasTopLevelField(value, name));
|
|
972
|
-
if (missing.length > 0) fail(`planner 规划路由无效,缺少:${missing.join(", ")}`);
|
|
973
|
-
const duplicated = requiredFields.filter((name) => {
|
|
974
|
-
return countTopLevelFields(value, name) > 1;
|
|
975
|
-
});
|
|
976
|
-
if (duplicated.length > 0) {
|
|
977
|
-
fail(`planner 规划路由无效:顶层字段不得重复:${duplicated.join(", ")}`);
|
|
978
|
-
}
|
|
979
|
-
const normalizedMode = mode;
|
|
980
|
-
const execute = field(value, "execute").toLowerCase().match(/^(?:controller|executor)\b/)?.[0] || "";
|
|
981
|
-
if (!execute) fail("规划路由无效:planned 的 execute 只能是 controller 或 executor");
|
|
982
|
-
const acceptance = parseAcceptance(value);
|
|
983
|
-
if (acceptance.length === 0) fail("planner 规划路由无效:accept 下必须有逐项、非空的验收条件");
|
|
984
|
-
const reviewIds = validateReview(value, acceptance);
|
|
985
|
-
if (/<odai_(?:delivery|closeout)>/i.test(value)) {
|
|
986
|
-
fail("planner 规划路由无效:planned 尚未实施,不能输出交付或 closeout");
|
|
987
|
-
}
|
|
988
|
-
return { mode: normalizedMode, acceptance, execute, reviewIds };
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
function validateReview(value, acceptance) {
|
|
992
|
-
const review = field(value, "review");
|
|
993
|
-
if (/^none(?:\b|[;;])/i.test(review)) return [];
|
|
994
|
-
if (!/\breviewer\b/i.test(review)) {
|
|
995
|
-
fail("planner 规划路由无效:review 只能是 none,或点名 reviewer 与验收 ID");
|
|
996
|
-
}
|
|
997
|
-
if (/\bnone\b/i.test(review)) fail("planner 规划路由无效:reviewer 路由不能混入 none");
|
|
998
|
-
if (/\b[A-Za-z][A-Za-z0-9_-]*\d[A-Za-z0-9_-]*\s*(?:等|等等|及其他)(?:\s|$|[,,;;])|(?:全部|所有)验收/i.test(review)) {
|
|
999
|
-
fail("planner 规划路由无效:reviewer 路由不能使用开放式验收缩写");
|
|
1000
|
-
}
|
|
1001
|
-
const known = new Set(acceptance.map((item) => item.id));
|
|
1002
|
-
const ids = expandReviewIds(review);
|
|
1003
|
-
if (ids.length === 0) fail("planner 规划路由无效:reviewer 路由必须点名待判定的验收 ID");
|
|
1004
|
-
const unknown = ids.filter((id) => !known.has(id));
|
|
1005
|
-
if (unknown.length > 0) fail(`planner 规划路由无效:review 引用了未知验收 ID ${[...new Set(unknown)].join(", ")}`);
|
|
1006
|
-
return [...new Set(ids)];
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
function expandReviewIds(value) {
|
|
1010
|
-
const source = String(value || "");
|
|
1011
|
-
const ids = [];
|
|
1012
|
-
const ranges = [];
|
|
1013
|
-
const rangePattern = /\b([A-Za-z][A-Za-z0-9_-]*?)(\d+)\s*[-–—]\s*\1(\d+)\b/gi;
|
|
1014
|
-
let match;
|
|
1015
|
-
while ((match = rangePattern.exec(source))) {
|
|
1016
|
-
const start = Number(match[2]);
|
|
1017
|
-
const end = Number(match[3]);
|
|
1018
|
-
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 1 || end < start || end - start > 99) {
|
|
1019
|
-
fail(`planner 规划路由无效:无法展开验收范围 ${match[0]}`);
|
|
1020
|
-
}
|
|
1021
|
-
const prefix = match[1].toUpperCase();
|
|
1022
|
-
for (let index = start; index <= end; index += 1) ids.push(`${prefix}${index}`);
|
|
1023
|
-
ranges.push([match.index, match.index + match[0].length]);
|
|
1024
|
-
}
|
|
1025
|
-
const outsideRanges = [...source]
|
|
1026
|
-
.map((character, index) => ranges.some(([start, end]) => index >= start && index < end) ? " " : character)
|
|
1027
|
-
.join("");
|
|
1028
|
-
for (const exact of outsideRanges.matchAll(/\b[A-Za-z][A-Za-z0-9_-]*\d[A-Za-z0-9_-]*\b/g)) {
|
|
1029
|
-
ids.push(exact[0].toUpperCase());
|
|
1030
|
-
}
|
|
1031
|
-
return ids;
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
function parseAcceptance(value) {
|
|
1035
|
-
const lines = String(value || "").split(/\r?\n/);
|
|
1036
|
-
const start = lines.findIndex((line) => topLevelFieldLine("accept").test(line));
|
|
1037
|
-
if (start < 0) return [];
|
|
1038
|
-
const result = [];
|
|
1039
|
-
const seen = new Set();
|
|
1040
|
-
for (const line of lines.slice(start + 1)) {
|
|
1041
|
-
if (["stop", "steps?", "slices?"].some((name) => topLevelFieldLine(name).test(line))) break;
|
|
1042
|
-
const match = /^\s*(?:[-*]\s*)?([A-Za-z][A-Za-z0-9_-]*\d[A-Za-z0-9_-]*)(?:\s*[::]\s*|\s+)(\S[\s\S]*)$/i.exec(line);
|
|
1043
|
-
if (!match) continue;
|
|
1044
|
-
const id = match[1].toUpperCase();
|
|
1045
|
-
if (seen.has(id)) fail(`planner 规划路由无效:重复验收 ID ${id}`);
|
|
1046
|
-
seen.add(id);
|
|
1047
|
-
result.push({ id, condition: match[2].trim() });
|
|
1048
|
-
}
|
|
1049
|
-
return result;
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
function renderDelivery(raw, acceptance, reviewState = null, reviewIds = []) {
|
|
1053
|
-
const source = String(raw || "");
|
|
1054
|
-
const closeout = parseCloseout(source, "最终执行回交");
|
|
1055
|
-
const message = source.replace(/<odai_closeout>[\s\S]*?<\/odai_closeout>/gi, "").trim();
|
|
1056
|
-
const passedReviewIds = new Set(reviewState?.status === "pass" ? reviewIds : []);
|
|
1057
|
-
const verified = [];
|
|
1058
|
-
const open = [];
|
|
1059
|
-
for (const item of acceptance) {
|
|
1060
|
-
const report = closeout[item.id] && typeof closeout[item.id] === "object" ? closeout[item.id] : {};
|
|
1061
|
-
const promotedByReview = passedReviewIds.has(item.id) && report.status !== "verified";
|
|
1062
|
-
const status = passedReviewIds.has(item.id)
|
|
1063
|
-
? "verified"
|
|
1064
|
-
: new Set(["verified", "unresolved", "failed"]).has(report.status)
|
|
1065
|
-
? report.status
|
|
1066
|
-
: "unresolved";
|
|
1067
|
-
const evidence = promotedByReview
|
|
1068
|
-
? ""
|
|
1069
|
-
: cleanInline(report.evidence || (status === "unresolved" ? "尚未提供足以闭合该项的证据" : ""));
|
|
1070
|
-
const next = cleanInline(report.next || "");
|
|
1071
|
-
if (status === "verified") {
|
|
1072
|
-
if (evidence && !includesLoose(message, evidence)) verified.push(evidence);
|
|
1073
|
-
continue;
|
|
1074
|
-
}
|
|
1075
|
-
const label = status === "failed" ? "未通过" : "未验证";
|
|
1076
|
-
const missingEvidence = evidence && !includesLoose(message, evidence) ? evidence : "";
|
|
1077
|
-
const missingNext = next && !includesLoose(message, next) ? next : "";
|
|
1078
|
-
if (!missingEvidence && !missingNext) continue;
|
|
1079
|
-
const details = [missingEvidence && `原因:${missingEvidence}`, missingNext && `继续条件:${missingNext}`].filter(Boolean).join(";");
|
|
1080
|
-
open.push(`- ${item.id} ${label}${details ? `:${details}` : ""}`);
|
|
1081
|
-
}
|
|
1082
|
-
const sections = [message];
|
|
1083
|
-
if (passedReviewIds.size > 0) sections.push(`独立验收:${[...passedReviewIds].join(", ")} 全部通过。`);
|
|
1084
|
-
if (verified.length > 0) sections.push(`验证:\n${verified.map((item) => `- ${item}`).join("\n")}`);
|
|
1085
|
-
if (open.length > 0) sections.push(`尚未闭合:\n${open.join("\n")}`);
|
|
1086
|
-
if (reviewState && reviewState.status !== "pass") {
|
|
1087
|
-
sections.push(`独立验收未闭合:\n${reviewState.output}`);
|
|
1088
|
-
}
|
|
1089
|
-
return `${sections.filter(Boolean).join("\n\n").trim()}\n`;
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
function cleanInline(value) {
|
|
1093
|
-
return String(value || "").replace(/\s+/g, " ").trim();
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
function includesLoose(message, value) {
|
|
1097
|
-
const normalize = (text) => String(text || "").toLowerCase().replace(/[\s`*_,。;:、,:;.!?()[\]{}'"“”‘’-]+/g, "");
|
|
1098
|
-
const needle = normalize(value);
|
|
1099
|
-
return needle.length >= 8 && normalize(message).includes(needle);
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
function field(value, name) {
|
|
1103
|
-
const match = new RegExp(`(?:^|\\n)\\s*(?:[-*]\\s*)?${name}\\s*[::]\\s*([^\\n]+)`, "i").exec(value);
|
|
1104
|
-
return String(match?.[1] || "").trim();
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
function topLevelFieldLine(name, flags = "i") {
|
|
1108
|
-
return new RegExp(`^\\s*(?:#{1,6}\\s*)?(?:[-*]\\s*)?${name}(?:\\s*[::]\\s*[^\\n]*)?\\s*$`, flags);
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
function hasTopLevelField(value, name) {
|
|
1112
|
-
return String(value || "").split(/\r?\n/).some((line) => topLevelFieldLine(name).test(line));
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
function countTopLevelFields(value, name) {
|
|
1116
|
-
return String(value || "").split(/\r?\n/).filter((line) => topLevelFieldLine(name).test(line)).length;
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
function readJson(file, label) {
|
|
1120
|
-
if (!existsSync(file)) fail(`缺少${label}:${file}`);
|
|
1121
|
-
try {
|
|
1122
|
-
return JSON.parse(readFileSync(file, "utf8"));
|
|
1123
|
-
} catch (error) {
|
|
1124
|
-
fail(`无法读取${label}:${error.message}`);
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
function parseArgs(argv) {
|
|
1129
|
-
const result = { cwd: "", output: "", sandbox: "workspace-write", manifest: "", codexBin: "", evidenceDir: "", timeout: 900, help: false };
|
|
1130
|
-
const fields = new Map([
|
|
1131
|
-
["--cwd", "cwd"], ["--output", "output"], ["--sandbox", "sandbox"], ["--manifest", "manifest"],
|
|
1132
|
-
["--codex-bin", "codexBin"],
|
|
1133
|
-
["--evidence-dir", "evidenceDir"], ["--timeout", "timeout"],
|
|
1134
|
-
]);
|
|
1135
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
1136
|
-
const value = argv[index];
|
|
1137
|
-
if (value === "--help" || value === "-h") result.help = true;
|
|
1138
|
-
else if (fields.has(value)) {
|
|
1139
|
-
const next = argv[++index] || "";
|
|
1140
|
-
if (!next || next.startsWith("--")) fail(`${value} 需要一个值`);
|
|
1141
|
-
result[fields.get(value)] = fields.get(value) === "timeout" ? Number(next) : next;
|
|
1142
|
-
} else fail(`未知参数:${value}`);
|
|
1143
|
-
}
|
|
1144
|
-
if (!Number.isFinite(result.timeout) || result.timeout <= 0) fail("--timeout 必须是正数秒数");
|
|
1145
|
-
return result;
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
function tail(value) {
|
|
1149
|
-
return String(value || "").trim().slice(-2000);
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
function fail(message) {
|
|
1153
|
-
throw new Error(message);
|
|
1154
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
process.on("uncaughtException", (error) => {
|
|
11
|
+
console.error(error?.message || String(error));
|
|
12
|
+
process.exitCode = 1;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const args = parseArgs(process.argv.slice(2));
|
|
17
|
+
|
|
18
|
+
if (args.help) {
|
|
19
|
+
console.log(`用已安装的 Codex odai stage 路由处理一次真实任务。一个持续总控 thread 负责目标、定路、修正与最终交付;只在规划冻结且交接有净收益时启动 executor,只在独立验收能改变放行结果时启动 reviewer。
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
printf '%s' '<request>' | node .codex/odai-run-routing.mjs [--cwd PATH] [--output FILE]
|
|
23
|
+
[--sandbox MODE] [--manifest FILE] [--codex-bin FILE] [--evidence-dir DIR]
|
|
24
|
+
|
|
25
|
+
输入从 stdin 读取。总控必须返回 direct 或 planned,execute 与 review 由宿主机械执行。`);
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const manifestFile = path.resolve(args.manifest || path.join(scriptDir, "odai-routing.json"));
|
|
30
|
+
const manifest = readJson(manifestFile, "路由清单");
|
|
31
|
+
if (manifest.host !== "codex") fail(`确定性规划入口当前只支持 Codex,收到:${manifest.host || "unknown"}`);
|
|
32
|
+
const routingMode = manifest.routingPolicy?.mode || "conditional";
|
|
33
|
+
if (routingMode !== "stage") fail("当前安装未启用 Codex stage 路由");
|
|
34
|
+
|
|
35
|
+
const request = readFileSync(0, "utf8").trim();
|
|
36
|
+
if (!request) fail("stdin 中没有用户请求");
|
|
37
|
+
const workdir = path.resolve(args.cwd || process.cwd());
|
|
38
|
+
const decisionRole = "controller";
|
|
39
|
+
const decisionMapping = manifest.mapping?.[decisionRole];
|
|
40
|
+
if (!decisionMapping?.model) fail(`路由清单缺少 ${decisionRole} 映射`);
|
|
41
|
+
if (routingMode === "stage" && (decisionMapping.provider || manifest.host) !== "codex") {
|
|
42
|
+
fail("stage 路由要求 controller 由 Codex 承载,以便在同一任务线程换档");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const roleRunner = path.join(scriptDir, "odai-run-role.mjs");
|
|
46
|
+
if (!existsSync(roleRunner)) fail(`缺少角色 runner:${roleRunner}`);
|
|
47
|
+
const temporary = mkdtempSync(path.join(tmpdir(), "odai-routing-run-"));
|
|
48
|
+
const evidenceDir = args.evidenceDir ? path.resolve(args.evidenceDir) : temporary;
|
|
49
|
+
mkdirSync(evidenceDir, { recursive: true });
|
|
50
|
+
const planFile = path.join(evidenceDir, `${decisionRole}-route.txt`);
|
|
51
|
+
const plannerLog = path.join(evidenceDir, `${decisionRole}-route.jsonl`);
|
|
52
|
+
const plannerEvidence = path.join(evidenceDir, `${decisionRole}-runtime.json`);
|
|
53
|
+
const outputFile = path.resolve(args.output || path.join(evidenceDir, "last-message.txt"));
|
|
54
|
+
const summaryFile = path.join(evidenceDir, "routing-run.json");
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const runStarted = Date.now();
|
|
58
|
+
const runSummary = { version: 2, mode: routingMode, started_at: new Date().toISOString(), roles: [], stage_transitions: [] };
|
|
59
|
+
const directOwner = "controller";
|
|
60
|
+
const plannerPrompt = `这是 odai 同一总控线程的首次定路。你就是用户任务的总控,不是总控之前的另一个角色;后续可以恢复这个 thread,不创建第二份目标与全局状态。本轮只读定路:不得编辑项目、实施、运行实施后验证或创建 agent;宿主会执行你选定的后续责任。当前 cwd 就是完整项目根;调查只限于该目录内的项目文件,不遍历父目录、兄弟目录、用户目录或其他工作区,也不盘点 .git 与路由器自身的 .codex 产物。
|
|
61
|
+
|
|
62
|
+
先从完整请求判断当前责任。用户给出的原因、手段、数值或完成标准会改变路线时,必须先读取足以裁决它的最少权威原件;未经核实的前提不得冻结进 target、decision 或 accept。用户当前要求的完整结果能在回复中交付时必须 direct,其中描述的组成部分或未来实施切片不改变本轮交付类型;只有当前获授权的交付本身在最小契约核对后仍需多个相互依赖的实施产物时才 planned,不读取实现和测试来预做。一项外部动作受阻而其余结果可当轮交付时仍 direct。高后果提高证据和验收强度,但不自动制造交接。
|
|
63
|
+
|
|
64
|
+
准备 direct 写入时,按目标对象在既有项目约定与验证入口中做一次有界发现;不能只看实现文件或通用启动配置,就宣称项目没有专用契约或测试。现成验证入口须原样运行,验证需求本身不授权修改其载体。
|
|
65
|
+
|
|
66
|
+
若结果依赖尚不可用的能力,且下一步涉及安装、启用、联网、凭据或更高权限,按 odai leverage 规则和项目权威来源处理。同意前让用户看到会改变决定的真实影响、继续动作、验证和替代差异,不把关键信息只留在内部路由中。
|
|
67
|
+
|
|
68
|
+
当前请求只需回答或只读交付,并且本轮已取得充分证据时才用 direct,直接给面向用户的完整结果。当前请求需要写入时必须 planned:同一总控上下文继续实施更可靠或更省时,写 execute: controller;只有决定已经冻结、实施有界可验且新上下文有可观察净收益时,才写 execute: executor。高后果本身只提高证据和验收强度,不强制拆开实施。新证据推翻路线时回到总控,只改受影响部分。
|
|
69
|
+
|
|
70
|
+
验收已定义结果、范围与保持项时,低风险、可逆且可测试的局部实现选择由执行责任自主决定;不要因缺少唯一先例而停止。只有选择会改变业务语义、公共契约、用户取舍或高代价后果时才升级决定。
|
|
71
|
+
|
|
72
|
+
已安装能力映射:
|
|
73
|
+
${JSON.stringify(manifest.mapping, null, 2)}
|
|
74
|
+
|
|
75
|
+
用户请求:
|
|
76
|
+
${request}`;
|
|
77
|
+
const plannerRepositoryBefore = captureRepositoryState();
|
|
78
|
+
const decisionArgs = [
|
|
79
|
+
"--role", decisionRole, "--manifest", manifestFile, "--cwd", workdir,
|
|
80
|
+
"--output", planFile, "--evidence", plannerEvidence, "--sandbox", args.sandbox,
|
|
81
|
+
"--route-card",
|
|
82
|
+
];
|
|
83
|
+
if (decisionRole === "planner") decisionArgs.push("--planner-may-complete-direct");
|
|
84
|
+
const plannerResult = runProviderRole(decisionArgs, plannerPrompt, Math.min(args.timeout, 240));
|
|
85
|
+
writeFileSync(plannerLog, plannerResult.output, "utf8");
|
|
86
|
+
if (plannerResult.status !== 0) fail(`${decisionRole} 定路失败:${tail(plannerResult.output)}`);
|
|
87
|
+
const plannerObserved = readJson(plannerEvidence, `${decisionRole} 运行证据`);
|
|
88
|
+
requireObservedRole(decisionRole, plannerObserved);
|
|
89
|
+
const controllerThreadId = routingMode === "stage" ? plannerObserved.observed?.thread_id : "";
|
|
90
|
+
if (routingMode === "stage" && !controllerThreadId) fail("controller 没有形成可核实的 Codex thread ID");
|
|
91
|
+
runSummary.controller_thread_id = controllerThreadId || null;
|
|
92
|
+
const decisionRoleLabel = "controller-route";
|
|
93
|
+
runSummary.roles.push(roleSummary(decisionRoleLabel, plannerObserved));
|
|
94
|
+
runSummary.stage_transitions.push({ stage: "decision", role: decisionRole, reason: "initial-capable-first" });
|
|
95
|
+
|
|
96
|
+
let plan = existsSync(planFile) ? normalizePlanCard(readFileSync(planFile, "utf8").trim()) : "";
|
|
97
|
+
let planState = validatePlan(plan, directOwner);
|
|
98
|
+
const plannerRepositoryAfter = captureRepositoryState();
|
|
99
|
+
if (planState.mode === "planned" && repositoryStateChanged(plannerRepositoryBefore, plannerRepositoryAfter)) {
|
|
100
|
+
fail(`${decisionRole} 选择 planned 却提前修改了项目;复杂任务只能交付方案,不能预做实现`);
|
|
101
|
+
}
|
|
102
|
+
let activePlannerObserved = plannerObserved;
|
|
103
|
+
const initialExecute = planState.execute;
|
|
104
|
+
const initialReviewIds = [...planState.reviewIds];
|
|
105
|
+
let executionResult;
|
|
106
|
+
if (planState.mode === "direct") {
|
|
107
|
+
executionResult = captureDirectCompletion({
|
|
108
|
+
plan, planState, plannerObserved: activePlannerObserved,
|
|
109
|
+
repositoryBefore: plannerRepositoryBefore, repositoryAfter: plannerRepositoryAfter,
|
|
110
|
+
directRole: directOwner,
|
|
111
|
+
});
|
|
112
|
+
} else {
|
|
113
|
+
try {
|
|
114
|
+
executionResult = runSelectedExecution({
|
|
115
|
+
plan, planState, plannerObserved: activePlannerObserved, runSummary, suffix: "",
|
|
116
|
+
sessionId: planState.execute === "controller" ? controllerThreadId : "",
|
|
117
|
+
});
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (routingMode !== "stage" || planState.execute !== "executor" || !isRecoverableExecutionError(error)) throw error;
|
|
120
|
+
executionResult = runControllerRecovery({
|
|
121
|
+
plan, planState, plannerObserved: activePlannerObserved, runSummary,
|
|
122
|
+
sessionId: controllerThreadId, failure: error?.message || String(error),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
let rawOutput = executionResult.output;
|
|
127
|
+
let reviewState = null;
|
|
128
|
+
if (planState.reviewIds.length > 0) {
|
|
129
|
+
reviewState = runReview({
|
|
130
|
+
request, plan, planState, executionResult, plannerObserved: activePlannerObserved, runSummary, round: 1,
|
|
131
|
+
});
|
|
132
|
+
if (reviewState.route === "execution") {
|
|
133
|
+
executionResult = runExecutionCorrection({
|
|
134
|
+
request, plan, planState, reviewState, plannerObserved: activePlannerObserved, runSummary,
|
|
135
|
+
sessionId: executionResult.observed?.observed?.thread_id || "",
|
|
136
|
+
});
|
|
137
|
+
rawOutput = executionResult.output;
|
|
138
|
+
reviewState = runReview({
|
|
139
|
+
request, plan, planState, executionResult, plannerObserved: activePlannerObserved, runSummary, round: 2,
|
|
140
|
+
});
|
|
141
|
+
} else if (reviewState.route === "planning") {
|
|
142
|
+
const replanned = runIncrementalReplan({
|
|
143
|
+
request, plan, planState, reviewState, plannerObserved: activePlannerObserved, runSummary,
|
|
144
|
+
sessionId: controllerThreadId, role: routingMode === "stage" ? "controller" : "planner",
|
|
145
|
+
directOwner,
|
|
146
|
+
});
|
|
147
|
+
plan = replanned.plan;
|
|
148
|
+
planState = replanned.planState;
|
|
149
|
+
activePlannerObserved = replanned.observed;
|
|
150
|
+
if (planState.reviewIds.length === 0) {
|
|
151
|
+
fail("增量重规划必须保留 finding 所涉验收 ID 的独立复验");
|
|
152
|
+
}
|
|
153
|
+
executionResult = runSelectedExecution({
|
|
154
|
+
plan, planState, plannerObserved: activePlannerObserved, runSummary, suffix: "-recovery",
|
|
155
|
+
sessionId: planState.execute === "controller" ? controllerThreadId : "",
|
|
156
|
+
});
|
|
157
|
+
rawOutput = executionResult.output;
|
|
158
|
+
reviewState = runReview({
|
|
159
|
+
request, plan, planState, executionResult, plannerObserved: activePlannerObserved, runSummary, round: 2,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const output = planState.mode === "direct"
|
|
164
|
+
? `${String(rawOutput || "").trim()}\n`
|
|
165
|
+
: renderDelivery(rawOutput, planState.acceptance, reviewState, planState.reviewIds);
|
|
166
|
+
writeFileSync(outputFile, output, "utf8");
|
|
167
|
+
runSummary.completed_at = new Date().toISOString();
|
|
168
|
+
runSummary.initial_execute = initialExecute;
|
|
169
|
+
runSummary.execute = planState.execute;
|
|
170
|
+
runSummary.initial_review_ids = initialReviewIds;
|
|
171
|
+
runSummary.review_ids = planState.reviewIds;
|
|
172
|
+
runSummary.execution_context = planState.mode === "direct"
|
|
173
|
+
? `${directOwner}-direct-same-context`
|
|
174
|
+
: planState.execute === "controller"
|
|
175
|
+
? "controller-resumed-same-context"
|
|
176
|
+
: "bounded-fresh-execution-context";
|
|
177
|
+
runSummary.controller_context = routingMode === "stage" ? "persistent-task-thread" : "host-owned-no-model-call";
|
|
178
|
+
runSummary.review_status = reviewState?.status || "not-requested";
|
|
179
|
+
runSummary.review_route = reviewState?.route || "none";
|
|
180
|
+
runSummary.duration_ms = Date.now() - runStarted;
|
|
181
|
+
runSummary.totals = summarizeRoles(runSummary.roles);
|
|
182
|
+
writeFileSync(summaryFile, `${JSON.stringify(runSummary, null, 2)}\n`, "utf8");
|
|
183
|
+
process.stdout.write(output);
|
|
184
|
+
if (!output.endsWith("\n")) process.stdout.write("\n");
|
|
185
|
+
if (reviewState?.status === "fail") process.exitCode = 1;
|
|
186
|
+
else if (reviewState?.status === "unresolved") process.exitCode = 2;
|
|
187
|
+
} finally {
|
|
188
|
+
if (!args.evidenceDir) rmSync(temporary, { recursive: true, force: true });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function runProviderRole(commandArgs, input, timeout = args.timeout) {
|
|
192
|
+
if (args.codexBin) commandArgs.push("--codex-bin", args.codexBin);
|
|
193
|
+
commandArgs.push("--timeout", String(timeout));
|
|
194
|
+
const result = spawnSync(process.execPath, [roleRunner, ...commandArgs], {
|
|
195
|
+
cwd: workdir,
|
|
196
|
+
input,
|
|
197
|
+
env: { ...process.env, ODAI_ROUTING_ACTIVE: "1" },
|
|
198
|
+
encoding: "utf8",
|
|
199
|
+
windowsHide: true,
|
|
200
|
+
timeout: timeout * 1000,
|
|
201
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
202
|
+
});
|
|
203
|
+
if (result.error) fail(result.error.message);
|
|
204
|
+
return { status: result.status, output: `${result.stdout || ""}${result.stderr || ""}` };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function executionContext() {
|
|
208
|
+
return `原始用户请求由总控、planner 与 reviewer 保管,用于防遗漏、修正路由和最终验收;受托执行者只接收已经覆盖该请求的冻结方案,不重新解释或扩写需求。`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function buildExecutionPrompt(role, plan, planState, plannerObserved) {
|
|
212
|
+
return `按 odai 宿主已经核实的规划承担一次 ${role} 执行切片。只在规划的允许范围内形成真实产物,不重新规划、不扩大目标、不批准最终交付。
|
|
213
|
+
|
|
214
|
+
${executionContext()}
|
|
215
|
+
|
|
216
|
+
已核实规划(这是决定与边界的 owner,不要求重新证明):
|
|
217
|
+
${plan}
|
|
218
|
+
|
|
219
|
+
planner 已核实的来源指针如下。不要为了复述规划而重读;只读取完成实际产物仍必需的实现原件:
|
|
220
|
+
${formatCapturedEvidence(plannerObserved, plan)}
|
|
221
|
+
|
|
222
|
+
${planState.reviewIds.length > 0
|
|
223
|
+
? `独立 reviewer 负责 ${planState.reviewIds.join(", ")} 的最终判定。不要先跑基线检查;完成改动后,每项必要验证只运行一次。只有检查失败并完成针对性修正后才可重跑。`
|
|
224
|
+
: "不要先跑基线检查;完成改动后,每项闭合验收所需的检查只运行一次,失败并修正后才可重跑。"}
|
|
225
|
+
决定性验证成功且没有新的失败证据时立即停止分析与检索,直接形成回交和 closeout;不为再次解释、安心复核或等待 reviewer 继续推理。
|
|
226
|
+
回交时点名每份改变决定的权威来源路径;宿主会把未修改的来源原件纳入证据包,不能只说“已查阅”。执行中从权威来源取得的、会改变复验结果的准确对象、条件、输入或位置须保留,不能被规划中的宽泛说法或简洁摘要稀释。需要验证不自动授权修改测试、基准、文档或其他证据源;项目已点名现成验证入口时先原样运行,不能为了产生更漂亮或更完整的证据改变它。用户当前就要审阅或使用的正文、判断或其他内容必须在回交中自足给出;除非契约明确只交文件,文件路径不能替代实际内容。写入持久文件必须有请求、项目契约或规划对该载体的明确需要,不为暂存内容或证明完成自造文件。
|
|
227
|
+
新建或替换持久 owner 时,规划捕获的既有 owner 查找结果必须进入对应验收项与最终回交;没有可追溯证据才补最小查找,不重复已经捕获的调查。
|
|
228
|
+
指定验证器因当前入口或依赖失败时,先尝试用现有兼容入口运行同一验证器;不得把自写近似检查称为等价。权威项目原件已明确某项环境、入口或数据不存在,且当前授权不包含补建时,不再加载外部能力、安装工具或广泛搜索来重复证明缺失;准确保留未决与继续条件。规划中执行后仍成立的 stop 必须保留在面向用户的回交中。
|
|
229
|
+
完成后按宿主提供的结构化输出契约回交:delivery 是面向用户的简洁结果;acceptance 覆盖全部验收 ID:${planState.acceptance.map((item) => item.id).join(", ")},每项给 status、实际 evidence 与 next。verified 的 next 必须为空,未决或失败必须给继续条件;未来仍需观察的属性写 unresolved。不得把 reviewer 尚未作出的判断写成独立验收通过。`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function captureDirectCompletion({ plan, planState, plannerObserved, repositoryBefore, repositoryAfter, directRole = "planner" }) {
|
|
233
|
+
const output = directExecutionOutput(plan);
|
|
234
|
+
const packet = buildEvidencePacket({
|
|
235
|
+
plan,
|
|
236
|
+
planState,
|
|
237
|
+
plannerObserved,
|
|
238
|
+
executionObserved: plannerObserved,
|
|
239
|
+
executionOutput: output,
|
|
240
|
+
repositoryBefore,
|
|
241
|
+
repositoryAfter,
|
|
242
|
+
});
|
|
243
|
+
const packetFile = path.join(evidenceDir, "evidence-packet-direct.json");
|
|
244
|
+
writeFileSync(packetFile, `${JSON.stringify(packet, null, 2)}\n`, "utf8");
|
|
245
|
+
return {
|
|
246
|
+
role: directRole,
|
|
247
|
+
output,
|
|
248
|
+
evidence: plannerEvidence,
|
|
249
|
+
observed: plannerObserved,
|
|
250
|
+
packet,
|
|
251
|
+
packetFile,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function directExecutionOutput(plan) {
|
|
256
|
+
const lines = String(plan || "").trim().split(/\r?\n/);
|
|
257
|
+
const first = lines.findIndex((line) => line.trim());
|
|
258
|
+
if (first < 0 || !/^\s*mode\s*[::]\s*direct\s*$/i.test(lines[first])) {
|
|
259
|
+
fail("direct 交付的首个非空行必须且只能是 mode: direct");
|
|
260
|
+
}
|
|
261
|
+
const output = lines.slice(first + 1).join("\n").trim();
|
|
262
|
+
if (!output) fail("direct 没有形成面向用户的完整交付");
|
|
263
|
+
if (/<odai_(?:delivery|closeout|plan)>/i.test(output)) {
|
|
264
|
+
fail("direct 交付不得包含内部路由标签");
|
|
265
|
+
}
|
|
266
|
+
return output;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function runSelectedExecution({ plan, planState, plannerObserved, runSummary, suffix, sessionId = "" }) {
|
|
270
|
+
const role = planState.execute;
|
|
271
|
+
const roleOutput = path.join(evidenceDir, `${role}-result${suffix}.txt`);
|
|
272
|
+
const roleEvidence = path.join(evidenceDir, `${role}-runtime${suffix}.json`);
|
|
273
|
+
const roleLog = path.join(evidenceDir, `${role}${suffix}.log`);
|
|
274
|
+
const before = captureRepositoryState();
|
|
275
|
+
const rolePrompt = buildExecutionPrompt(role, plan, planState, plannerObserved);
|
|
276
|
+
const command = [
|
|
277
|
+
"--role", role, "--manifest", manifestFile, "--cwd", workdir,
|
|
278
|
+
"--output", roleOutput, "--evidence", roleEvidence, "--sandbox", args.sandbox,
|
|
279
|
+
"--closeout-ids", planState.acceptance.map((item) => item.id).join(","),
|
|
280
|
+
];
|
|
281
|
+
if (sessionId) command.push("--session-id", sessionId);
|
|
282
|
+
const executed = runProviderRole(command, rolePrompt);
|
|
283
|
+
writeFileSync(roleLog, executed.output, "utf8");
|
|
284
|
+
if (executed.status !== 0) fail(`${role} 执行失败:${tail(executed.output)}`);
|
|
285
|
+
const observed = readJson(roleEvidence, `${role} 运行证据`);
|
|
286
|
+
requireObservedRole(role, observed);
|
|
287
|
+
if (sessionId) requireSameThread(sessionId, observed, role);
|
|
288
|
+
runSummary.roles.push(roleSummary(`${role}${suffix}`, observed));
|
|
289
|
+
if (routingMode === "stage") {
|
|
290
|
+
runSummary.stage_transitions.push({
|
|
291
|
+
stage: "implementation",
|
|
292
|
+
role,
|
|
293
|
+
reason: sessionId
|
|
294
|
+
? role === "controller" ? "same-controller-continued-after-read-only-route" : "finding-correction-in-original-execution-context"
|
|
295
|
+
: "frozen-bounded-work-in-fresh-context",
|
|
296
|
+
thread_id: observed.observed?.thread_id || null,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
const output = existsSync(roleOutput) ? readFileSync(roleOutput, "utf8").trim() : executed.output.trim();
|
|
300
|
+
validateCloseout(output, planState.acceptance, `${role} 回交`);
|
|
301
|
+
const packet = buildEvidencePacket({
|
|
302
|
+
plan,
|
|
303
|
+
planState,
|
|
304
|
+
plannerObserved,
|
|
305
|
+
executionObserved: observed,
|
|
306
|
+
executionOutput: output,
|
|
307
|
+
repositoryBefore: before,
|
|
308
|
+
repositoryAfter: captureRepositoryState(),
|
|
309
|
+
});
|
|
310
|
+
const packetFile = path.join(evidenceDir, `evidence-packet${suffix}.json`);
|
|
311
|
+
writeFileSync(packetFile, `${JSON.stringify(packet, null, 2)}\n`, "utf8");
|
|
312
|
+
return {
|
|
313
|
+
role,
|
|
314
|
+
output,
|
|
315
|
+
evidence: roleEvidence,
|
|
316
|
+
observed,
|
|
317
|
+
packet,
|
|
318
|
+
packetFile,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function runControllerRecovery({ plan, planState, plannerObserved, runSummary, sessionId, failure }) {
|
|
323
|
+
const roleOutput = path.join(evidenceDir, "controller-recovery-result.txt");
|
|
324
|
+
const roleEvidence = path.join(evidenceDir, "controller-recovery-runtime.json");
|
|
325
|
+
const roleLog = path.join(evidenceDir, "controller-recovery.log");
|
|
326
|
+
const before = captureRepositoryState();
|
|
327
|
+
const prompt = `执行阶段出现了可观察失败,宿主现已把同一任务线程升回 controller 档。你仍是原总控;不要重做完整调查,也不要为失败扩大目标。
|
|
328
|
+
|
|
329
|
+
冻结路线:
|
|
330
|
+
${plan}
|
|
331
|
+
|
|
332
|
+
执行失败:
|
|
333
|
+
${failure}
|
|
334
|
+
|
|
335
|
+
只检查失败实际暴露的缺口。路线仍成立时,在原边界内完成最小修正与必要验证;新证据推翻决定时,只修正受影响决定后继续完成。不得丢失验收 ID:${planState.acceptance.map((item) => item.id).join(", ")}。
|
|
336
|
+
按宿主提供的结构化输出契约回交,delivery 给面向用户的简洁结果,acceptance 覆盖全部验收 ID;每项包含 status、实际 evidence 与 next,verified 的 next 为空,未决或失败时给真实继续条件。`;
|
|
337
|
+
const recovered = runProviderRole([
|
|
338
|
+
"--role", "controller", "--manifest", manifestFile, "--cwd", workdir,
|
|
339
|
+
"--output", roleOutput, "--evidence", roleEvidence, "--sandbox", args.sandbox,
|
|
340
|
+
"--closeout-ids", planState.acceptance.map((item) => item.id).join(","),
|
|
341
|
+
"--session-id", sessionId,
|
|
342
|
+
], prompt);
|
|
343
|
+
writeFileSync(roleLog, recovered.output, "utf8");
|
|
344
|
+
if (recovered.status !== 0) fail(`controller 升档恢复失败:${tail(recovered.output)}`);
|
|
345
|
+
const observed = readJson(roleEvidence, "controller 升档恢复证据");
|
|
346
|
+
requireObservedRole("controller", observed);
|
|
347
|
+
requireSameThread(sessionId, observed, "controller");
|
|
348
|
+
runSummary.roles.push(roleSummary("controller-recovery", observed));
|
|
349
|
+
runSummary.stage_transitions.push({ stage: "recovery", role: "controller", reason: "execution-failure", thread_id: sessionId });
|
|
350
|
+
const output = existsSync(roleOutput) ? readFileSync(roleOutput, "utf8").trim() : recovered.output.trim();
|
|
351
|
+
validateCloseout(output, planState.acceptance, "controller 恢复回交");
|
|
352
|
+
const packet = buildEvidencePacket({
|
|
353
|
+
plan,
|
|
354
|
+
planState,
|
|
355
|
+
plannerObserved,
|
|
356
|
+
executionObserved: observed,
|
|
357
|
+
executionOutput: output,
|
|
358
|
+
repositoryBefore: before,
|
|
359
|
+
repositoryAfter: captureRepositoryState(),
|
|
360
|
+
executionFailure: failure,
|
|
361
|
+
});
|
|
362
|
+
const packetFile = path.join(evidenceDir, "evidence-packet-controller-recovery.json");
|
|
363
|
+
writeFileSync(packetFile, `${JSON.stringify(packet, null, 2)}\n`, "utf8");
|
|
364
|
+
return { role: "controller", output, evidence: roleEvidence, observed, packet, packetFile };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function isRecoverableExecutionError(error) {
|
|
368
|
+
const message = String(error?.message || error || "");
|
|
369
|
+
return !/(实际模型不匹配|恢复了错误线程|没有恢复总控线程|路由清单|托管安装)/.test(message);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function runReview({ request: userRequest, plan, planState, executionResult, plannerObserved, runSummary, round }) {
|
|
373
|
+
const suffix = round === 1 ? "" : `-${round}`;
|
|
374
|
+
const resultFile = path.join(evidenceDir, `reviewer-result${suffix}.txt`);
|
|
375
|
+
const evidenceFile = path.join(evidenceDir, `reviewer-runtime${suffix}.json`);
|
|
376
|
+
const logFile = path.join(evidenceDir, `reviewer${suffix}.log`);
|
|
377
|
+
const reviewPrompt = `独立验收当前实际产物。宿主已经提供自足的不可变证据包;不得调用工具、读取仓库或重复运行已经成功的确定性检查。先核对完整 accept 是否覆盖原始请求中每个会改变结果的要求;遗漏属于方案或验收设计缺陷,使用 route: planning。覆盖成立后,只检查定路中点名的这些 ID:${planState.reviewIds.join(", ")}。
|
|
378
|
+
|
|
379
|
+
宿主不可变证据包(${executionResult.packetFile},其中用户请求、规划与执行回交各只有一份):
|
|
380
|
+
${JSON.stringify(executionResult.packet, null, 2)}
|
|
381
|
+
|
|
382
|
+
只依据以上方案、回交与证据包作出判断。执行输出和执行者新写的验证记录都只是主张;命令、退出码与输出只认 execution.observed.verification_evidence,且命令本身必须能产生所声称的观察。证据缺失或互相冲突时,不自行重新取证:方案或验收设计有误用 route: planning,实施偏差用 route: execution,缺用户决定或外部条件用 unresolved 的相应 route。finding 不授权新增证明文件,只指出原产物的最小修正或应取得的真实观察。首行只写 pass、fail 或 unresolved;第二行按角色契约只写 route。随后逐项给出证据、最小修正或继续条件。不得自行修复。`;
|
|
383
|
+
const reviewed = runProviderRole([
|
|
384
|
+
"--role", "reviewer", "--manifest", manifestFile, "--cwd", workdir,
|
|
385
|
+
"--output", resultFile, "--evidence", evidenceFile,
|
|
386
|
+
], reviewPrompt);
|
|
387
|
+
writeFileSync(logFile, reviewed.output, "utf8");
|
|
388
|
+
if (reviewed.status !== 0) fail(`reviewer 验收失败:${tail(reviewed.output)}`);
|
|
389
|
+
const observed = readJson(evidenceFile, "reviewer 运行证据");
|
|
390
|
+
requireObservedRole("reviewer", observed);
|
|
391
|
+
if (observed.observed?.tool_use_detected === true) {
|
|
392
|
+
fail("reviewer 已收到完整宿主证据包却仍调用工具,拒绝把重复取证计作有效验收");
|
|
393
|
+
}
|
|
394
|
+
runSummary.roles.push(roleSummary(`reviewer-${round}`, observed));
|
|
395
|
+
runSummary.stage_transitions.push({ stage: "review", role: "reviewer", reason: "independent-acceptance-can-change-release" });
|
|
396
|
+
const output = existsSync(resultFile) ? readFileSync(resultFile, "utf8").trim() : reviewed.output.trim();
|
|
397
|
+
return { ...parseReviewDecision(output), output, round };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function runIncrementalReplan({ request: userRequest, plan, planState, reviewState, plannerObserved, runSummary, sessionId = "", role = "planner", directOwner = "planner" }) {
|
|
401
|
+
const resultFile = path.join(evidenceDir, `${role}-replan.txt`);
|
|
402
|
+
const evidenceFile = path.join(evidenceDir, `${role}-replan-runtime.json`);
|
|
403
|
+
const logFile = path.join(evidenceDir, `${role}-replan.log`);
|
|
404
|
+
const prompt = `这是 reviewer 已判定为 route: planning 的增量重规划。只处理 finding 推翻的决定;不要重做首次调查、编辑文件或实施。
|
|
405
|
+
|
|
406
|
+
用户请求:
|
|
407
|
+
${userRequest}
|
|
408
|
+
|
|
409
|
+
当前规划:
|
|
410
|
+
${plan}
|
|
411
|
+
|
|
412
|
+
首次规划已取得的证据:
|
|
413
|
+
${formatCapturedEvidence(plannerObserved, plan)}
|
|
414
|
+
|
|
415
|
+
独立验收 finding:
|
|
416
|
+
${reviewState.output}
|
|
417
|
+
|
|
418
|
+
只利用已有证据做增量修正,不重复首次调查;仅当 finding 明确指出证据包缺少一个会改变路线的事实时,才补取该事实。返回同一格式的完整替换路由。保留未受影响验收 ID;为 finding 涉及且现已可取得证据的属性继续点名 reviewer。`;
|
|
419
|
+
const command = [
|
|
420
|
+
"--role", role, "--manifest", manifestFile, "--cwd", workdir,
|
|
421
|
+
"--output", resultFile, "--evidence", evidenceFile,
|
|
422
|
+
];
|
|
423
|
+
if (sessionId) command.push("--session-id", sessionId);
|
|
424
|
+
else if (role === "planner") command.push("--planner-may-complete-direct");
|
|
425
|
+
const replanned = runProviderRole(command, prompt);
|
|
426
|
+
writeFileSync(logFile, replanned.output, "utf8");
|
|
427
|
+
if (replanned.status !== 0) fail(`${role} 增量重规划失败:${tail(replanned.output)}`);
|
|
428
|
+
const observed = readJson(evidenceFile, `${role} 增量重规划证据`);
|
|
429
|
+
requireObservedRole(role, observed);
|
|
430
|
+
if (sessionId) requireSameThread(sessionId, observed, role);
|
|
431
|
+
runSummary.roles.push(roleSummary(`${role}-replan`, observed));
|
|
432
|
+
if (sessionId) runSummary.stage_transitions.push({ stage: "decision", role, reason: "review-invalidated-route", thread_id: sessionId });
|
|
433
|
+
const nextPlan = existsSync(resultFile) ? readFileSync(resultFile, "utf8").trim() : replanned.output.trim();
|
|
434
|
+
const nextState = validatePlan(nextPlan, directOwner);
|
|
435
|
+
const nextIds = new Set(nextState.acceptance.map((item) => item.id));
|
|
436
|
+
const lost = planState.reviewIds.filter((id) => !nextIds.has(id));
|
|
437
|
+
if (lost.length > 0) fail(`增量重规划丢失 finding 所涉验收 ID:${lost.join(", ")}`);
|
|
438
|
+
return { plan: nextPlan, planState: nextState, observed };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function runExecutionCorrection({ plan, planState, reviewState, plannerObserved, runSummary, sessionId = "" }) {
|
|
442
|
+
const role = planState.execute;
|
|
443
|
+
const resultFile = path.join(evidenceDir, `${role}-repair-result.txt`);
|
|
444
|
+
const evidenceFile = path.join(evidenceDir, `${role}-repair-runtime.json`);
|
|
445
|
+
const logFile = path.join(evidenceDir, `${role}-repair.log`);
|
|
446
|
+
const prompt = `reviewer 已将 finding 判定为实施偏差,原规划仍成立。你是原执行承载 ${role};只修正 finding 指向的实际产物,不重新规划、不扩大范围、不改写验收源,也不批准最终交付。
|
|
447
|
+
|
|
448
|
+
已核实规划:
|
|
449
|
+
${plan}
|
|
450
|
+
|
|
451
|
+
[首次规划证据]
|
|
452
|
+
${formatCapturedEvidence(plannerObserved, plan)}
|
|
453
|
+
|
|
454
|
+
[独立验收 finding]
|
|
455
|
+
${reviewState.output}
|
|
456
|
+
|
|
457
|
+
finding 只授权修正原产物,不授权新建验证报告、检查日志或其他交付外文件。需要补证时运行能真实判定该属性的现有检查并回交宿主捕获的原始命令结果;检查不存在或当前不可运行就保留未决,不能用注释、示例输出、自写记录或叙述替代执行证据。
|
|
458
|
+
|
|
459
|
+
不要重复首次调查或已经成功且未受 finding 影响的检查。完成最小修正与相称验证后,按宿主提供的结构化输出契约回交:delivery 给面向用户的简洁结果,acceptance 覆盖 ${planState.acceptance.map((item) => item.id).join(", ")};每项含 status、实际 evidence 与 next,verified 的 next 为空,未闭合项给真实继续条件。宿主与 reviewer 负责最终收口。`;
|
|
460
|
+
const before = captureRepositoryState();
|
|
461
|
+
const command = [
|
|
462
|
+
"--role", role, "--manifest", manifestFile, "--cwd", workdir,
|
|
463
|
+
"--output", resultFile, "--evidence", evidenceFile, "--sandbox", args.sandbox,
|
|
464
|
+
"--closeout-ids", planState.acceptance.map((item) => item.id).join(","),
|
|
465
|
+
];
|
|
466
|
+
if (sessionId) command.push("--session-id", sessionId);
|
|
467
|
+
const repaired = runProviderRole(command, prompt);
|
|
468
|
+
writeFileSync(logFile, repaired.output, "utf8");
|
|
469
|
+
if (repaired.status !== 0) fail(`${role} 修正失败:${tail(repaired.output)}`);
|
|
470
|
+
const observed = readJson(evidenceFile, `${role} 修正证据`);
|
|
471
|
+
requireObservedRole(role, observed);
|
|
472
|
+
if (sessionId) requireSameThread(sessionId, observed, role);
|
|
473
|
+
runSummary.roles.push(roleSummary(`${role}-repair`, observed));
|
|
474
|
+
if (sessionId) runSummary.stage_transitions.push({ stage: "implementation", role, reason: "review-found-execution-defect", thread_id: sessionId });
|
|
475
|
+
const output = existsSync(resultFile) ? readFileSync(resultFile, "utf8").trim() : repaired.output.trim();
|
|
476
|
+
validateCloseout(output, planState.acceptance, `${role} 修正回交`);
|
|
477
|
+
const packet = buildEvidencePacket({
|
|
478
|
+
plan,
|
|
479
|
+
planState,
|
|
480
|
+
plannerObserved,
|
|
481
|
+
executionObserved: observed,
|
|
482
|
+
executionOutput: output,
|
|
483
|
+
repositoryBefore: before,
|
|
484
|
+
repositoryAfter: captureRepositoryState(),
|
|
485
|
+
reviewFinding: reviewState.output,
|
|
486
|
+
});
|
|
487
|
+
const packetFile = path.join(evidenceDir, "evidence-packet-repair.json");
|
|
488
|
+
writeFileSync(packetFile, `${JSON.stringify(packet, null, 2)}\n`, "utf8");
|
|
489
|
+
return {
|
|
490
|
+
role,
|
|
491
|
+
output,
|
|
492
|
+
evidence: evidenceFile,
|
|
493
|
+
observed,
|
|
494
|
+
packet,
|
|
495
|
+
packetFile,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function formatCapturedEvidence(evidence, plan) {
|
|
500
|
+
const observed = evidence?.observed || {};
|
|
501
|
+
const compact = {
|
|
502
|
+
provider: observed.provider || evidence?.requested?.provider || null,
|
|
503
|
+
models: observed.models || [],
|
|
504
|
+
cited_paths: plannerEvidencePointers(plan),
|
|
505
|
+
};
|
|
506
|
+
return JSON.stringify(compact, null, 2);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function plannerEvidencePointers(plan) {
|
|
510
|
+
const seen = new Set();
|
|
511
|
+
const pointers = [];
|
|
512
|
+
const source = String(plan || "");
|
|
513
|
+
const candidates = [
|
|
514
|
+
...[...source.matchAll(/`([^`\r\n]+)`/g)].map((match) => match[1]),
|
|
515
|
+
...[...source.matchAll(/\[[^\]\r\n]*\]\(([^)\r\n]+)\)/g)].map((match) => match[1]),
|
|
516
|
+
...[...source.matchAll(/\b(?:[A-Za-z0-9_.-]+\/)+[A-Za-z0-9_.-]+\b/g)].map((match) => match[0]),
|
|
517
|
+
];
|
|
518
|
+
for (const rawCandidate of candidates) {
|
|
519
|
+
const raw = String(rawCandidate || "").trim().replace(/(?::\d+)?(?:#.*)?$/, "");
|
|
520
|
+
if (!raw || raw.includes(" ") || raw.includes("*") || /^[a-z]+:\/\//i.test(raw)) continue;
|
|
521
|
+
const file = path.resolve(workdir, raw.replace(/^\.\//, ""));
|
|
522
|
+
if (file !== workdir && !file.startsWith(`${workdir}${path.sep}`)) continue;
|
|
523
|
+
if (!existsSync(file)) continue;
|
|
524
|
+
const candidate = path.relative(workdir, file) || ".";
|
|
525
|
+
if (seen.has(candidate)) continue;
|
|
526
|
+
seen.add(candidate);
|
|
527
|
+
pointers.push(candidate);
|
|
528
|
+
}
|
|
529
|
+
return pointers;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function captureCitedSources(value, repositoryBefore, repositoryAfter) {
|
|
533
|
+
const changed = repositoryChangedPaths(repositoryBefore, repositoryAfter);
|
|
534
|
+
let remaining = 50000;
|
|
535
|
+
return plannerEvidencePointers(value).flatMap((relativePath) => {
|
|
536
|
+
if (changed.has(relativePath)) return [];
|
|
537
|
+
if (remaining <= 0) return [];
|
|
538
|
+
try {
|
|
539
|
+
const data = readFileSync(path.resolve(workdir, relativePath));
|
|
540
|
+
if (data.includes(0)) return [{ path: relativePath, content: "[binary file omitted]" }];
|
|
541
|
+
const content = clip(data.toString("utf8"), Math.min(remaining, 20000));
|
|
542
|
+
remaining -= content.length;
|
|
543
|
+
return [{ path: relativePath, content }];
|
|
544
|
+
} catch {
|
|
545
|
+
return [{ path: relativePath, content: "[unreadable file]" }];
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function repositoryChangedPaths(before, after) {
|
|
551
|
+
const candidates = new Set();
|
|
552
|
+
for (const state of [before, after]) {
|
|
553
|
+
for (const item of state?.untracked_files || []) candidates.add(item.path);
|
|
554
|
+
for (const line of String(state?.status?.output || "").split(/\r?\n/)) {
|
|
555
|
+
if (line.length < 4) continue;
|
|
556
|
+
const pathText = line.slice(3).trim();
|
|
557
|
+
const parts = pathText.includes(" -> ") ? pathText.split(" -> ") : [pathText];
|
|
558
|
+
for (const item of parts) if (item) candidates.add(item.replace(/^"|"$/g, ""));
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return new Set([...candidates].filter((relativePath) => repositoryPathSignature(before, relativePath) !== repositoryPathSignature(after, relativePath)));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function repositoryPathSignature(state, relativePath) {
|
|
565
|
+
const statusLines = String(state?.status?.output || "").split(/\r?\n/)
|
|
566
|
+
.filter((line) => line.slice(3).trim().split(" -> ").some((item) => item.replace(/^"|"$/g, "") === relativePath));
|
|
567
|
+
const diffBlocks = [state?.unstaged_diff?.output, state?.staged_diff?.output]
|
|
568
|
+
.flatMap((value) => String(value || "").split(/(?=^diff --git )/m))
|
|
569
|
+
.filter((block) => block.startsWith("diff --git ") && block.split(/\r?\n/, 1)[0].includes(` b/${relativePath}`));
|
|
570
|
+
const untracked = (state?.untracked_files || []).find((item) => item.path === relativePath) || null;
|
|
571
|
+
return JSON.stringify({ statusLines, diffBlocks, untracked });
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function buildEvidencePacket({
|
|
575
|
+
plan,
|
|
576
|
+
planState,
|
|
577
|
+
plannerObserved,
|
|
578
|
+
executionObserved,
|
|
579
|
+
executionOutput,
|
|
580
|
+
repositoryBefore,
|
|
581
|
+
repositoryAfter,
|
|
582
|
+
reviewFinding = null,
|
|
583
|
+
executionFailure = null,
|
|
584
|
+
}) {
|
|
585
|
+
return {
|
|
586
|
+
version: 1,
|
|
587
|
+
immutable: true,
|
|
588
|
+
request,
|
|
589
|
+
plan,
|
|
590
|
+
acceptance: planState.acceptance,
|
|
591
|
+
review_ids: planState.reviewIds,
|
|
592
|
+
planner: {
|
|
593
|
+
requested: plannerObserved?.requested || null,
|
|
594
|
+
observed: pickRoleIdentityEvidence(plannerObserved?.observed),
|
|
595
|
+
},
|
|
596
|
+
execution: {
|
|
597
|
+
requested: executionObserved?.requested || null,
|
|
598
|
+
observed: {
|
|
599
|
+
...pickRoleIdentityEvidence(executionObserved?.observed),
|
|
600
|
+
verification_evidence: pickExecutionVerificationEvidence(executionObserved?.observed?.tool_evidence || []),
|
|
601
|
+
cited_sources: captureCitedSources(executionOutput, repositoryBefore, repositoryAfter),
|
|
602
|
+
},
|
|
603
|
+
output: executionOutput,
|
|
604
|
+
},
|
|
605
|
+
repository: {
|
|
606
|
+
before: repositoryBefore,
|
|
607
|
+
after: repositoryAfter,
|
|
608
|
+
},
|
|
609
|
+
prior_review_finding: reviewFinding,
|
|
610
|
+
prior_execution_failure: executionFailure,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function pickRoleIdentityEvidence(observed = {}) {
|
|
615
|
+
return {
|
|
616
|
+
provider: observed.provider || null,
|
|
617
|
+
models: observed.models || [],
|
|
618
|
+
model_verified: observed.model_verified === true,
|
|
619
|
+
reasoning_efforts: observed.reasoning_efforts || [],
|
|
620
|
+
tool_use_detected: observed.tool_use_detected ?? null,
|
|
621
|
+
usage: observed.usage || null,
|
|
622
|
+
cost_usd: observed.cost_usd ?? null,
|
|
623
|
+
duration_ms: observed.duration_ms ?? null,
|
|
624
|
+
exit_code: observed.exit_code ?? null,
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function pickExecutionVerificationEvidence(items) {
|
|
629
|
+
const lastChange = items.reduce((latest, item, index) => item?.type === "file_change" ? index : latest, -1);
|
|
630
|
+
return items.flatMap((item, index) => {
|
|
631
|
+
if (item?.type !== "command_execution" || item.status !== "completed") return [];
|
|
632
|
+
const command = String(item.command || "").trim();
|
|
633
|
+
if (!isVerificationCommand(command)) return [];
|
|
634
|
+
return [{
|
|
635
|
+
sequence: index,
|
|
636
|
+
after_last_file_change: index > lastChange,
|
|
637
|
+
command,
|
|
638
|
+
status: item.status,
|
|
639
|
+
exit_code: item.exit_code,
|
|
640
|
+
output: clip(String(item.output || ""), 30000),
|
|
641
|
+
}];
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function isVerificationCommand(command) {
|
|
646
|
+
if (!command) return false;
|
|
647
|
+
return /\bgit\s+(?:diff\s+--check|status\b)|\b(?:npm|pnpm|yarn|bun)\s+(?:test|run\s+(?:test|check|verify|lint|build|typecheck))\b|\b(?:pytest|go\s+test|cargo\s+test|make\s+(?:test|check|verify|lint|build))\b|\bnode\s+(?:--check|tests?\/|[^\s;|&]*?(?:test|check|verify|validate)[^\s;|&]*\.m?js\b)|\b(?:python3?|py\s+-3)\b[^\n]*(?:test|check|verify|validate)|\bassert(?:\.|\s|\()/i.test(command);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function captureRepositoryState() {
|
|
651
|
+
const status = runGit(["status", "--short", "--untracked-files=all"]);
|
|
652
|
+
return {
|
|
653
|
+
status,
|
|
654
|
+
unstaged_diff: runGit(["diff", "--no-ext-diff", "--binary", "--"]),
|
|
655
|
+
staged_diff: runGit(["diff", "--cached", "--no-ext-diff", "--binary", "--"]),
|
|
656
|
+
untracked_files: captureUntrackedFiles(),
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function captureUntrackedFiles() {
|
|
661
|
+
const result = spawnSync("git", ["ls-files", "--others", "--exclude-standard", "-z", "--"], {
|
|
662
|
+
cwd: workdir,
|
|
663
|
+
encoding: "buffer",
|
|
664
|
+
windowsHide: true,
|
|
665
|
+
timeout: Math.min(args.timeout, 60) * 1000,
|
|
666
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
667
|
+
});
|
|
668
|
+
if (result.status !== 0) return [];
|
|
669
|
+
const paths = result.stdout.toString("utf8").split("\0").filter(Boolean);
|
|
670
|
+
let remaining = 100000;
|
|
671
|
+
return paths.flatMap((relativePath) => {
|
|
672
|
+
if (remaining <= 0) return [];
|
|
673
|
+
const absolutePath = path.resolve(workdir, relativePath);
|
|
674
|
+
if (absolutePath !== workdir && !absolutePath.startsWith(`${workdir}${path.sep}`)) return [];
|
|
675
|
+
try {
|
|
676
|
+
const data = readFileSync(absolutePath);
|
|
677
|
+
if (data.includes(0)) return [{ path: relativePath, content: "[binary file omitted]" }];
|
|
678
|
+
const content = clip(data.toString("utf8"), Math.min(remaining, 50000));
|
|
679
|
+
remaining -= content.length;
|
|
680
|
+
return [{ path: relativePath, content }];
|
|
681
|
+
} catch {
|
|
682
|
+
return [{ path: relativePath, content: "[unreadable file]" }];
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function repositoryStateChanged(before, after) {
|
|
688
|
+
return JSON.stringify(before || {}) !== JSON.stringify(after || {});
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function runGit(commandArgs) {
|
|
692
|
+
const result = spawnSync("git", commandArgs, {
|
|
693
|
+
cwd: workdir,
|
|
694
|
+
encoding: "utf8",
|
|
695
|
+
windowsHide: true,
|
|
696
|
+
timeout: Math.min(args.timeout, 60) * 1000,
|
|
697
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
698
|
+
});
|
|
699
|
+
return {
|
|
700
|
+
command: `git ${commandArgs.join(" ")}`,
|
|
701
|
+
exit_code: result.status,
|
|
702
|
+
output: clip(`${result.stdout || ""}${result.stderr || ""}`, 100000),
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function validateCloseout(value, acceptance, label) {
|
|
707
|
+
const parsed = parseCloseout(value, label);
|
|
708
|
+
const expected = new Set(acceptance.map((item) => item.id));
|
|
709
|
+
const unknown = Object.keys(parsed).filter((id) => !expected.has(id));
|
|
710
|
+
if (unknown.length > 0) fail(`${label} 的 closeout 含未知验收 ID:${unknown.join(", ")}`);
|
|
711
|
+
for (const item of acceptance) {
|
|
712
|
+
const report = parsed[item.id];
|
|
713
|
+
if (!report || typeof report !== "object" || Array.isArray(report)) {
|
|
714
|
+
fail(`${label} 的 closeout 缺少 ${item.id}`);
|
|
715
|
+
}
|
|
716
|
+
const unknownFields = Object.keys(report).filter((name) => !new Set(["status", "evidence", "next"]).has(name));
|
|
717
|
+
if (unknownFields.length > 0) {
|
|
718
|
+
fail(`${label} 的 closeout 中 ${item.id} 含未知字段:${unknownFields.join(", ")}`);
|
|
719
|
+
}
|
|
720
|
+
if (!new Set(["verified", "unresolved", "failed"]).has(report.status)) {
|
|
721
|
+
fail(`${label} 的 closeout 中 ${item.id} 状态无效`);
|
|
722
|
+
}
|
|
723
|
+
if (!cleanInline(report.evidence)) fail(`${label} 的 closeout 中 ${item.id} 缺少实际证据`);
|
|
724
|
+
const next = cleanInline(report.next);
|
|
725
|
+
if (report.status === "verified" && next) {
|
|
726
|
+
fail(`${label} 的 closeout 中 ${item.id} 已写 verified 却仍有继续条件`);
|
|
727
|
+
}
|
|
728
|
+
if (report.status !== "verified" && !next) {
|
|
729
|
+
fail(`${label} 的 closeout 中 ${item.id} 未闭合却缺少继续条件`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return parsed;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function parseCloseout(value, label = "交付") {
|
|
736
|
+
const blocks = [...String(value || "").matchAll(/<odai_closeout>\s*([\s\S]*?)\s*<\/odai_closeout>/gi)];
|
|
737
|
+
if (blocks.length !== 1) fail(`${label} 必须且只能包含一个 <odai_closeout>`);
|
|
738
|
+
try {
|
|
739
|
+
const parsed = parseCloseoutJson(blocks[0][1]);
|
|
740
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("根节点不是对象");
|
|
741
|
+
return parsed;
|
|
742
|
+
} catch (error) {
|
|
743
|
+
fail(`${label} 的 closeout 不是有效 JSON:${error.message}`);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function parseCloseoutJson(value) {
|
|
748
|
+
const text = String(value || "").trim();
|
|
749
|
+
try {
|
|
750
|
+
return JSON.parse(text);
|
|
751
|
+
} catch (originalError) {
|
|
752
|
+
if (!text.startsWith("{")) throw originalError;
|
|
753
|
+
let depth = 0;
|
|
754
|
+
let inString = false;
|
|
755
|
+
let escaped = false;
|
|
756
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
757
|
+
const character = text[index];
|
|
758
|
+
if (inString) {
|
|
759
|
+
if (escaped) escaped = false;
|
|
760
|
+
else if (character === "\\") escaped = true;
|
|
761
|
+
else if (character === '"') inString = false;
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
if (character === '"') inString = true;
|
|
765
|
+
else if (character === "{") depth += 1;
|
|
766
|
+
else if (character === "}") depth -= 1;
|
|
767
|
+
if (depth !== 0) continue;
|
|
768
|
+
const trailing = text.slice(index + 1).trim();
|
|
769
|
+
if (!trailing || !/^}+$/.test(trailing)) throw originalError;
|
|
770
|
+
return JSON.parse(text.slice(0, index + 1));
|
|
771
|
+
}
|
|
772
|
+
throw originalError;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function clip(value, limit) {
|
|
777
|
+
const text = String(value || "");
|
|
778
|
+
if (text.length <= limit) return text;
|
|
779
|
+
return `${text.slice(0, limit)}\n...[truncated ${text.length - limit} chars]`;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function parseReviewDecision(value) {
|
|
783
|
+
const lines = String(value || "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
784
|
+
const first = lines[0] || "";
|
|
785
|
+
const match = /^(pass|fail|unresolved)\b/i.exec(first);
|
|
786
|
+
if (!match) fail("reviewer 回执首行必须是 pass、fail 或 unresolved");
|
|
787
|
+
const routeMatch = /^route\s*:\s*(none|execution|planning|user|blocked)\s*$/i.exec(lines[1] || "");
|
|
788
|
+
if (!routeMatch) fail("reviewer 回执第二行必须是 route: none、execution、planning、user 或 blocked");
|
|
789
|
+
const status = match[1].toLowerCase();
|
|
790
|
+
const route = routeMatch[1].toLowerCase();
|
|
791
|
+
const allowed = {
|
|
792
|
+
pass: new Set(["none"]),
|
|
793
|
+
fail: new Set(["execution", "planning"]),
|
|
794
|
+
unresolved: new Set(["planning", "user", "blocked"]),
|
|
795
|
+
};
|
|
796
|
+
if (!allowed[status].has(route)) fail(`reviewer 回执组合无效:${status} / ${route}`);
|
|
797
|
+
return { status, route };
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function requireObservedRole(role, evidence) {
|
|
801
|
+
const expected = manifest.mapping?.[role];
|
|
802
|
+
const observed = evidence?.observed || {};
|
|
803
|
+
if (!observed.model_verified || !Array.isArray(observed.models) || observed.models.length === 0) {
|
|
804
|
+
fail(`${role} 没有形成可核实的实际模型证据`);
|
|
805
|
+
}
|
|
806
|
+
if ((expected?.provider || manifest.host) === "codex" && !observed.models.includes(expected.model)) {
|
|
807
|
+
fail(`${role} 实际模型不匹配:期望 ${expected.model},实际 ${observed.models.join(", ")}`);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function requireSameThread(expected, evidence, role) {
|
|
812
|
+
const actual = String(evidence?.observed?.thread_id || "");
|
|
813
|
+
if (!actual || actual !== expected) {
|
|
814
|
+
fail(`${role} 没有恢复总控线程:期望 ${expected},实际 ${actual || "unknown"}`);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function roleSummary(role, evidence) {
|
|
819
|
+
return {
|
|
820
|
+
role,
|
|
821
|
+
requested: evidence.requested || null,
|
|
822
|
+
observed: evidence.observed || null,
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function summarizeRoles(roles) {
|
|
827
|
+
const total = {
|
|
828
|
+
calls: roles.length,
|
|
829
|
+
duration_ms: 0,
|
|
830
|
+
cost_usd_known: 0,
|
|
831
|
+
cost_usd_complete: true,
|
|
832
|
+
usage: {},
|
|
833
|
+
models: [],
|
|
834
|
+
};
|
|
835
|
+
const models = new Set();
|
|
836
|
+
for (const item of roles) {
|
|
837
|
+
const observed = item?.observed || {};
|
|
838
|
+
total.duration_ms += Number(observed.duration_ms) || 0;
|
|
839
|
+
if (Number.isFinite(Number(observed.cost_usd))) total.cost_usd_known += Number(observed.cost_usd);
|
|
840
|
+
else total.cost_usd_complete = false;
|
|
841
|
+
for (const model of observed.models || []) models.add(model);
|
|
842
|
+
for (const [key, value] of Object.entries(observed.usage || {})) {
|
|
843
|
+
if (Number.isFinite(Number(value))) total.usage[key] = (total.usage[key] || 0) + Number(value);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
total.models = [...models];
|
|
847
|
+
total.cost_usd_known = Number(total.cost_usd_known.toFixed(6));
|
|
848
|
+
return total;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function normalizePlanCard(value) {
|
|
852
|
+
const source = String(value || "").trim();
|
|
853
|
+
const wrapped = /<odai_(delivery|plan)(?:\s+[^>]*)?>\s*([\s\S]*?)\s*<\/odai_\1>/i.exec(source);
|
|
854
|
+
if (!wrapped) return normalizeAcceptanceIds(normalizeReviewField(source));
|
|
855
|
+
let parsed;
|
|
856
|
+
try {
|
|
857
|
+
parsed = JSON.parse(wrapped[2]);
|
|
858
|
+
} catch {
|
|
859
|
+
return normalizeAcceptanceIds(source);
|
|
860
|
+
}
|
|
861
|
+
const declaredMode = String(parsed?.status || parsed?.mode || parsed?.route
|
|
862
|
+
|| (wrapped[1].toLowerCase() === "plan" ? "planned" : "")).toLowerCase();
|
|
863
|
+
if (declaredMode !== "planned") return source;
|
|
864
|
+
const required = ["target", "evidence", "scope", "decision", "execute", "review", "accept", "stop"];
|
|
865
|
+
if (required.some((key) => parsed[key] == null)) return source;
|
|
866
|
+
const list = (value) => Array.isArray(value) ? value : value == null ? [] : [value];
|
|
867
|
+
const scalar = (value) => typeof value === "string" ? value : JSON.stringify(value);
|
|
868
|
+
const rawReview = parsed.review && typeof parsed.review === "object" && !Array.isArray(parsed.review)
|
|
869
|
+
? parsed.review.accept_ids ?? parsed.review.ids ?? []
|
|
870
|
+
: parsed.review;
|
|
871
|
+
const reviewIds = list(rawReview).flatMap((value) => String(value).match(/\b[A-Za-z][A-Za-z0-9_-]*\d[A-Za-z0-9_-]*\b/g) || []);
|
|
872
|
+
const acceptance = parsed.accept && typeof parsed.accept === "object" && !Array.isArray(parsed.accept)
|
|
873
|
+
? Object.entries(parsed.accept)
|
|
874
|
+
: list(parsed.accept).map((condition, index) => [`A${index + 1}`, condition]);
|
|
875
|
+
const steps = list(parsed.steps ?? parsed.slices);
|
|
876
|
+
return normalizeAcceptanceIds(normalizeReviewField([
|
|
877
|
+
"mode: planned",
|
|
878
|
+
`target: ${scalar(parsed.target)}`,
|
|
879
|
+
`evidence: ${scalar(parsed.evidence)}`,
|
|
880
|
+
`scope: ${scalar(parsed.scope)}`,
|
|
881
|
+
`decision: ${scalar(parsed.decision)}`,
|
|
882
|
+
`execute: ${scalar(parsed.execute)}`,
|
|
883
|
+
`review: ${reviewIds.length > 0 ? `reviewer ${reviewIds.join(" ")}` : "none"}`,
|
|
884
|
+
"accept:",
|
|
885
|
+
...acceptance.map(([id, condition]) => `- ${id}: ${scalar(condition)}`),
|
|
886
|
+
`stop: ${scalar(parsed.stop)}`,
|
|
887
|
+
...(steps.length > 0 ? ["steps:", ...steps.map((item) => `- ${scalar(item)}`)] : []),
|
|
888
|
+
].join("\n")));
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function normalizeReviewField(value) {
|
|
892
|
+
const source = String(value || "").trim();
|
|
893
|
+
if (field(source, "mode").toLowerCase() !== "planned") return source;
|
|
894
|
+
const lines = source.split(/\r?\n/);
|
|
895
|
+
const start = lines.findIndex((line) => topLevelFieldLine("review").test(line));
|
|
896
|
+
if (start < 0) return source;
|
|
897
|
+
const inline = /^\s*(?:#{1,6}\s*)?(?:[-*]\s*)?review\s*[::]\s*(\S[\s\S]*)?\s*$/i.exec(lines[start]);
|
|
898
|
+
if (inline?.[1]) return source;
|
|
899
|
+
let end = lines.length;
|
|
900
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
901
|
+
if (["target", "evidence", "scope", "decision", "execute", "accept", "stop", "steps?", "slices?"]
|
|
902
|
+
.some((name) => topLevelFieldLine(name).test(lines[index]))) {
|
|
903
|
+
end = index;
|
|
904
|
+
break;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
const section = lines.slice(start + 1, end).join("\n");
|
|
908
|
+
if (/^\s*(?:[-*]\s*)?none\s*$/im.test(section)) {
|
|
909
|
+
lines.splice(start, end - start, "review: none");
|
|
910
|
+
return lines.join("\n");
|
|
911
|
+
}
|
|
912
|
+
const ids = [...new Set(expandReviewIds(section))];
|
|
913
|
+
if (!/\breviewer\b/i.test(section) || ids.length === 0) return source;
|
|
914
|
+
lines.splice(start, end - start, "review: reviewer " + ids.join(" "));
|
|
915
|
+
return lines.join("\n");
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function normalizeAcceptanceIds(value) {
|
|
919
|
+
const source = String(value || "").trim();
|
|
920
|
+
if (field(source, "mode").toLowerCase() !== "planned") return source;
|
|
921
|
+
const lines = source.split(/\r?\n/);
|
|
922
|
+
const start = lines.findIndex((line) => topLevelFieldLine("accept").test(line));
|
|
923
|
+
if (start < 0) return source;
|
|
924
|
+
let end = lines.length;
|
|
925
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
926
|
+
if (["stop", "steps?", "slices?"].some((name) => topLevelFieldLine(name).test(lines[index]))) {
|
|
927
|
+
end = index;
|
|
928
|
+
break;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
const explicit = new Set();
|
|
932
|
+
for (const line of lines.slice(start + 1, end)) {
|
|
933
|
+
const match = /^\s*(?:[-*]\s*)?([A-Za-z][A-Za-z0-9_-]*\d[A-Za-z0-9_-]*)(?:\s*[::]\s*|\s+)(\S[\s\S]*)$/i.exec(line);
|
|
934
|
+
if (match) explicit.add(match[1].toUpperCase());
|
|
935
|
+
}
|
|
936
|
+
let next = 1;
|
|
937
|
+
const generatedId = () => {
|
|
938
|
+
while (explicit.has(`A${next}`)) next += 1;
|
|
939
|
+
const id = `A${next}`;
|
|
940
|
+
explicit.add(id);
|
|
941
|
+
next += 1;
|
|
942
|
+
return id;
|
|
943
|
+
};
|
|
944
|
+
for (let index = start + 1; index < end; index += 1) {
|
|
945
|
+
if (/^\s*(?:[-*]\s*)?([A-Za-z][A-Za-z0-9_-]*\d[A-Za-z0-9_-]*)(?:\s*[::]\s*|\s+)\S/i.test(lines[index])) continue;
|
|
946
|
+
const implicit = /^\s*(?:[-*]\s+|\d+[.)]\s+)(\S[\s\S]*)$/.exec(lines[index]);
|
|
947
|
+
if (implicit) lines[index] = `- ${generatedId()}: ${implicit[1].trim()}`;
|
|
948
|
+
}
|
|
949
|
+
return lines.join("\n");
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function stripInternalBlocks(value) {
|
|
953
|
+
return String(value || "")
|
|
954
|
+
.replace(/<odai_(?:plan|closeout)(?:\s+[^>]*)?>[\s\S]*?<\/odai_(?:plan|closeout)>/gi, "")
|
|
955
|
+
.trim();
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function validatePlan(value, directOwner = "planner") {
|
|
959
|
+
if (!value) fail("planner 没有形成路由或交付");
|
|
960
|
+
const first = String(value).split(/\r?\n/).find((line) => line.trim()) || "";
|
|
961
|
+
if (!/^\s*mode\s*[::]\s*(direct|planned)\s*$/i.test(first)) {
|
|
962
|
+
fail("planner 规划路由无效:首个非空行必须且只能是 mode: direct 或 mode: planned");
|
|
963
|
+
}
|
|
964
|
+
const mode = field(value, "mode").toLowerCase();
|
|
965
|
+
if (mode === "direct") {
|
|
966
|
+
if (countTopLevelFields(value, "mode") !== 1) fail("direct 交付的 mode 不得重复");
|
|
967
|
+
directExecutionOutput(value);
|
|
968
|
+
return { mode, acceptance: [], execute: directOwner, reviewIds: [] };
|
|
969
|
+
}
|
|
970
|
+
const requiredFields = ["mode", "target", "evidence", "scope", "decision", "execute", "review", "accept", "stop"];
|
|
971
|
+
const missing = requiredFields.filter((name) => !hasTopLevelField(value, name));
|
|
972
|
+
if (missing.length > 0) fail(`planner 规划路由无效,缺少:${missing.join(", ")}`);
|
|
973
|
+
const duplicated = requiredFields.filter((name) => {
|
|
974
|
+
return countTopLevelFields(value, name) > 1;
|
|
975
|
+
});
|
|
976
|
+
if (duplicated.length > 0) {
|
|
977
|
+
fail(`planner 规划路由无效:顶层字段不得重复:${duplicated.join(", ")}`);
|
|
978
|
+
}
|
|
979
|
+
const normalizedMode = mode;
|
|
980
|
+
const execute = field(value, "execute").toLowerCase().match(/^(?:controller|executor)\b/)?.[0] || "";
|
|
981
|
+
if (!execute) fail("规划路由无效:planned 的 execute 只能是 controller 或 executor");
|
|
982
|
+
const acceptance = parseAcceptance(value);
|
|
983
|
+
if (acceptance.length === 0) fail("planner 规划路由无效:accept 下必须有逐项、非空的验收条件");
|
|
984
|
+
const reviewIds = validateReview(value, acceptance);
|
|
985
|
+
if (/<odai_(?:delivery|closeout)>/i.test(value)) {
|
|
986
|
+
fail("planner 规划路由无效:planned 尚未实施,不能输出交付或 closeout");
|
|
987
|
+
}
|
|
988
|
+
return { mode: normalizedMode, acceptance, execute, reviewIds };
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function validateReview(value, acceptance) {
|
|
992
|
+
const review = field(value, "review");
|
|
993
|
+
if (/^none(?:\b|[;;])/i.test(review)) return [];
|
|
994
|
+
if (!/\breviewer\b/i.test(review)) {
|
|
995
|
+
fail("planner 规划路由无效:review 只能是 none,或点名 reviewer 与验收 ID");
|
|
996
|
+
}
|
|
997
|
+
if (/\bnone\b/i.test(review)) fail("planner 规划路由无效:reviewer 路由不能混入 none");
|
|
998
|
+
if (/\b[A-Za-z][A-Za-z0-9_-]*\d[A-Za-z0-9_-]*\s*(?:等|等等|及其他)(?:\s|$|[,,;;])|(?:全部|所有)验收/i.test(review)) {
|
|
999
|
+
fail("planner 规划路由无效:reviewer 路由不能使用开放式验收缩写");
|
|
1000
|
+
}
|
|
1001
|
+
const known = new Set(acceptance.map((item) => item.id));
|
|
1002
|
+
const ids = expandReviewIds(review);
|
|
1003
|
+
if (ids.length === 0) fail("planner 规划路由无效:reviewer 路由必须点名待判定的验收 ID");
|
|
1004
|
+
const unknown = ids.filter((id) => !known.has(id));
|
|
1005
|
+
if (unknown.length > 0) fail(`planner 规划路由无效:review 引用了未知验收 ID ${[...new Set(unknown)].join(", ")}`);
|
|
1006
|
+
return [...new Set(ids)];
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function expandReviewIds(value) {
|
|
1010
|
+
const source = String(value || "");
|
|
1011
|
+
const ids = [];
|
|
1012
|
+
const ranges = [];
|
|
1013
|
+
const rangePattern = /\b([A-Za-z][A-Za-z0-9_-]*?)(\d+)\s*[-–—]\s*\1(\d+)\b/gi;
|
|
1014
|
+
let match;
|
|
1015
|
+
while ((match = rangePattern.exec(source))) {
|
|
1016
|
+
const start = Number(match[2]);
|
|
1017
|
+
const end = Number(match[3]);
|
|
1018
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 1 || end < start || end - start > 99) {
|
|
1019
|
+
fail(`planner 规划路由无效:无法展开验收范围 ${match[0]}`);
|
|
1020
|
+
}
|
|
1021
|
+
const prefix = match[1].toUpperCase();
|
|
1022
|
+
for (let index = start; index <= end; index += 1) ids.push(`${prefix}${index}`);
|
|
1023
|
+
ranges.push([match.index, match.index + match[0].length]);
|
|
1024
|
+
}
|
|
1025
|
+
const outsideRanges = [...source]
|
|
1026
|
+
.map((character, index) => ranges.some(([start, end]) => index >= start && index < end) ? " " : character)
|
|
1027
|
+
.join("");
|
|
1028
|
+
for (const exact of outsideRanges.matchAll(/\b[A-Za-z][A-Za-z0-9_-]*\d[A-Za-z0-9_-]*\b/g)) {
|
|
1029
|
+
ids.push(exact[0].toUpperCase());
|
|
1030
|
+
}
|
|
1031
|
+
return ids;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function parseAcceptance(value) {
|
|
1035
|
+
const lines = String(value || "").split(/\r?\n/);
|
|
1036
|
+
const start = lines.findIndex((line) => topLevelFieldLine("accept").test(line));
|
|
1037
|
+
if (start < 0) return [];
|
|
1038
|
+
const result = [];
|
|
1039
|
+
const seen = new Set();
|
|
1040
|
+
for (const line of lines.slice(start + 1)) {
|
|
1041
|
+
if (["stop", "steps?", "slices?"].some((name) => topLevelFieldLine(name).test(line))) break;
|
|
1042
|
+
const match = /^\s*(?:[-*]\s*)?([A-Za-z][A-Za-z0-9_-]*\d[A-Za-z0-9_-]*)(?:\s*[::]\s*|\s+)(\S[\s\S]*)$/i.exec(line);
|
|
1043
|
+
if (!match) continue;
|
|
1044
|
+
const id = match[1].toUpperCase();
|
|
1045
|
+
if (seen.has(id)) fail(`planner 规划路由无效:重复验收 ID ${id}`);
|
|
1046
|
+
seen.add(id);
|
|
1047
|
+
result.push({ id, condition: match[2].trim() });
|
|
1048
|
+
}
|
|
1049
|
+
return result;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function renderDelivery(raw, acceptance, reviewState = null, reviewIds = []) {
|
|
1053
|
+
const source = String(raw || "");
|
|
1054
|
+
const closeout = parseCloseout(source, "最终执行回交");
|
|
1055
|
+
const message = source.replace(/<odai_closeout>[\s\S]*?<\/odai_closeout>/gi, "").trim();
|
|
1056
|
+
const passedReviewIds = new Set(reviewState?.status === "pass" ? reviewIds : []);
|
|
1057
|
+
const verified = [];
|
|
1058
|
+
const open = [];
|
|
1059
|
+
for (const item of acceptance) {
|
|
1060
|
+
const report = closeout[item.id] && typeof closeout[item.id] === "object" ? closeout[item.id] : {};
|
|
1061
|
+
const promotedByReview = passedReviewIds.has(item.id) && report.status !== "verified";
|
|
1062
|
+
const status = passedReviewIds.has(item.id)
|
|
1063
|
+
? "verified"
|
|
1064
|
+
: new Set(["verified", "unresolved", "failed"]).has(report.status)
|
|
1065
|
+
? report.status
|
|
1066
|
+
: "unresolved";
|
|
1067
|
+
const evidence = promotedByReview
|
|
1068
|
+
? ""
|
|
1069
|
+
: cleanInline(report.evidence || (status === "unresolved" ? "尚未提供足以闭合该项的证据" : ""));
|
|
1070
|
+
const next = cleanInline(report.next || "");
|
|
1071
|
+
if (status === "verified") {
|
|
1072
|
+
if (evidence && !includesLoose(message, evidence)) verified.push(evidence);
|
|
1073
|
+
continue;
|
|
1074
|
+
}
|
|
1075
|
+
const label = status === "failed" ? "未通过" : "未验证";
|
|
1076
|
+
const missingEvidence = evidence && !includesLoose(message, evidence) ? evidence : "";
|
|
1077
|
+
const missingNext = next && !includesLoose(message, next) ? next : "";
|
|
1078
|
+
if (!missingEvidence && !missingNext) continue;
|
|
1079
|
+
const details = [missingEvidence && `原因:${missingEvidence}`, missingNext && `继续条件:${missingNext}`].filter(Boolean).join(";");
|
|
1080
|
+
open.push(`- ${item.id} ${label}${details ? `:${details}` : ""}`);
|
|
1081
|
+
}
|
|
1082
|
+
const sections = [message];
|
|
1083
|
+
if (passedReviewIds.size > 0) sections.push(`独立验收:${[...passedReviewIds].join(", ")} 全部通过。`);
|
|
1084
|
+
if (verified.length > 0) sections.push(`验证:\n${verified.map((item) => `- ${item}`).join("\n")}`);
|
|
1085
|
+
if (open.length > 0) sections.push(`尚未闭合:\n${open.join("\n")}`);
|
|
1086
|
+
if (reviewState && reviewState.status !== "pass") {
|
|
1087
|
+
sections.push(`独立验收未闭合:\n${reviewState.output}`);
|
|
1088
|
+
}
|
|
1089
|
+
return `${sections.filter(Boolean).join("\n\n").trim()}\n`;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function cleanInline(value) {
|
|
1093
|
+
return String(value || "").replace(/\s+/g, " ").trim();
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function includesLoose(message, value) {
|
|
1097
|
+
const normalize = (text) => String(text || "").toLowerCase().replace(/[\s`*_,。;:、,:;.!?()[\]{}'"“”‘’-]+/g, "");
|
|
1098
|
+
const needle = normalize(value);
|
|
1099
|
+
return needle.length >= 8 && normalize(message).includes(needle);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function field(value, name) {
|
|
1103
|
+
const match = new RegExp(`(?:^|\\n)\\s*(?:[-*]\\s*)?${name}\\s*[::]\\s*([^\\n]+)`, "i").exec(value);
|
|
1104
|
+
return String(match?.[1] || "").trim();
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function topLevelFieldLine(name, flags = "i") {
|
|
1108
|
+
return new RegExp(`^\\s*(?:#{1,6}\\s*)?(?:[-*]\\s*)?${name}(?:\\s*[::]\\s*[^\\n]*)?\\s*$`, flags);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function hasTopLevelField(value, name) {
|
|
1112
|
+
return String(value || "").split(/\r?\n/).some((line) => topLevelFieldLine(name).test(line));
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function countTopLevelFields(value, name) {
|
|
1116
|
+
return String(value || "").split(/\r?\n/).filter((line) => topLevelFieldLine(name).test(line)).length;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function readJson(file, label) {
|
|
1120
|
+
if (!existsSync(file)) fail(`缺少${label}:${file}`);
|
|
1121
|
+
try {
|
|
1122
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
fail(`无法读取${label}:${error.message}`);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function parseArgs(argv) {
|
|
1129
|
+
const result = { cwd: "", output: "", sandbox: "workspace-write", manifest: "", codexBin: "", evidenceDir: "", timeout: 900, help: false };
|
|
1130
|
+
const fields = new Map([
|
|
1131
|
+
["--cwd", "cwd"], ["--output", "output"], ["--sandbox", "sandbox"], ["--manifest", "manifest"],
|
|
1132
|
+
["--codex-bin", "codexBin"],
|
|
1133
|
+
["--evidence-dir", "evidenceDir"], ["--timeout", "timeout"],
|
|
1134
|
+
]);
|
|
1135
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1136
|
+
const value = argv[index];
|
|
1137
|
+
if (value === "--help" || value === "-h") result.help = true;
|
|
1138
|
+
else if (fields.has(value)) {
|
|
1139
|
+
const next = argv[++index] || "";
|
|
1140
|
+
if (!next || next.startsWith("--")) fail(`${value} 需要一个值`);
|
|
1141
|
+
result[fields.get(value)] = fields.get(value) === "timeout" ? Number(next) : next;
|
|
1142
|
+
} else fail(`未知参数:${value}`);
|
|
1143
|
+
}
|
|
1144
|
+
if (!Number.isFinite(result.timeout) || result.timeout <= 0) fail("--timeout 必须是正数秒数");
|
|
1145
|
+
return result;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function tail(value) {
|
|
1149
|
+
return String(value || "").trim().slice(-2000);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function fail(message) {
|
|
1153
|
+
throw new Error(message);
|
|
1154
|
+
}
|