soloforge 1.1.36 → 1.1.37

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.
Files changed (84) hide show
  1. package/dist/engine/audit/evolver.d.ts.map +1 -1
  2. package/dist/engine/audit/evolver.js +29 -2
  3. package/dist/engine/audit/evolver.js.map +1 -1
  4. package/dist/engine/contracts/architecture_decision_workshop.d.ts +6 -1
  5. package/dist/engine/contracts/architecture_decision_workshop.d.ts.map +1 -1
  6. package/dist/engine/contracts/architecture_decision_workshop.js +5 -1
  7. package/dist/engine/contracts/architecture_decision_workshop.js.map +1 -1
  8. package/dist/engine/knowledge/knowledge_injection_boundary.d.ts.map +1 -1
  9. package/dist/engine/knowledge/knowledge_injection_boundary.js +7 -3
  10. package/dist/engine/knowledge/knowledge_injection_boundary.js.map +1 -1
  11. package/dist/engine/pipeline/classifier.js +6 -4
  12. package/dist/engine/pipeline/classifier.js.map +1 -1
  13. package/dist/engine/pipeline/intent_expander/knowledge.d.ts.map +1 -1
  14. package/dist/engine/pipeline/intent_expander/knowledge.js +17 -8
  15. package/dist/engine/pipeline/intent_expander/knowledge.js.map +1 -1
  16. package/dist/engine/pipeline/intent_expander/templates.d.ts +1 -0
  17. package/dist/engine/pipeline/intent_expander/templates.d.ts.map +1 -1
  18. package/dist/engine/pipeline/intent_expander/templates.js +73 -0
  19. package/dist/engine/pipeline/intent_expander/templates.js.map +1 -1
  20. package/dist/engine/pipeline/intent_router.d.ts +22 -1
  21. package/dist/engine/pipeline/intent_router.d.ts.map +1 -1
  22. package/dist/engine/pipeline/intent_router.js +130 -0
  23. package/dist/engine/pipeline/intent_router.js.map +1 -1
  24. package/dist/engine/pipeline/intent_signal_extractor.d.ts +2 -2
  25. package/dist/engine/pipeline/intent_signal_extractor.d.ts.map +1 -1
  26. package/dist/engine/pipeline/intent_signal_extractor.js +77 -3
  27. package/dist/engine/pipeline/intent_signal_extractor.js.map +1 -1
  28. package/dist/engine/pipeline/task_context/manager.d.ts +39 -0
  29. package/dist/engine/pipeline/task_context/manager.d.ts.map +1 -1
  30. package/dist/engine/pipeline/task_context/manager.js +293 -109
  31. package/dist/engine/pipeline/task_context/manager.js.map +1 -1
  32. package/dist/engine/pipeline/task_context/manager_setters.d.ts +23 -1
  33. package/dist/engine/pipeline/task_context/manager_setters.d.ts.map +1 -1
  34. package/dist/engine/pipeline/task_context/manager_setters.js +237 -119
  35. package/dist/engine/pipeline/task_context/manager_setters.js.map +1 -1
  36. package/dist/engine/pipeline/task_stage_detector.d.ts.map +1 -1
  37. package/dist/engine/pipeline/task_stage_detector.js +13 -0
  38. package/dist/engine/pipeline/task_stage_detector.js.map +1 -1
  39. package/dist/engine/release/release_issue_scenario_registry/scenarios_template_contract.d.ts.map +1 -1
  40. package/dist/engine/release/release_issue_scenario_registry/scenarios_template_contract.js +84 -2
  41. package/dist/engine/release/release_issue_scenario_registry/scenarios_template_contract.js.map +1 -1
  42. package/dist/engine/templates/explicit_asset_registry/procedures_part2.d.ts.map +1 -1
  43. package/dist/engine/templates/explicit_asset_registry/procedures_part2.js +17 -0
  44. package/dist/engine/templates/explicit_asset_registry/procedures_part2.js.map +1 -1
  45. package/dist/engine/workflow/legacy_type_migration.d.ts.map +1 -1
  46. package/dist/engine/workflow/legacy_type_migration.js +5 -0
  47. package/dist/engine/workflow/legacy_type_migration.js.map +1 -1
  48. package/dist/engine/workflow/workflow_contract_registry.d.ts.map +1 -1
  49. package/dist/engine/workflow/workflow_contract_registry.js +24 -0
  50. package/dist/engine/workflow/workflow_contract_registry.js.map +1 -1
  51. package/dist/knowledge/index_manager.d.ts +6 -0
  52. package/dist/knowledge/index_manager.d.ts.map +1 -1
  53. package/dist/knowledge/index_manager.js +168 -55
  54. package/dist/knowledge/index_manager.js.map +1 -1
  55. package/dist/knowledge/writer.d.ts +10 -0
  56. package/dist/knowledge/writer.d.ts.map +1 -1
  57. package/dist/knowledge/writer.js +40 -0
  58. package/dist/knowledge/writer.js.map +1 -1
  59. package/dist/server/tools/gate_checks.d.ts +9 -0
  60. package/dist/server/tools/gate_checks.d.ts.map +1 -1
  61. package/dist/server/tools/gate_checks.js +55 -1
  62. package/dist/server/tools/gate_checks.js.map +1 -1
  63. package/dist/server/tools/middleware.d.ts.map +1 -1
  64. package/dist/server/tools/middleware.js +444 -381
  65. package/dist/server/tools/middleware.js.map +1 -1
  66. package/dist/server/tools/schemas.d.ts +38 -5
  67. package/dist/server/tools/schemas.d.ts.map +1 -1
  68. package/dist/server/tools/schemas.js +23 -7
  69. package/dist/server/tools/schemas.js.map +1 -1
  70. package/dist/server/tools/tool_groups/expand_handler.d.ts.map +1 -1
  71. package/dist/server/tools/tool_groups/expand_handler.js +40 -50
  72. package/dist/server/tools/tool_groups/expand_handler.js.map +1 -1
  73. package/dist/server/tools/tool_groups/status_plan_analyze_review.d.ts.map +1 -1
  74. package/dist/server/tools/tool_groups/status_plan_analyze_review.js +26 -0
  75. package/dist/server/tools/tool_groups/status_plan_analyze_review.js.map +1 -1
  76. package/dist/server/tools/tool_groups/verify_learn.d.ts.map +1 -1
  77. package/dist/server/tools/tool_groups/verify_learn.js +155 -7
  78. package/dist/server/tools/tool_groups/verify_learn.js.map +1 -1
  79. package/dist/types/pipeline.d.ts +2 -2
  80. package/dist/types/pipeline.d.ts.map +1 -1
  81. package/dist/types/task.d.ts +26 -0
  82. package/dist/types/task.d.ts.map +1 -1
  83. package/package.json +3 -3
  84. package/templates/procedures//350/256/276/350/256/241/345/256/241/350/256/241/346/265/201/347/250/213.md +186 -0
@@ -6,7 +6,7 @@
6
6
  // 从 tools.ts 第 1096-1658 行提取。
7
7
  import path from "node:path";
8
8
  import { promises as fsp } from "node:fs";
9
- import { debug } from "../../engine/core/logger.js";
9
+ import { debug, internalWarn as warn } from "../../engine/core/logger.js";
10
10
  import { ENV } from "../../engine/core/env.js";
11
11
  import { findToolInvocationContractByName, createToolTrace, validateToolInvocation, hasWriteSideEffect, } from "../../engine/contracts/tool_invocation_contract_registry.js";
12
12
  import { TOOL_DIAGNOSTIC_CODES } from "../../engine/audit/diagnostic_registry.js";
@@ -36,7 +36,9 @@ export function createToolRegistrar(ctx) {
36
36
  /** 状态预检: 在合同守卫之前验证任务状态 */
37
37
  const STATE_PRECHECKS = {
38
38
  sf_accept: ["executing", "verifying", "retrying"],
39
- sf_verify: ["executing", "retrying"],
39
+ // P0-C2: sf_verify 接受 verifying 状态,与 sf_status resume 提示对齐
40
+ // 防止任务卡 verifying 时 sf_status 提示调用 sf_verify 但被状态预检拒绝
41
+ sf_verify: ["executing", "retrying", "verifying"],
40
42
  sf_record_verification_execution: ["verifying", "executing"],
41
43
  sf_learn: ["verifying", "executing"],
42
44
  sf_expand: ["classifying", "expanding", "clarifying"],
@@ -112,314 +114,184 @@ export function createToolRegistrar(ctx) {
112
114
  }
113
115
  // ── 安全工具注册 ──
114
116
  function registerSafeTool(name, desc, schema, handler) {
115
- // @ts-expect-error -- MCP SDK 桥接点: schema 参数类型不兼容 Record<string, ZodTypeAny>
116
117
  server.tool(name, desc, schema, async (args) => {
117
- const invocationId = `inv-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
118
- const taskId = typeof args.task_id === "string" ? args.task_id : undefined;
119
- const contract = findToolInvocationContractByName(name);
120
- // 无合同 hard fail
121
- if (!contract) {
118
+ // P0-B3: 工具调用超时保护 防止 MCP 客户端被永久阻塞
119
+ const timeoutMs = getToolTimeoutMs(name);
120
+ const timeoutToken = { timedOut: false };
121
+ const timeoutPromise = new Promise((_, reject) => {
122
+ setTimeout(() => {
123
+ timeoutToken.timedOut = true;
124
+ reject(new Error(`工具 ${name} 执行超时 (${timeoutMs}ms)`));
125
+ }, timeoutMs);
126
+ });
127
+ const handlerPromise = performToolCall(name, args, invocationIdSeed(), handler);
128
+ try {
129
+ return await Promise.race([handlerPromise, timeoutPromise]);
130
+ }
131
+ catch (err) {
132
+ // 超时或其他错误统一走错误返回
122
133
  return {
123
- content: [{ type: "text", text: JSON.stringify({ error: `工具 ${name} 未注册契约` }) }],
134
+ content: [{ type: "text", text: JSON.stringify({
135
+ error: errorMessage(err),
136
+ tool_name: name,
137
+ timed_out: timeoutToken.timedOut,
138
+ }) }],
124
139
  isError: true,
125
140
  };
126
141
  }
127
- const effectiveCategory = computeEffectiveCategory(name, args, contract);
128
- const effectiveSideEffects = computeEffectiveSideEffects(name, args, contract);
129
- // 加载 TaskContext
130
- let tCtx = null;
131
- if (taskId) {
132
- tCtx = await taskContext.load(taskId);
142
+ finally {
143
+ // 注意: setTimeout 已触发后无法 clear,但 handler 完成后 timeoutPromise 不再影响结果
133
144
  }
134
- // 状态预检: 在合同守卫之前验证任务状态
135
- if (taskId && STATE_PRECHECKS[name] && tCtx) {
136
- const validStates = STATE_PRECHECKS[name];
137
- if (!validStates.includes(tCtx.status)) {
138
- const label = STATE_ERROR_LABELS[name] ?? "状态不正确";
139
- // 写入 blocked trace + 清除旧 violations,防止旧 trace 数据残留
140
- // 注意:authorization/bypass 在此路径尚未计算,传 undefined
141
- const blockedTrace = createToolTrace({
142
- tool_name: name, invocation_id: invocationId, task_id: taskId,
143
- actual_side_effects: effectiveSideEffects,
144
- next_allowed_tools: contract.default_next_tools,
145
- forbidden_tools: contract.forbidden_next_tools,
146
- });
147
- try {
148
- await taskContext.setToolTrace(taskId, blockedTrace, []);
149
- }
150
- catch { /* best effort */ }
151
- return {
152
- content: [{ type: "text", text: JSON.stringify({
153
- error: `任务状态 ${tCtx.status} ${label},需要 ${validStates.join(" 或 ")}`,
154
- status: tCtx.status,
155
- }) }],
156
- isError: true,
157
- };
145
+ });
146
+ }
147
+ /** P0-B3: 工具超时配置(毫秒) */
148
+ const DEFAULT_TOOL_TIMEOUT_MS = 5 * 60 * 1000; // 5 分钟
149
+ const TOOL_TIMEOUTS_MS = {
150
+ sf_expand: 10 * 60 * 1000, // 10 分钟
151
+ sf_plan: 10 * 60 * 1000, // 10 分钟
152
+ sf_classify: 60 * 1000, // 1 分钟
153
+ sf_verify: 10 * 60 * 1000, // 10 分钟
154
+ sf_learn: 60 * 1000, // 1 分钟
155
+ sf_review: 10 * 60 * 1000, // 10 分钟
156
+ sf_status: 30 * 1000, // 30 秒
157
+ sf_next: 30 * 1000, // 30 秒
158
+ };
159
+ function getToolTimeoutMs(toolName) {
160
+ return TOOL_TIMEOUTS_MS[toolName] ?? DEFAULT_TOOL_TIMEOUT_MS;
161
+ }
162
+ function invocationIdSeed() {
163
+ return `inv-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
164
+ }
165
+ /** P0-B3: 实际工具执行逻辑(从 server.tool handler 抽出,便于超时包裹) */
166
+ async function performToolCall(name, args, invocationId, handler) {
167
+ const taskId = typeof args.task_id === "string" ? args.task_id : undefined;
168
+ const contract = findToolInvocationContractByName(name);
169
+ // 无合同 → hard fail
170
+ if (!contract) {
171
+ return {
172
+ content: [{ type: "text", text: JSON.stringify({ error: `工具 ${name} 未注册契约` }) }],
173
+ isError: true,
174
+ };
175
+ }
176
+ const effectiveCategory = computeEffectiveCategory(name, args, contract);
177
+ const effectiveSideEffects = computeEffectiveSideEffects(name, args, contract);
178
+ // 加载 TaskContext
179
+ let tCtx = null;
180
+ if (taskId) {
181
+ tCtx = await taskContext.load(taskId);
182
+ }
183
+ // 状态预检: 在合同守卫之前验证任务状态
184
+ if (taskId && STATE_PRECHECKS[name] && tCtx) {
185
+ const validStates = STATE_PRECHECKS[name];
186
+ if (!validStates.includes(tCtx.status)) {
187
+ const label = STATE_ERROR_LABELS[name] ?? "状态不正确";
188
+ // 写入 blocked trace + 清除旧 violations,防止旧 trace 数据残留
189
+ // 注意:authorization/bypass 在此路径尚未计算,传 undefined
190
+ const blockedTrace = createToolTrace({
191
+ tool_name: name, invocation_id: invocationId, task_id: taskId,
192
+ actual_side_effects: effectiveSideEffects,
193
+ next_allowed_tools: contract.default_next_tools,
194
+ forbidden_tools: contract.forbidden_next_tools,
195
+ });
196
+ try {
197
+ await taskContext.setToolTrace(taskId, blockedTrace, []);
198
+ }
199
+ catch (e) {
200
+ warn("中间件: setToolTrace 失败 (blocked)", errorMessage(e));
158
201
  }
159
- }
160
- // 严格受控但缺少 task_id
161
- const isStandaloneStrict = !contract.requires_workflow;
162
- if (contract.category === "strict_controlled" && !taskId && !isStandaloneStrict) {
163
202
  return {
164
- content: [{ type: "text", text: JSON.stringify({ error: `strict controlled 工具 ${name} 需要 task_id` }) }],
203
+ content: [{ type: "text", text: JSON.stringify({
204
+ error: `任务状态 ${tCtx.status} ${label},需要 ${validStates.join(" 或 ")}`,
205
+ status: tCtx.status,
206
+ }) }],
165
207
  isError: true,
166
208
  };
167
209
  }
168
- // 授权检查
169
- const authorization = checkAuth(name, args, contract, tCtx ?? undefined, effectiveCategory);
170
- // 授权拒绝 仅对网关级拒绝执行 hard fail
171
- // (动态写入升级、破坏性操作、无授权源的 strict_controlled
172
- // Handler 级授权(如 sf_capability_update confirm)由 handler 自行处理
173
- const gatewayDenial = !authorization.granted &&
174
- authorization.reason !== "tool requires explicit authorization";
175
- if (gatewayDenial) {
210
+ }
211
+ // 严格受控但缺少 task_id
212
+ const isStandaloneStrict = !contract.requires_workflow;
213
+ if (contract.category === "strict_controlled" && !taskId && !isStandaloneStrict) {
214
+ return {
215
+ content: [{ type: "text", text: JSON.stringify({ error: `strict controlled 工具 ${name} 需要 task_id` }) }],
216
+ isError: true,
217
+ };
218
+ }
219
+ // 授权检查
220
+ const authorization = checkAuth(name, args, contract, tCtx ?? undefined, effectiveCategory);
221
+ // 授权拒绝 → 仅对网关级拒绝执行 hard fail
222
+ // (动态写入升级、破坏性操作、无授权源的 strict_controlled)
223
+ // Handler 级授权(如 sf_capability_update confirm)由 handler 自行处理
224
+ const gatewayDenial = !authorization.granted &&
225
+ authorization.reason !== "tool requires explicit authorization";
226
+ if (gatewayDenial) {
227
+ const blockedTrace = createToolTrace({
228
+ tool_name: name, invocation_id: invocationId, task_id: taskId,
229
+ actual_side_effects: effectiveSideEffects,
230
+ next_allowed_tools: contract.default_next_tools,
231
+ forbidden_tools: contract.forbidden_next_tools,
232
+ authorization, bypass: undefined,
233
+ });
234
+ const authViolation = {
235
+ invocation_id: invocationId, tool_name: name,
236
+ violation_type: "missing_authorization", severity: "hard_fail",
237
+ reason: authorization.reason ?? "authorization denied",
238
+ recovery: "提供 confirm=true / authorized=true / authorization_token",
239
+ };
240
+ if (taskId && tCtx) {
241
+ await taskContext.setToolTrace(taskId, blockedTrace, [authViolation]);
242
+ }
243
+ return {
244
+ content: [{ type: "text", text: JSON.stringify({
245
+ error: authViolation.reason, violation: authViolation,
246
+ tool_trace: blockedTrace,
247
+ next_allowed_tools: contract.default_next_tools,
248
+ forbidden_tools: contract.forbidden_next_tools,
249
+ }) }],
250
+ isError: true,
251
+ };
252
+ }
253
+ // read_only_bypass 的旁路处理
254
+ const bypass = effectiveCategory === "read_only_bypass"
255
+ ? { allowed: true, reason: "read_only" } : undefined;
256
+ // 计划前置门: 在合同验证之前检查 — 写工具必须有 plan_proposal_gate
257
+ // sf_classify 创建任务、sf_expand 创建 plan_proposal_gate — 两者豁免
258
+ const hasWriteEffect = hasWriteSideEffect(effectiveSideEffects);
259
+ const planGateExempt = name === "sf_classify" || name === "sf_expand";
260
+ if (hasWriteEffect && taskId && !planGateExempt) {
261
+ const pgResult = checkWriteToolPlanGate({ ctx: tCtx, toolName: name, sideEffects: effectiveSideEffects });
262
+ if (!pgResult.allowed) {
263
+ const planViolation = {
264
+ invocation_id: invocationId, tool_name: name,
265
+ violation_type: "guard_blocked", severity: "hard_fail",
266
+ reason: pgResult.reason ?? "执行前缺少 Plan Proposal Gate",
267
+ recovery: "先调用 sf_classify → sf_expand → 设置 plan_proposal_gate 后再执行此工具",
268
+ };
176
269
  const blockedTrace = createToolTrace({
177
270
  tool_name: name, invocation_id: invocationId, task_id: taskId,
178
271
  actual_side_effects: effectiveSideEffects,
179
272
  next_allowed_tools: contract.default_next_tools,
180
273
  forbidden_tools: contract.forbidden_next_tools,
181
- authorization, bypass: undefined,
274
+ authorization, bypass,
182
275
  });
183
- const authViolation = {
184
- invocation_id: invocationId, tool_name: name,
185
- violation_type: "missing_authorization", severity: "hard_fail",
186
- reason: authorization.reason ?? "authorization denied",
187
- recovery: "提供 confirm=true / authorized=true / authorization_token",
188
- };
189
- if (taskId && tCtx) {
190
- await taskContext.setToolTrace(taskId, blockedTrace, [authViolation]);
191
- }
276
+ await taskContext.setToolTrace(taskId, blockedTrace, [planViolation]);
192
277
  return {
193
278
  content: [{ type: "text", text: JSON.stringify({
194
- error: authViolation.reason, violation: authViolation,
195
- tool_trace: blockedTrace,
196
- next_allowed_tools: contract.default_next_tools,
197
- forbidden_tools: contract.forbidden_next_tools,
279
+ error: planViolation.reason,
280
+ violation: planViolation,
281
+ diagnostic_code: pgResult.diagnostic_code ?? TOOL_DIAGNOSTIC_CODES.planGate,
282
+ recovery: planViolation.recovery,
198
283
  }) }],
199
284
  isError: true,
200
285
  };
201
286
  }
202
- // read_only_bypass 的旁路处理
203
- const bypass = effectiveCategory === "read_only_bypass"
204
- ? { allowed: true, reason: "read_only" } : undefined;
205
- // 计划前置门: 在合同验证之前检查 — 写工具必须有 plan_proposal_gate
206
- // sf_classify 创建任务、sf_expand 创建 plan_proposal_gate — 两者豁免
207
- const hasWriteEffect = hasWriteSideEffect(effectiveSideEffects);
208
- const planGateExempt = name === "sf_classify" || name === "sf_expand";
209
- if (hasWriteEffect && taskId && !planGateExempt) {
210
- const pgResult = checkWriteToolPlanGate({ ctx: tCtx, toolName: name, sideEffects: effectiveSideEffects });
211
- if (!pgResult.allowed) {
212
- const planViolation = {
213
- invocation_id: invocationId, tool_name: name,
214
- violation_type: "guard_blocked", severity: "hard_fail",
215
- reason: pgResult.reason ?? "执行前缺少 Plan Proposal Gate",
216
- recovery: "先调用 sf_classify → sf_expand → 设置 plan_proposal_gate 后再执行此工具",
217
- };
218
- const blockedTrace = createToolTrace({
219
- tool_name: name, invocation_id: invocationId, task_id: taskId,
220
- actual_side_effects: effectiveSideEffects,
221
- next_allowed_tools: contract.default_next_tools,
222
- forbidden_tools: contract.forbidden_next_tools,
223
- authorization, bypass,
224
- });
225
- await taskContext.setToolTrace(taskId, blockedTrace, [planViolation]);
226
- return {
227
- content: [{ type: "text", text: JSON.stringify({
228
- error: planViolation.reason,
229
- violation: planViolation,
230
- diagnostic_code: pgResult.diagnostic_code ?? TOOL_DIAGNOSTIC_CODES.planGate,
231
- recovery: planViolation.recovery,
232
- }) }],
233
- isError: true,
234
- };
235
- }
236
- const planGateCheck = taskContext.checkPlanGateBeforeWrite(tCtx);
237
- if (!planGateCheck.allowed) {
238
- const planViolation = {
239
- invocation_id: invocationId, tool_name: name,
240
- violation_type: "guard_blocked", severity: "hard_fail",
241
- reason: planGateCheck.reason_zh,
242
- recovery: "请先通过 PlanProposalFirstGate 生成并确认执行计划",
243
- };
244
- const blockedTrace = createToolTrace({
245
- tool_name: name, invocation_id: invocationId, task_id: taskId,
246
- actual_side_effects: effectiveSideEffects,
247
- next_allowed_tools: contract.default_next_tools,
248
- forbidden_tools: contract.forbidden_next_tools,
249
- authorization, bypass,
250
- });
251
- await taskContext.setToolTrace(taskId, blockedTrace, [planViolation]);
252
- return {
253
- content: [{ type: "text", text: JSON.stringify({
254
- error: planViolation.reason,
255
- violation: planViolation,
256
- diagnostic_code: TOOL_DIAGNOSTIC_CODES.planGate,
257
- recovery: planViolation.recovery,
258
- }) }],
259
- isError: true,
260
- };
261
- }
262
- }
263
- // 施工指令契约门: 写操作前检查 instruction_contract 状态
264
- if (hasWriteEffect && taskId) {
265
- const instrResult = await checkInstructionContractGate({ ctx: tCtx, toolName: name, sideEffects: effectiveSideEffects, task_id: taskId, taskContextMgr: taskContext });
266
- if (!instrResult.allowed) {
267
- const instrViolation = {
268
- invocation_id: invocationId, tool_name: name,
269
- violation_type: "guard_blocked", severity: "hard_fail",
270
- reason: instrResult.reason ?? "施工指令未就绪",
271
- recovery: "请先完善施工指令(目标、范围、非目标、落点、验收标准、禁止绕过项)",
272
- };
273
- const instrTrace = createToolTrace({
274
- tool_name: name, invocation_id: invocationId, task_id: taskId,
275
- actual_side_effects: effectiveSideEffects,
276
- next_allowed_tools: contract.default_next_tools,
277
- forbidden_tools: contract.forbidden_next_tools,
278
- authorization, bypass,
279
- });
280
- await taskContext.setToolTrace(taskId, instrTrace, [instrViolation]);
281
- return {
282
- content: [{ type: "text", text: JSON.stringify({
283
- error: instrViolation.reason,
284
- violation: instrViolation,
285
- diagnostic_code: instrResult.diagnostic_code ?? TOOL_DIAGNOSTIC_CODES.instructionContract,
286
- recovery: instrViolation.recovery,
287
- }) }],
288
- isError: true,
289
- };
290
- }
291
- }
292
- // 问题六十二: MCP 写入路径同样必须消费设计产物包状态,不能绕开 CLI hook。
293
- if (hasWriteEffect && taskId) {
294
- const designWriteGate = checkDesignArtifactWriteGate({ ctx: tCtx, toolName: name, sideEffects: effectiveSideEffects });
295
- if (!designWriteGate.allowed) {
296
- const designViolation = {
297
- invocation_id: invocationId, tool_name: name,
298
- violation_type: "guard_blocked", severity: "hard_fail",
299
- reason: designWriteGate.reason ?? "设计产物包未达到实现就绪状态",
300
- recovery: "仅可继续补充设计资产并执行 sf_verify 真实复验;通过前不得写入业务实现",
301
- };
302
- const designTrace = createToolTrace({
303
- tool_name: name, invocation_id: invocationId, task_id: taskId,
304
- actual_side_effects: effectiveSideEffects,
305
- next_allowed_tools: contract.default_next_tools,
306
- forbidden_tools: contract.forbidden_next_tools,
307
- authorization, bypass,
308
- });
309
- await taskContext.setToolTrace(taskId, designTrace, [designViolation]);
310
- return {
311
- content: [{ type: "text", text: JSON.stringify({
312
- error: designViolation.reason,
313
- violation: designViolation,
314
- diagnostic_code: designWriteGate.diagnostic_code,
315
- recovery: designViolation.recovery,
316
- }) }],
317
- isError: true,
318
- };
319
- }
320
- }
321
- // 用户项目知识门禁: review/verify/deliver 不能绕过不可消费的项目 hard rule。
322
- if (["sf_review", "sf_verify", "sf_deliver"].includes(name)) {
323
- const { evaluateLifecycleKnowledgeDecision } = await import("../../engine/contracts/lifecycle_knowledge_contract.js");
324
- const lifecycleStage = name === "sf_review" ? "implementation" : name === "sf_verify" ? "testing" : "delivery";
325
- const lifecycleDecision = evaluateLifecycleKnowledgeDecision({
326
- projectPath,
327
- consumer: name,
328
- intent: tCtx?.intent ?? name,
329
- route: name === "sf_review" ? "review" : name === "sf_verify" ? "verification" : "delivery",
330
- lifecycle_stage: lifecycleStage,
331
- changed_files: Array.isArray(args.changed_files) ? args.changed_files : tCtx?.execution?.changed_files ?? [],
332
- verified_files: Array.isArray(args.changed_files) ? args.changed_files : tCtx?.execution?.changed_files ?? [],
333
- traceability_ids: [
334
- ...(tCtx?.traceability_binding?.requirement_ids ?? []),
335
- ...(tCtx?.traceability_binding?.prototype_ids ?? []),
336
- ...(tCtx?.traceability_binding?.architecture_ids ?? []),
337
- ...(tCtx?.traceability_binding?.detail_design_ids ?? []),
338
- ...(tCtx?.traceability_binding?.phase_ids ?? []),
339
- ...(tCtx?.traceability_binding?.slice_ids ?? []),
340
- ...(tCtx?.traceability_binding?.acceptance_ids ?? []),
341
- ],
342
- config,
343
- task_id: taskId,
344
- require_design_audit: false,
345
- enforce_control_plane: name !== "sf_review",
346
- generation_traces: [
347
- ...(tCtx?.classification ? [{ tool_name: "sf_classify", task_id: taskId, status: "passed", evidence_kind: "result" }] : []),
348
- ...(tCtx?.expansion ? [{ tool_name: "sf_expand", task_id: taskId, status: "passed", evidence_kind: "result" }] : []),
349
- ...(name === "sf_verify" ? [{ tool_name: "sf_verify", task_id: taskId, status: "blocked", evidence_kind: "plan" }] : []),
350
- ],
351
- selection_limit: 8,
352
- });
353
- if (lifecycleDecision.hard_fail_count > 0) {
354
- const hardFindings = lifecycleDecision.findings
355
- .filter((finding) => finding.severity === "hard_fail")
356
- .map((finding) => `[${finding.code}] ${finding.message_zh}`);
357
- const pkViolation = {
358
- invocation_id: invocationId,
359
- tool_name: name,
360
- violation_type: "guard_blocked",
361
- severity: "hard_fail",
362
- reason: lifecycleDecision.decision_summary_zh,
363
- recovery: lifecycleDecision.recovery_commands.join(";") || "运行 soloforge next 查看统一生命周期知识合同阻断项",
364
- };
365
- const pkTrace = createToolTrace({
366
- tool_name: name, invocation_id: invocationId, task_id: taskId,
367
- actual_side_effects: effectiveSideEffects,
368
- next_allowed_tools: ["sf_expand", "sf_status"],
369
- forbidden_tools: ["sf_review", "sf_verify", "sf_deliver"],
370
- authorization, bypass,
371
- });
372
- if (taskId && tCtx)
373
- await taskContext.setToolTrace(taskId, pkTrace, [pkViolation]);
374
- return {
375
- content: [{ type: "text", text: JSON.stringify({
376
- error: pkViolation.reason,
377
- violation: pkViolation,
378
- diagnostic_code: TOOL_DIAGNOSTIC_CODES.projectKnowledgeBlocked,
379
- findings: hardFindings,
380
- lifecycle_knowledge_decision: {
381
- contract_id: lifecycleDecision.contract_id,
382
- lifecycle_stage: lifecycleDecision.lifecycle_stage,
383
- authoritative_paths: lifecycleDecision.authoritative_paths,
384
- recovery_commands: lifecycleDecision.recovery_commands,
385
- },
386
- recovery: pkViolation.recovery,
387
- }) }],
388
- isError: true,
389
- };
390
- }
391
- }
392
- // 合同验证
393
- const lastTrace = tCtx?.last_tool_trace;
394
- // 为动态工具构建有效的合同覆盖(如 sf_status cancel)
395
- let contractOverride;
396
- if (effectiveCategory !== contract.category || effectiveSideEffects !== contract.side_effects) {
397
- contractOverride = { ...contract, category: effectiveCategory, side_effects: effectiveSideEffects };
398
- }
399
- // Workflow ID: 仅来自真实的 expansion trace
400
- const explicitWorkflowId = tCtx?.expansion?.workflow_trace?.workflow_id;
401
- const violations = validateToolInvocation({
402
- tool_name: name,
403
- current_next_allowed: lastTrace?.next_allowed_tools ?? [],
404
- current_forbidden: lastTrace?.forbidden_tools ?? [],
405
- authorization,
406
- bypass,
407
- actual_side_effects: effectiveSideEffects,
408
- workflow_id: explicitWorkflowId,
409
- _contractOverride: contractOverride,
410
- });
411
- // 基于状态的回退: 将 contract_state_mismatch 从 hard_fail 降级为 require_human
412
- if (authorization.reason === "task in valid state for tool") {
413
- for (const v of violations) {
414
- if (v.violation_type === "contract_state_mismatch" && v.severity === "hard_fail") {
415
- v.severity = "require_human";
416
- v.reason += " (state-based fallback: no real workflow_id)";
417
- }
418
- }
419
- }
420
- // Hard-fail 违规 → 阻止
421
- const hardFail = violations.find(v => v.severity === "hard_fail");
422
- if (hardFail) {
287
+ const planGateCheck = taskContext.checkPlanGateBeforeWrite(tCtx);
288
+ if (!planGateCheck.allowed) {
289
+ const planViolation = {
290
+ invocation_id: invocationId, tool_name: name,
291
+ violation_type: "guard_blocked", severity: "hard_fail",
292
+ reason: planGateCheck.reason_zh,
293
+ recovery: "请先通过 PlanProposalFirstGate 生成并确认执行计划",
294
+ };
423
295
  const blockedTrace = createToolTrace({
424
296
  tool_name: name, invocation_id: invocationId, task_id: taskId,
425
297
  actual_side_effects: effectiveSideEffects,
@@ -427,124 +299,313 @@ export function createToolRegistrar(ctx) {
427
299
  forbidden_tools: contract.forbidden_next_tools,
428
300
  authorization, bypass,
429
301
  });
430
- if (taskId && tCtx) {
431
- await taskContext.setToolTrace(taskId, blockedTrace, violations);
432
- }
302
+ await taskContext.setToolTrace(taskId, blockedTrace, [planViolation]);
433
303
  return {
434
304
  content: [{ type: "text", text: JSON.stringify({
435
- error: hardFail.reason, violation: hardFail,
436
- tool_trace: blockedTrace,
437
- next_allowed_tools: contract.default_next_tools,
438
- forbidden_tools: contract.forbidden_next_tools,
305
+ error: planViolation.reason,
306
+ violation: planViolation,
307
+ diagnostic_code: TOOL_DIAGNOSTIC_CODES.planGate,
308
+ recovery: planViolation.recovery,
439
309
  }) }],
440
310
  isError: true,
441
311
  };
442
312
  }
443
- // 执行 handler
444
- try {
445
- const raw = await handler(args);
446
- // 如果 handler 直接返回 { content } 则透传,但补充 trace 写入
447
- if (raw && typeof raw === "object" && "content" in raw && Array.isArray(raw.content)) {
448
- // [ENG-6] content 格式路径也需要写入 trace
449
- if (taskId) {
450
- const passthroughTrace = createToolTrace({
451
- tool_name: name, invocation_id: invocationId, task_id: taskId,
452
- workflow_id: tCtx?.expansion?.workflow_trace?.workflow_id,
453
- actual_side_effects: effectiveSideEffects,
454
- next_allowed_tools: contract.default_next_tools,
455
- forbidden_tools: contract.forbidden_next_tools,
456
- authorization, bypass,
457
- });
458
- try {
459
- await taskContext.setToolTrace(taskId, passthroughTrace, violations);
460
- }
461
- catch { /* best effort */ }
462
- }
463
- return raw;
464
- }
465
- // 从 { result } 模式中提取数据
466
- const rawRec = raw;
467
- const data = rawRec?.result !== undefined ? rawRec.result : rawRec;
468
- const hasError = !!(data && typeof data === "object" && "error" in data);
469
- const dataRec = data;
470
- // 门禁阻断后通用重试机制:任何工具在 error 状态下都应可重试自身。
471
- // handler 显式提供的 recovery 值优先;未提供时自动将自身加入 next_allowed 并移出 forbidden。
472
- // 覆盖 sf_expand/sf_deliver/sf_scaffold 及未来所有 forbidden_self 工具。
473
- const recoveryNextTools = hasError && Array.isArray(dataRec.recovery_next_tools)
474
- ? dataRec.recovery_next_tools
475
- : hasError
476
- ? [...contract.default_next_tools, name]
477
- : contract.default_next_tools;
478
- const recoveryForbiddenTools = hasError && Array.isArray(dataRec.recovery_forbidden_tools)
479
- ? dataRec.recovery_forbidden_tools
480
- : hasError
481
- ? contract.forbidden_next_tools.filter((t) => t !== name)
482
- : contract.forbidden_next_tools;
483
- // 构建 trace;失败处理器可显式开放修复重验路径
484
- const trace = createToolTrace({
313
+ }
314
+ // 施工指令契约门: 写操作前检查 instruction_contract 状态
315
+ if (hasWriteEffect && taskId) {
316
+ const instrResult = await checkInstructionContractGate({ ctx: tCtx, toolName: name, sideEffects: effectiveSideEffects, task_id: taskId, taskContextMgr: taskContext });
317
+ if (!instrResult.allowed) {
318
+ const instrViolation = {
319
+ invocation_id: invocationId, tool_name: name,
320
+ violation_type: "guard_blocked", severity: "hard_fail",
321
+ reason: instrResult.reason ?? "施工指令未就绪",
322
+ recovery: "请先完善施工指令(目标、范围、非目标、落点、验收标准、禁止绕过项)",
323
+ };
324
+ const instrTrace = createToolTrace({
485
325
  tool_name: name, invocation_id: invocationId, task_id: taskId,
486
- workflow_id: tCtx?.expansion?.workflow_trace?.workflow_id,
487
326
  actual_side_effects: effectiveSideEffects,
488
- next_allowed_tools: recoveryNextTools,
489
- forbidden_tools: recoveryForbiddenTools,
327
+ next_allowed_tools: contract.default_next_tools,
328
+ forbidden_tools: contract.forbidden_next_tools,
490
329
  authorization, bypass,
491
330
  });
492
- // trace 写入 TaskContext
493
- if (taskId) {
494
- await taskContext.setToolTrace(taskId, trace, violations);
495
- }
496
- // 用 trace + next/forbidden 装饰响应
497
- const response = {
498
- ...data,
499
- tool_trace: trace,
500
- next_allowed_tools: recoveryNextTools,
501
- forbidden_tools: recoveryForbiddenTools,
331
+ await taskContext.setToolTrace(taskId, instrTrace, [instrViolation]);
332
+ return {
333
+ content: [{ type: "text", text: JSON.stringify({
334
+ error: instrViolation.reason,
335
+ violation: instrViolation,
336
+ diagnostic_code: instrResult.diagnostic_code ?? TOOL_DIAGNOSTIC_CODES.instructionContract,
337
+ recovery: instrViolation.recovery,
338
+ }) }],
339
+ isError: true,
502
340
  };
503
- // 基于状态的回退: 在响应中暴露降级 workflow
504
- if (authorization.reason === "task in valid state for tool") {
505
- response.degraded_workflow = true;
506
- response.workflow_source = "state-based-fallback";
507
- }
341
+ }
342
+ }
343
+ // 问题六十二: MCP 写入路径同样必须消费设计产物包状态,不能绕开 CLI hook。
344
+ if (hasWriteEffect && taskId) {
345
+ const designWriteGate = checkDesignArtifactWriteGate({ ctx: tCtx, toolName: name, sideEffects: effectiveSideEffects });
346
+ if (!designWriteGate.allowed) {
347
+ const designViolation = {
348
+ invocation_id: invocationId, tool_name: name,
349
+ violation_type: "guard_blocked", severity: "hard_fail",
350
+ reason: designWriteGate.reason ?? "设计产物包未达到实现就绪状态",
351
+ recovery: "仅可继续补充设计资产并执行 sf_verify 真实复验;通过前不得写入业务实现",
352
+ };
353
+ const designTrace = createToolTrace({
354
+ tool_name: name, invocation_id: invocationId, task_id: taskId,
355
+ actual_side_effects: effectiveSideEffects,
356
+ next_allowed_tools: contract.default_next_tools,
357
+ forbidden_tools: contract.forbidden_next_tools,
358
+ authorization, bypass,
359
+ });
360
+ await taskContext.setToolTrace(taskId, designTrace, [designViolation]);
508
361
  return {
509
- content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
510
- isError: hasError ? true : undefined,
362
+ content: [{ type: "text", text: JSON.stringify({
363
+ error: designViolation.reason,
364
+ violation: designViolation,
365
+ diagnostic_code: designWriteGate.diagnostic_code,
366
+ recovery: designViolation.recovery,
367
+ }) }],
368
+ isError: true,
511
369
  };
512
370
  }
513
- catch (err) {
514
- // 通用重试原则:异常路径同样允许工具重试自身(与 handler error 路径一致)
515
- const errorNextTools = [...contract.default_next_tools, name];
516
- const errorForbiddenTools = contract.forbidden_next_tools.filter((t) => t !== name);
517
- if (name === "sf_expand" && taskId) {
518
- // [ENG-7] 记录失败信息到 trace 再标记 failed,不跳过失败分析流程
519
- try {
520
- await taskContext.updateStatus(taskId, "failed");
521
- }
522
- catch { /* best effort */ }
523
- }
524
- const errorTrace = createToolTrace({
371
+ }
372
+ // 用户项目知识门禁: review/verify/deliver 不能绕过不可消费的项目 hard rule。
373
+ if (["sf_review", "sf_verify", "sf_deliver"].includes(name)) {
374
+ const { evaluateLifecycleKnowledgeDecision } = await import("../../engine/contracts/lifecycle_knowledge_contract.js");
375
+ const lifecycleStage = name === "sf_review" ? "implementation" : name === "sf_verify" ? "testing" : "delivery";
376
+ const lifecycleDecision = evaluateLifecycleKnowledgeDecision({
377
+ projectPath,
378
+ consumer: name,
379
+ intent: tCtx?.intent ?? name,
380
+ route: name === "sf_review" ? "review" : name === "sf_verify" ? "verification" : "delivery",
381
+ lifecycle_stage: lifecycleStage,
382
+ changed_files: Array.isArray(args.changed_files) ? args.changed_files : tCtx?.execution?.changed_files ?? [],
383
+ verified_files: Array.isArray(args.changed_files) ? args.changed_files : tCtx?.execution?.changed_files ?? [],
384
+ traceability_ids: [
385
+ ...(tCtx?.traceability_binding?.requirement_ids ?? []),
386
+ ...(tCtx?.traceability_binding?.prototype_ids ?? []),
387
+ ...(tCtx?.traceability_binding?.architecture_ids ?? []),
388
+ ...(tCtx?.traceability_binding?.detail_design_ids ?? []),
389
+ ...(tCtx?.traceability_binding?.phase_ids ?? []),
390
+ ...(tCtx?.traceability_binding?.slice_ids ?? []),
391
+ ...(tCtx?.traceability_binding?.acceptance_ids ?? []),
392
+ ],
393
+ config,
394
+ task_id: taskId,
395
+ require_design_audit: false,
396
+ enforce_control_plane: name !== "sf_review",
397
+ generation_traces: [
398
+ ...(tCtx?.classification ? [{ tool_name: "sf_classify", task_id: taskId, status: "passed", evidence_kind: "result" }] : []),
399
+ ...(tCtx?.expansion ? [{ tool_name: "sf_expand", task_id: taskId, status: "passed", evidence_kind: "result" }] : []),
400
+ ...(name === "sf_verify" ? [{ tool_name: "sf_verify", task_id: taskId, status: "blocked", evidence_kind: "plan" }] : []),
401
+ ],
402
+ selection_limit: 8,
403
+ });
404
+ if (lifecycleDecision.hard_fail_count > 0) {
405
+ const hardFindings = lifecycleDecision.findings
406
+ .filter((finding) => finding.severity === "hard_fail")
407
+ .map((finding) => `[${finding.code}] ${finding.message_zh}`);
408
+ const pkViolation = {
409
+ invocation_id: invocationId,
410
+ tool_name: name,
411
+ violation_type: "guard_blocked",
412
+ severity: "hard_fail",
413
+ reason: lifecycleDecision.decision_summary_zh,
414
+ recovery: lifecycleDecision.recovery_commands.join(";") || "运行 soloforge next 查看统一生命周期知识合同阻断项",
415
+ };
416
+ const pkTrace = createToolTrace({
525
417
  tool_name: name, invocation_id: invocationId, task_id: taskId,
526
418
  actual_side_effects: effectiveSideEffects,
527
- next_allowed_tools: errorNextTools,
528
- forbidden_tools: errorForbiddenTools,
419
+ next_allowed_tools: ["sf_expand", "sf_status"],
420
+ forbidden_tools: ["sf_review", "sf_verify", "sf_deliver"],
529
421
  authorization, bypass,
530
422
  });
531
- if (taskId) {
532
- try {
533
- await taskContext.setToolTrace(taskId, errorTrace, []);
534
- }
535
- catch { /* best effort */ }
536
- }
423
+ if (taskId && tCtx)
424
+ await taskContext.setToolTrace(taskId, pkTrace, [pkViolation]);
537
425
  return {
538
426
  content: [{ type: "text", text: JSON.stringify({
539
- error: errorMessage(err),
540
- tool_trace: errorTrace,
541
- next_allowed_tools: errorNextTools,
542
- forbidden_tools: errorForbiddenTools,
427
+ error: pkViolation.reason,
428
+ violation: pkViolation,
429
+ diagnostic_code: TOOL_DIAGNOSTIC_CODES.projectKnowledgeBlocked,
430
+ findings: hardFindings,
431
+ lifecycle_knowledge_decision: {
432
+ contract_id: lifecycleDecision.contract_id,
433
+ lifecycle_stage: lifecycleDecision.lifecycle_stage,
434
+ authoritative_paths: lifecycleDecision.authoritative_paths,
435
+ recovery_commands: lifecycleDecision.recovery_commands,
436
+ },
437
+ recovery: pkViolation.recovery,
543
438
  }) }],
544
439
  isError: true,
545
440
  };
546
441
  }
442
+ }
443
+ // 合同验证
444
+ const lastTrace = tCtx?.last_tool_trace;
445
+ // 为动态工具构建有效的合同覆盖(如 sf_status cancel)
446
+ let contractOverride;
447
+ if (effectiveCategory !== contract.category || effectiveSideEffects !== contract.side_effects) {
448
+ contractOverride = { ...contract, category: effectiveCategory, side_effects: effectiveSideEffects };
449
+ }
450
+ // Workflow ID: 仅来自真实的 expansion trace
451
+ const explicitWorkflowId = tCtx?.expansion?.workflow_trace?.workflow_id;
452
+ const violations = validateToolInvocation({
453
+ tool_name: name,
454
+ current_next_allowed: lastTrace?.next_allowed_tools ?? [],
455
+ current_forbidden: lastTrace?.forbidden_tools ?? [],
456
+ authorization,
457
+ bypass,
458
+ actual_side_effects: effectiveSideEffects,
459
+ workflow_id: explicitWorkflowId,
460
+ _contractOverride: contractOverride,
547
461
  });
462
+ // 基于状态的回退: 将 contract_state_mismatch 从 hard_fail 降级为 require_human
463
+ if (authorization.reason === "task in valid state for tool") {
464
+ for (const v of violations) {
465
+ if (v.violation_type === "contract_state_mismatch" && v.severity === "hard_fail") {
466
+ v.severity = "require_human";
467
+ v.reason += " (state-based fallback: no real workflow_id)";
468
+ }
469
+ }
470
+ }
471
+ // Hard-fail 违规 → 阻止
472
+ const hardFail = violations.find(v => v.severity === "hard_fail");
473
+ if (hardFail) {
474
+ const blockedTrace = createToolTrace({
475
+ tool_name: name, invocation_id: invocationId, task_id: taskId,
476
+ actual_side_effects: effectiveSideEffects,
477
+ next_allowed_tools: contract.default_next_tools,
478
+ forbidden_tools: contract.forbidden_next_tools,
479
+ authorization, bypass,
480
+ });
481
+ if (taskId && tCtx) {
482
+ await taskContext.setToolTrace(taskId, blockedTrace, violations);
483
+ }
484
+ return {
485
+ content: [{ type: "text", text: JSON.stringify({
486
+ error: hardFail.reason, violation: hardFail,
487
+ tool_trace: blockedTrace,
488
+ next_allowed_tools: contract.default_next_tools,
489
+ forbidden_tools: contract.forbidden_next_tools,
490
+ }) }],
491
+ isError: true,
492
+ };
493
+ }
494
+ // 执行 handler
495
+ try {
496
+ const raw = await handler(args);
497
+ // 如果 handler 直接返回 { content } 则透传,但补充 trace 写入
498
+ if (raw && typeof raw === "object" && "content" in raw && Array.isArray(raw.content)) {
499
+ // [ENG-6] content 格式路径也需要写入 trace
500
+ if (taskId) {
501
+ const passthroughTrace = createToolTrace({
502
+ tool_name: name, invocation_id: invocationId, task_id: taskId,
503
+ workflow_id: tCtx?.expansion?.workflow_trace?.workflow_id,
504
+ actual_side_effects: effectiveSideEffects,
505
+ next_allowed_tools: contract.default_next_tools,
506
+ forbidden_tools: contract.forbidden_next_tools,
507
+ authorization, bypass,
508
+ });
509
+ try {
510
+ await taskContext.setToolTrace(taskId, passthroughTrace, violations);
511
+ }
512
+ catch (e) {
513
+ warn("中间件: setToolTrace 失败 (passthrough)", errorMessage(e));
514
+ }
515
+ }
516
+ return raw;
517
+ }
518
+ // 从 { result } 模式中提取数据
519
+ const rawRec = raw;
520
+ const data = rawRec?.result !== undefined ? rawRec.result : rawRec;
521
+ const hasError = !!(data && typeof data === "object" && "error" in data);
522
+ const dataRec = data;
523
+ // 门禁阻断后通用重试机制:任何工具在 error 状态下都应可重试自身。
524
+ // handler 显式提供的 recovery 值优先;未提供时自动将自身加入 next_allowed 并移出 forbidden。
525
+ // 覆盖 sf_expand/sf_deliver/sf_scaffold 及未来所有 forbidden_self 工具。
526
+ const recoveryNextTools = hasError && Array.isArray(dataRec.recovery_next_tools)
527
+ ? dataRec.recovery_next_tools
528
+ : hasError
529
+ ? [...contract.default_next_tools, name]
530
+ : contract.default_next_tools;
531
+ const recoveryForbiddenTools = hasError && Array.isArray(dataRec.recovery_forbidden_tools)
532
+ ? dataRec.recovery_forbidden_tools
533
+ : hasError
534
+ ? contract.forbidden_next_tools.filter((t) => t !== name)
535
+ : contract.forbidden_next_tools;
536
+ // 构建 trace;失败处理器可显式开放修复重验路径
537
+ const trace = createToolTrace({
538
+ tool_name: name, invocation_id: invocationId, task_id: taskId,
539
+ workflow_id: tCtx?.expansion?.workflow_trace?.workflow_id,
540
+ actual_side_effects: effectiveSideEffects,
541
+ next_allowed_tools: recoveryNextTools,
542
+ forbidden_tools: recoveryForbiddenTools,
543
+ authorization, bypass,
544
+ });
545
+ // 将 trace 写入 TaskContext
546
+ if (taskId) {
547
+ await taskContext.setToolTrace(taskId, trace, violations);
548
+ }
549
+ // 用 trace + next/forbidden 装饰响应
550
+ const response = {
551
+ ...data,
552
+ tool_trace: trace,
553
+ next_allowed_tools: recoveryNextTools,
554
+ forbidden_tools: recoveryForbiddenTools,
555
+ };
556
+ // 基于状态的回退: 在响应中暴露降级 workflow
557
+ if (authorization.reason === "task in valid state for tool") {
558
+ response.degraded_workflow = true;
559
+ response.workflow_source = "state-based-fallback";
560
+ }
561
+ return {
562
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
563
+ isError: hasError ? true : undefined,
564
+ };
565
+ }
566
+ catch (err) {
567
+ // 通用重试原则:异常路径同样允许工具重试自身(与 handler error 路径一致)
568
+ const errorNextTools = [...contract.default_next_tools, name];
569
+ const errorForbiddenTools = contract.forbidden_next_tools.filter((t) => t !== name);
570
+ // P0-C1: 所有状态转换工具失败时统一标记 failed,不仅 sf_expand
571
+ // 防止 sf_classify/sf_plan/sf_verify/sf_learn 异常后任务卡 executing 30 分钟
572
+ const STATE_TRANSITION_TOOLS = new Set([
573
+ "sf_classify", "sf_plan", "sf_expand", "sf_verify", "sf_learn",
574
+ ]);
575
+ if (STATE_TRANSITION_TOOLS.has(name) && taskId) {
576
+ // [ENG-7] 记录失败信息到 trace 再标记 failed,不跳过失败分析流程
577
+ try {
578
+ await taskContext.updateStatus(taskId, "failed");
579
+ }
580
+ catch (e) {
581
+ warn("中间件: 标记任务 failed 失败", errorMessage(e));
582
+ }
583
+ }
584
+ const errorTrace = createToolTrace({
585
+ tool_name: name, invocation_id: invocationId, task_id: taskId,
586
+ actual_side_effects: effectiveSideEffects,
587
+ next_allowed_tools: errorNextTools,
588
+ forbidden_tools: errorForbiddenTools,
589
+ authorization, bypass,
590
+ });
591
+ if (taskId) {
592
+ try {
593
+ await taskContext.setToolTrace(taskId, errorTrace, []);
594
+ }
595
+ catch (e) {
596
+ warn("中间件: setToolTrace 失败 (error)", errorMessage(e));
597
+ }
598
+ }
599
+ return {
600
+ content: [{ type: "text", text: JSON.stringify({
601
+ error: errorMessage(err),
602
+ tool_trace: errorTrace,
603
+ next_allowed_tools: errorNextTools,
604
+ forbidden_tools: errorForbiddenTools,
605
+ }) }],
606
+ isError: true,
607
+ };
608
+ }
548
609
  }
549
610
  // ── 加载认知锚点上下文 ──
550
611
  // 加载认知锚点上下文 — 必须提供相关性参数,禁止全量加载
@@ -568,7 +629,9 @@ export function createToolRegistrar(ctx) {
568
629
  }
569
630
  }
570
631
  }
571
- catch { /* ignore */ }
632
+ catch (e) {
633
+ warn("中间件: 认知锚点文件存在性检查失败", errorMessage(e));
634
+ }
572
635
  const checked = (await lazyAnchor()).checkAnchorStaleness(mapData.anchors, existingFiles);
573
636
  // 必须有 module 或 file_paths,否则不返回任何锚点(禁止全量加载)
574
637
  if (!query.module && (!query.file_paths || query.file_paths.length === 0))