odai-dsh-agent 0.0.2 → 0.0.4

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 (39) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +86 -86
  3. package/bin/odai-dsh-agent.mjs +106 -87
  4. package/package.json +68 -68
  5. package/preset/odai/agent.cordis.yml +260 -260
  6. package/preset/odai/preset.yml +3 -3
  7. package/preset/odai/runtime/governance.mjs +120 -120
  8. package/preset/odai/runtime/index.mjs +587 -584
  9. package/preset/odai/runtime/router.mjs +290 -290
  10. package/preset/odai/runtime/routing-config.mjs +323 -322
  11. package/preset/odai/runtime/session-compat.mjs +387 -0
  12. package/preset/odai/runtime/session-evidence.mjs +344 -0
  13. package/preset/odai/skills/odai/SKILL.md +65 -65
  14. package/preset/odai/skills/odai/agents/openai.yaml +4 -4
  15. package/preset/odai/skills/odai/assets/claude-agents/agent.md +11 -11
  16. package/preset/odai/skills/odai/assets/codex-agents/config.toml +11 -11
  17. package/preset/odai/skills/odai/assets/codex-agents/role.toml +5 -5
  18. package/preset/odai/skills/odai/assets/copilot-agents/agent.md +13 -13
  19. package/preset/odai/skills/odai/assets/hooks-policy.example.json +24 -24
  20. package/preset/odai/skills/odai/assets/routing-roles/controller.md +15 -15
  21. package/preset/odai/skills/odai/assets/routing-roles/executor.md +11 -11
  22. package/preset/odai/skills/odai/assets/routing-roles/planner.md +29 -29
  23. package/preset/odai/skills/odai/assets/routing-roles/reviewer.md +11 -11
  24. package/preset/odai/skills/odai/assets/task-state.md +12 -12
  25. package/preset/odai/skills/odai/references/craft.md +53 -53
  26. package/preset/odai/skills/odai/references/dao.md +30 -30
  27. package/preset/odai/skills/odai/references/leverage.md +45 -45
  28. package/preset/odai/skills/odai/references/support.md +36 -36
  29. package/preset/odai/skills/odai/references/verification.md +23 -23
  30. package/preset/odai/skills/odai/scripts/build-hooks.mjs +273 -273
  31. package/preset/odai/skills/odai/scripts/build-routing.mjs +239 -239
  32. package/preset/odai/skills/odai/scripts/install-routing.mjs +375 -375
  33. package/preset/odai/skills/odai/scripts/odai-hook.mjs +457 -457
  34. package/preset/odai/skills/odai/scripts/run-role.mjs +336 -336
  35. package/preset/odai/skills/odai/scripts/run-routing.mjs +1154 -1154
  36. package/preset/odai/skills/odai/scripts/verify-routing.mjs +103 -103
  37. package/scripts/verify-agent-load.mjs +246 -217
  38. package/src/dsh-version.mjs +20 -0
  39. package/src/installer.mjs +323 -270
@@ -1,290 +1,290 @@
1
- export const HIGH_IMPACT_PLANNER_REASON = "PLANNER_UNVERIFIED_HIGH_IMPACT_CHANGE";
2
-
3
- const RESPONSIBILITY_LABELS = Object.freeze({
4
- planner: "规划",
5
- executor: "执行",
6
- reviewer: "验收",
7
- });
8
-
9
- const PLANNER_PATTERNS = [
10
- /独立(?:判断|规划|分析|决定|决策)/iu,
11
- /比较(?:一下)?(?:方案|路线|架构)/iu,
12
- /架构选型/iu,
13
- /先(?:判断|规划)(?:路线|方案)/iu,
14
- /second opinion (?:on|for) (?:the )?(?:plan|approach|architecture)/iu,
15
- /compare (?:the )?(?:approaches|options|architectures)/iu,
16
- /architecture decision/iu,
17
- /(?:independently decide|independent decision)/iu,
18
- ];
19
-
20
- const REVIEWER_PATTERNS = [
21
- /独立(?:审查|复核|评审)/iu,
22
- /(?:代码|安全|变更)(?:审查|复核|评审)/iu,
23
- /code review/iu,
24
- /security review/iu,
25
- /independent review/iu,
26
- /challenge (?:the )?(?:plan|implementation|result)/iu,
27
- ];
28
-
29
- const RISK_PATTERNS = [
30
- /(?:高风险|安全|权限|删除|迁移|发布|生产环境)/iu,
31
- /(?:支付|付款|结算|扣款|退款|订单)[^。!?\n]{0,60}(?:提供方|服务|接口|请求|结果|状态|超时|重试|金额|交易|配置|回调|不稳定)/iu,
32
- /(?:high[- ]risk|security|permission|delete|migration|production)/iu,
33
- /(?:checkout|payment|billing|charge|refund)[^.!?\n]{0,80}(?:provider|service|request|result|status|timeout|retry|transaction|configuration|unstable|fail|超时|重试|不稳定)/iu,
34
- ];
35
-
36
- const UNVERIFIED_CAUSAL_PATTERNS = [
37
- /(?:我看|看起来|看来|估计|大概|八成|明显|肯定)(?:就)?是/iu,
38
- /(?:问题|原因|根因)(?:应该|可能|大概|估计)(?:就)?在/iu,
39
- /\b(?:it\s+)?(?:looks?|seems)\s+like\b/iu,
40
- /\b(?:i think|probably|apparently|clearly|obviously)\b[^.!?\n]{0,80}\b(?:is|are|because|caused by|comes from)\b/iu,
41
- ];
42
-
43
- const CONCRETE_CHANGE_PATTERNS = [
44
- /把[^。!?\n]{1,120}(?:降到|提到|提高到|改成|设为|设置为|切到|换成|开启|关闭|删除|清空|迁移到|发布到|上线)/iu,
45
- /(?:降低|提高|增加|减少|修改|设置|开启|关闭|删除|清空|迁移|发布|上线)[^。!?\n]{0,80}(?:\d|配置|超时|重试|权限|数据|服务)/iu,
46
- /\b(?:set|change|reduce|lower|increase|raise|disable|enable|delete|drop|clear|migrate|deploy|release)\b[^.!?\n]{0,100}\b(?:to|from|timeout|retries|permission|data|service|production)\b/iu,
47
- ];
48
-
49
- const SPECIFIC_PARAMETER_PATTERNS = [
50
- /\d+(?:\.\d+)?\s*(?:毫秒|秒|分钟|小时|次|%|%|ms|sec(?:ond)?s?|min(?:ute)?s?|hours?|retries?|mb|gb)/iu,
51
- ];
52
-
53
- const URGENCY_PATTERNS = [
54
- /(?:先止血|立即|马上|赶紧|紧急|热修)/iu,
55
- /\b(?:hotfix|immediately|right away|stop the bleeding)\b/iu,
56
- ];
57
-
58
- const IRREVERSIBLE_ACTION_PATTERNS = [
59
- /(?:删除|清空|迁移|发布|上线|关闭)/iu,
60
- /\b(?:delete|drop|clear|migrate|deploy|release|disable)\b/iu,
61
- ];
62
-
63
- function matchesAny(text, patterns) {
64
- return patterns.some((pattern) => pattern.test(text));
65
- }
66
-
67
- function stripQuotedMaterial(text) {
68
- return text
69
- .replace(/```[\s\S]*?```/gu, " ")
70
- .replace(/^\s*>.*$/gmu, " ")
71
- .replace(/`[^`\n]*`/gu, " ")
72
- .replace(/“[^”\n]*”|‘[^’\n]*’|「[^」\n]*」|『[^』\n]*』|《[^》\n]*》|"[^"\n]*"/gu, " ");
73
- }
74
-
75
- function route(role, reasonCode, reason, signals, action = role === "controller" ? "direct" : "delegate", targetRole) {
76
- return Object.freeze({
77
- role,
78
- mode: action,
79
- action,
80
- ...(targetRole === undefined ? {} : { targetRole }),
81
- reasonCode,
82
- reason,
83
- signals: Object.freeze([...new Set(signals)]),
84
- });
85
- }
86
-
87
- /**
88
- * Choose the smallest useful odai role. Risk alone never creates another role.
89
- * Executor routing requires a frozen route card and observable net benefit;
90
- * those facts cannot be inferred from task prose.
91
- */
92
- export function decideRoute(input = {}) {
93
- const text = typeof input.text === "string" ? input.text.trim() : "";
94
- const explicitIntentText = stripQuotedMaterial(text);
95
- const signals = [];
96
- const riskPresent = matchesAny(text, RISK_PATTERNS);
97
- const unverifiedCausalClaim = matchesAny(text, UNVERIFIED_CAUSAL_PATTERNS);
98
- const concreteChangeRequest = matchesAny(text, CONCRETE_CHANGE_PATTERNS);
99
- const specificOperationalParameter = matchesAny(text, SPECIFIC_PARAMETER_PATTERNS);
100
- const urgencyPressure = matchesAny(text, URGENCY_PATTERNS);
101
- const irreversibleAction = matchesAny(text, IRREVERSIBLE_ACTION_PATTERNS);
102
-
103
- if (riskPresent) signals.push("risk-present");
104
- if (unverifiedCausalClaim) signals.push("unverified-causal-claim");
105
- if (concreteChangeRequest) signals.push("concrete-change-request");
106
- if (specificOperationalParameter) signals.push("specific-operational-parameter");
107
- if (urgencyPressure) signals.push("urgency-pressure");
108
- if (irreversibleAction) signals.push("irreversible-action");
109
-
110
- if (input.routeCard?.frozen === true && input.routeCard?.observableBenefit === true) {
111
- signals.push("route-card-frozen", "observable-net-benefit");
112
- return route(
113
- "executor",
114
- "EXECUTOR_FROZEN_ROUTE_NET_BENEFIT",
115
- "A frozen route card exists and executor delegation has an observable net benefit.",
116
- signals,
117
- );
118
- }
119
-
120
- if (matchesAny(explicitIntentText, REVIEWER_PATTERNS)) {
121
- signals.push("explicit-independent-review-gap");
122
- return route(
123
- "reviewer",
124
- "REVIEWER_EXPLICIT_ACCEPTANCE_GAP",
125
- "The request explicitly asks for an independent review or challenge.",
126
- signals,
127
- );
128
- }
129
-
130
- if (matchesAny(explicitIntentText, PLANNER_PATTERNS)) {
131
- signals.push("explicit-independent-decision-gap");
132
- return route(
133
- "planner",
134
- "PLANNER_EXPLICIT_DECISION_GAP",
135
- "The request explicitly asks for an independent plan or architecture decision.",
136
- signals,
137
- );
138
- }
139
-
140
- const hasContextualPlannerGap = riskPresent
141
- && unverifiedCausalClaim
142
- && concreteChangeRequest
143
- && (specificOperationalParameter || urgencyPressure || irreversibleAction);
144
- if (hasContextualPlannerGap) {
145
- return route(
146
- "controller",
147
- HIGH_IMPACT_PLANNER_REASON,
148
- "An unverified causal claim is being used to justify a concrete high-impact change, so the controller needs a stronger decision route.",
149
- signals,
150
- "upgrade",
151
- "planner",
152
- );
153
- }
154
-
155
- signals.push("no-independent-gap");
156
- return route(
157
- "controller",
158
- "DIRECT_DEFAULT_NO_INDEPENDENT_GAP",
159
- "No explicit independent decision, execution, or acceptance gap was found.",
160
- signals,
161
- );
162
- }
163
-
164
- export function extractLatestUserText(messages) {
165
- if (!Array.isArray(messages)) return "";
166
-
167
- for (let index = messages.length - 1; index >= 0; index -= 1) {
168
- const message = messages[index];
169
- if (message?.role !== "user") continue;
170
- if (message?.source?.kind !== "user") continue;
171
- if (!Array.isArray(message.content)) continue;
172
-
173
- const text = message.content
174
- .filter((block) => block?.type === "text" && typeof block.text === "string")
175
- .map((block) => block.text)
176
- .join("\n")
177
- .trim();
178
- if (text) return text;
179
- }
180
-
181
- return "";
182
- }
183
-
184
- export function requiresFailClosedProtection(decision) {
185
- if (decision?.reasonCode === HIGH_IMPACT_PLANNER_REASON) return true;
186
-
187
- const decisionRole = decision?.targetRole ?? decision?.role;
188
- if (!["planner", "executor", "reviewer"].includes(decisionRole)) return false;
189
-
190
- const signals = new Set(Array.isArray(decision.signals) ? decision.signals : []);
191
- if (!signals.has("risk-present")) return false;
192
- return signals.has("irreversible-action")
193
- || (signals.has("concrete-change-request")
194
- && (signals.has("specific-operational-parameter") || signals.has("urgency-pressure")));
195
- }
196
-
197
- function observeProtocol(decision) {
198
- const shared = [
199
- "Observe-mode controller protocol:",
200
- "- No independent role was run. Do not claim independent planning or review.",
201
- "- Perform the missing responsibility locally: separate facts from assumptions, inspect decisive project evidence, and state what remains unverified.",
202
- "- Ground the path in capabilities and evidence that actually exist. Treat unavailable environments, tools, owners, thresholds, and protections as missing conditions, not facts.",
203
- "- End with concrete evidence-gathering steps and explicit decision criteria that let the user safely continue; objection alone is not a complete delivery.",
204
- ];
205
- if (!requiresFailClosedProtection(decision)) return shared;
206
-
207
- return [
208
- ...shared,
209
- "- High-impact fail-closed boundary: do not implement, persist, or publish the requested change in this turn. Use read-only evidence only.",
210
- "- Explain the protection-chain gap and keep the current state unchanged until the decision basis and end-to-end safety dependency are verified.",
211
- ];
212
- }
213
-
214
- export function renderRouteNotice(decision, runtimeMode, actualRoute) {
215
- const routeRole = decision.targetRole ?? decision.role;
216
- const isUpgrade = decision.action === "upgrade" && runtimeMode === "auto";
217
- const action = isUpgrade
218
- ? "The current controller turn requested an in-place upgrade; no child was started."
219
- : runtimeMode === "observe"
220
- ? `The ${routeRole} gap was selected in observe mode; do not start a child automatically.`
221
- : `The ${routeRole} route was selected and executed as an independent child.`;
222
- const routeIdentity = actualRoute
223
- ? [`${isUpgrade ? "requested controller route" : "verified child route"}: ${actualRoute.provider}/${actualRoute.model} (reasoning: ${actualRoute.reasoningEffort ?? "unspecified"})`]
224
- : [];
225
-
226
- return [
227
- "odai automatic routing decision",
228
- `role: ${decision.role}`,
229
- `action: ${isUpgrade ? "upgrade" : decision.action}`,
230
- ...(decision.targetRole ? [`target responsibility: ${decision.targetRole}`] : []),
231
- `reason: ${decision.reasonCode}`,
232
- `runtime: ${runtimeMode}`,
233
- action,
234
- ...routeIdentity,
235
- ...(runtimeMode === "observe" ? ["", ...observeProtocol(decision)] : []),
236
- ].join("\n");
237
- }
238
-
239
- export function renderMissingRouteConfigNotice(decision, runtimeMode, configFailure) {
240
- const routeRole = decision.targetRole ?? decision.role;
241
- const naturalRole = RESPONSIBILITY_LABELS[routeRole] ?? routeRole;
242
- const invalidConfig = typeof configFailure === "string" && configFailure !== "";
243
- return [
244
- `odai routing capability is ${invalidConfig ? "invalid" : "not configured"}`,
245
- `required responsibility: ${routeRole}`,
246
- `runtime: ${runtimeMode}`,
247
- `${invalidConfig ? "untrusted" : "missing"} responsibility mapping: ${routeRole}`,
248
- ...(invalidConfig ? [`configuration error: ${configFailure}`] : []),
249
- `No ${routeRole} model was called. Do not claim that this responsibility ran or that the controller was upgraded.`,
250
- `Tell the user that the required ${routeRole} model is ${invalidConfig ? "unavailable because its saved configuration is invalid" : "not configured"}. Ask them to name the provider, model, and optional reasoning effort in natural language.`,
251
- `Example: “把${naturalRole}模型设为 <provider>/<model>,推理档设为 <effort>。”`,
252
- `When the user specifies that mapping, call the odai_routing_config tool to ${invalidConfig ? "repair and " : ""}persist it. Do not ask the user to edit YAML or JSON, run a command, or add routing terminology to future task prompts.`,
253
-
254
- ...(requiresFailClosedProtection(decision) ? [
255
- "High-impact fail-closed protection is active for this turn.",
256
- "Do not implement, persist, or publish the requested change. Use read-only evidence only until the missing responsibility is configured or the decision gap is otherwise resolved.",
257
- ] : [
258
- "Continue only with parts that do not depend on the missing independent responsibility.",
259
- ]),
260
- ].join("\n");
261
- }
262
-
263
- export function renderRouteFailureNotice(decision, failure) {
264
- if (!requiresFailClosedProtection(decision)) {
265
- return `odai ${decision.role} route failed (${failure}); continue directly as controller and do not claim delegated evidence.`;
266
- }
267
-
268
- return [
269
- `odai ${decision.role} route failed (${failure}); no independent evidence was obtained.`,
270
- "High-impact fail-closed protection is active for this turn.",
271
- "Do not implement, persist, or publish the requested change. Use read-only evidence only.",
272
- "Ground the path in capabilities and evidence that actually exist. Treat unavailable environments, tools, owners, thresholds, and protections as missing conditions, not facts.",
273
- "Explain the unresolved decision-evidence and protection-chain gaps, then provide concrete evidence-gathering steps and explicit decision criteria. Objection alone is not a complete delivery.",
274
- ].join("\n");
275
- }
276
-
277
- export function renderDelegationPrompt(decision, taskText, roleContract) {
278
- if (typeof roleContract !== "string" || roleContract.trim() === "") {
279
- throw new TypeError(`canonical ${decision.role} role contract must be a non-empty string`);
280
- }
281
-
282
- return [
283
- `You are the odai ${decision.role}.`,
284
- roleContract.trim(),
285
- "Runtime boundary: do not edit files, run shell commands, ask the user, or delegate further. The controller owns all final decisions and delivery.",
286
- "",
287
- "Task:",
288
- taskText,
289
- ].join("\n");
290
- }
1
+ export const HIGH_IMPACT_PLANNER_REASON = "PLANNER_UNVERIFIED_HIGH_IMPACT_CHANGE";
2
+
3
+ const RESPONSIBILITY_LABELS = Object.freeze({
4
+ planner: "规划",
5
+ executor: "执行",
6
+ reviewer: "验收",
7
+ });
8
+
9
+ const PLANNER_PATTERNS = [
10
+ /独立(?:判断|规划|分析|决定|决策)/iu,
11
+ /比较(?:一下)?(?:方案|路线|架构)/iu,
12
+ /架构选型/iu,
13
+ /先(?:判断|规划)(?:路线|方案)/iu,
14
+ /second opinion (?:on|for) (?:the )?(?:plan|approach|architecture)/iu,
15
+ /compare (?:the )?(?:approaches|options|architectures)/iu,
16
+ /architecture decision/iu,
17
+ /(?:independently decide|independent decision)/iu,
18
+ ];
19
+
20
+ const REVIEWER_PATTERNS = [
21
+ /独立(?:审查|复核|评审)/iu,
22
+ /(?:代码|安全|变更)(?:审查|复核|评审)/iu,
23
+ /code review/iu,
24
+ /security review/iu,
25
+ /independent review/iu,
26
+ /challenge (?:the )?(?:plan|implementation|result)/iu,
27
+ ];
28
+
29
+ const RISK_PATTERNS = [
30
+ /(?:高风险|安全|权限|删除|迁移|发布|生产环境)/iu,
31
+ /(?:支付|付款|结算|扣款|退款|订单)[^。!?\n]{0,60}(?:提供方|服务|接口|请求|结果|状态|超时|重试|金额|交易|配置|回调|不稳定)/iu,
32
+ /(?:high[- ]risk|security|permission|delete|migration|production)/iu,
33
+ /(?:checkout|payment|billing|charge|refund)[^.!?\n]{0,80}(?:provider|service|request|result|status|timeout|retry|transaction|configuration|unstable|fail|超时|重试|不稳定)/iu,
34
+ ];
35
+
36
+ const UNVERIFIED_CAUSAL_PATTERNS = [
37
+ /(?:我看|看起来|看来|估计|大概|八成|明显|肯定)(?:就)?是/iu,
38
+ /(?:问题|原因|根因)(?:应该|可能|大概|估计)(?:就)?在/iu,
39
+ /\b(?:it\s+)?(?:looks?|seems)\s+like\b/iu,
40
+ /\b(?:i think|probably|apparently|clearly|obviously)\b[^.!?\n]{0,80}\b(?:is|are|because|caused by|comes from)\b/iu,
41
+ ];
42
+
43
+ const CONCRETE_CHANGE_PATTERNS = [
44
+ /把[^。!?\n]{1,120}(?:降到|提到|提高到|改成|设为|设置为|切到|换成|开启|关闭|删除|清空|迁移到|发布到|上线)/iu,
45
+ /(?:降低|提高|增加|减少|修改|设置|开启|关闭|删除|清空|迁移|发布|上线)[^。!?\n]{0,80}(?:\d|配置|超时|重试|权限|数据|服务)/iu,
46
+ /\b(?:set|change|reduce|lower|increase|raise|disable|enable|delete|drop|clear|migrate|deploy|release)\b[^.!?\n]{0,100}\b(?:to|from|timeout|retries|permission|data|service|production)\b/iu,
47
+ ];
48
+
49
+ const SPECIFIC_PARAMETER_PATTERNS = [
50
+ /\d+(?:\.\d+)?\s*(?:毫秒|秒|分钟|小时|次|%|%|ms|sec(?:ond)?s?|min(?:ute)?s?|hours?|retries?|mb|gb)/iu,
51
+ ];
52
+
53
+ const URGENCY_PATTERNS = [
54
+ /(?:先止血|立即|马上|赶紧|紧急|热修)/iu,
55
+ /\b(?:hotfix|immediately|right away|stop the bleeding)\b/iu,
56
+ ];
57
+
58
+ const IRREVERSIBLE_ACTION_PATTERNS = [
59
+ /(?:删除|清空|迁移|发布|上线|关闭)/iu,
60
+ /\b(?:delete|drop|clear|migrate|deploy|release|disable)\b/iu,
61
+ ];
62
+
63
+ function matchesAny(text, patterns) {
64
+ return patterns.some((pattern) => pattern.test(text));
65
+ }
66
+
67
+ function stripQuotedMaterial(text) {
68
+ return text
69
+ .replace(/```[\s\S]*?```/gu, " ")
70
+ .replace(/^\s*>.*$/gmu, " ")
71
+ .replace(/`[^`\n]*`/gu, " ")
72
+ .replace(/“[^”\n]*”|‘[^’\n]*’|「[^」\n]*」|『[^』\n]*』|《[^》\n]*》|"[^"\n]*"/gu, " ");
73
+ }
74
+
75
+ function route(role, reasonCode, reason, signals, action = role === "controller" ? "direct" : "delegate", targetRole) {
76
+ return Object.freeze({
77
+ role,
78
+ mode: action,
79
+ action,
80
+ ...(targetRole === undefined ? {} : { targetRole }),
81
+ reasonCode,
82
+ reason,
83
+ signals: Object.freeze([...new Set(signals)]),
84
+ });
85
+ }
86
+
87
+ /**
88
+ * Choose the smallest useful odai role. Risk alone never creates another role.
89
+ * Executor routing requires a frozen route card and observable net benefit;
90
+ * those facts cannot be inferred from task prose.
91
+ */
92
+ export function decideRoute(input = {}) {
93
+ const text = typeof input.text === "string" ? input.text.trim() : "";
94
+ const explicitIntentText = stripQuotedMaterial(text);
95
+ const signals = [];
96
+ const riskPresent = matchesAny(text, RISK_PATTERNS);
97
+ const unverifiedCausalClaim = matchesAny(text, UNVERIFIED_CAUSAL_PATTERNS);
98
+ const concreteChangeRequest = matchesAny(text, CONCRETE_CHANGE_PATTERNS);
99
+ const specificOperationalParameter = matchesAny(text, SPECIFIC_PARAMETER_PATTERNS);
100
+ const urgencyPressure = matchesAny(text, URGENCY_PATTERNS);
101
+ const irreversibleAction = matchesAny(text, IRREVERSIBLE_ACTION_PATTERNS);
102
+
103
+ if (riskPresent) signals.push("risk-present");
104
+ if (unverifiedCausalClaim) signals.push("unverified-causal-claim");
105
+ if (concreteChangeRequest) signals.push("concrete-change-request");
106
+ if (specificOperationalParameter) signals.push("specific-operational-parameter");
107
+ if (urgencyPressure) signals.push("urgency-pressure");
108
+ if (irreversibleAction) signals.push("irreversible-action");
109
+
110
+ if (input.routeCard?.frozen === true && input.routeCard?.observableBenefit === true) {
111
+ signals.push("route-card-frozen", "observable-net-benefit");
112
+ return route(
113
+ "executor",
114
+ "EXECUTOR_FROZEN_ROUTE_NET_BENEFIT",
115
+ "A frozen route card exists and executor delegation has an observable net benefit.",
116
+ signals,
117
+ );
118
+ }
119
+
120
+ if (matchesAny(explicitIntentText, REVIEWER_PATTERNS)) {
121
+ signals.push("explicit-independent-review-gap");
122
+ return route(
123
+ "reviewer",
124
+ "REVIEWER_EXPLICIT_ACCEPTANCE_GAP",
125
+ "The request explicitly asks for an independent review or challenge.",
126
+ signals,
127
+ );
128
+ }
129
+
130
+ if (matchesAny(explicitIntentText, PLANNER_PATTERNS)) {
131
+ signals.push("explicit-independent-decision-gap");
132
+ return route(
133
+ "planner",
134
+ "PLANNER_EXPLICIT_DECISION_GAP",
135
+ "The request explicitly asks for an independent plan or architecture decision.",
136
+ signals,
137
+ );
138
+ }
139
+
140
+ const hasContextualPlannerGap = riskPresent
141
+ && unverifiedCausalClaim
142
+ && concreteChangeRequest
143
+ && (specificOperationalParameter || urgencyPressure || irreversibleAction);
144
+ if (hasContextualPlannerGap) {
145
+ return route(
146
+ "controller",
147
+ HIGH_IMPACT_PLANNER_REASON,
148
+ "An unverified causal claim is being used to justify a concrete high-impact change, so the controller needs a stronger decision route.",
149
+ signals,
150
+ "upgrade",
151
+ "planner",
152
+ );
153
+ }
154
+
155
+ signals.push("no-independent-gap");
156
+ return route(
157
+ "controller",
158
+ "DIRECT_DEFAULT_NO_INDEPENDENT_GAP",
159
+ "No explicit independent decision, execution, or acceptance gap was found.",
160
+ signals,
161
+ );
162
+ }
163
+
164
+ export function extractLatestUserText(messages) {
165
+ if (!Array.isArray(messages)) return "";
166
+
167
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
168
+ const message = messages[index];
169
+ if (message?.role !== "user") continue;
170
+ if (message?.source?.kind !== "user") continue;
171
+ if (!Array.isArray(message.content)) continue;
172
+
173
+ const text = message.content
174
+ .filter((block) => block?.type === "text" && typeof block.text === "string")
175
+ .map((block) => block.text)
176
+ .join("\n")
177
+ .trim();
178
+ if (text) return text;
179
+ }
180
+
181
+ return "";
182
+ }
183
+
184
+ export function requiresFailClosedProtection(decision) {
185
+ if (decision?.reasonCode === HIGH_IMPACT_PLANNER_REASON) return true;
186
+
187
+ const decisionRole = decision?.targetRole ?? decision?.role;
188
+ if (!["planner", "executor", "reviewer"].includes(decisionRole)) return false;
189
+
190
+ const signals = new Set(Array.isArray(decision.signals) ? decision.signals : []);
191
+ if (!signals.has("risk-present")) return false;
192
+ return signals.has("irreversible-action")
193
+ || (signals.has("concrete-change-request")
194
+ && (signals.has("specific-operational-parameter") || signals.has("urgency-pressure")));
195
+ }
196
+
197
+ function observeProtocol(decision) {
198
+ const shared = [
199
+ "Observe-mode controller protocol:",
200
+ "- No independent role was run. Do not claim independent planning or review.",
201
+ "- Perform the missing responsibility locally: separate facts from assumptions, inspect decisive project evidence, and state what remains unverified.",
202
+ "- Ground the path in capabilities and evidence that actually exist. Treat unavailable environments, tools, owners, thresholds, and protections as missing conditions, not facts.",
203
+ "- End with concrete evidence-gathering steps and explicit decision criteria that let the user safely continue; objection alone is not a complete delivery.",
204
+ ];
205
+ if (!requiresFailClosedProtection(decision)) return shared;
206
+
207
+ return [
208
+ ...shared,
209
+ "- High-impact fail-closed boundary: do not implement, persist, or publish the requested change in this turn. Use read-only evidence only.",
210
+ "- Explain the protection-chain gap and keep the current state unchanged until the decision basis and end-to-end safety dependency are verified.",
211
+ ];
212
+ }
213
+
214
+ export function renderRouteNotice(decision, runtimeMode, actualRoute) {
215
+ const routeRole = decision.targetRole ?? decision.role;
216
+ const isUpgrade = decision.action === "upgrade" && runtimeMode === "auto";
217
+ const action = isUpgrade
218
+ ? "The current controller turn requested an in-place upgrade; no child was started."
219
+ : runtimeMode === "observe"
220
+ ? `The ${routeRole} gap was selected in observe mode; do not start a child automatically.`
221
+ : `The ${routeRole} route was selected and executed as an independent child.`;
222
+ const routeIdentity = actualRoute
223
+ ? [`${isUpgrade ? "requested controller route" : "verified child route"}: ${actualRoute.provider}/${actualRoute.model} (reasoning: ${actualRoute.reasoningEffort ?? "unspecified"})`]
224
+ : [];
225
+
226
+ return [
227
+ "odai automatic routing decision",
228
+ `role: ${decision.role}`,
229
+ `action: ${isUpgrade ? "upgrade" : decision.action}`,
230
+ ...(decision.targetRole ? [`target responsibility: ${decision.targetRole}`] : []),
231
+ `reason: ${decision.reasonCode}`,
232
+ `runtime: ${runtimeMode}`,
233
+ action,
234
+ ...routeIdentity,
235
+ ...(runtimeMode === "observe" ? ["", ...observeProtocol(decision)] : []),
236
+ ].join("\n");
237
+ }
238
+
239
+ export function renderMissingRouteConfigNotice(decision, runtimeMode, configFailure) {
240
+ const routeRole = decision.targetRole ?? decision.role;
241
+ const naturalRole = RESPONSIBILITY_LABELS[routeRole] ?? routeRole;
242
+ const invalidConfig = typeof configFailure === "string" && configFailure !== "";
243
+ return [
244
+ `odai routing capability is ${invalidConfig ? "invalid" : "not configured"}`,
245
+ `required responsibility: ${routeRole}`,
246
+ `runtime: ${runtimeMode}`,
247
+ `${invalidConfig ? "untrusted" : "missing"} responsibility mapping: ${routeRole}`,
248
+ ...(invalidConfig ? [`configuration error: ${configFailure}`] : []),
249
+ `No ${routeRole} model was called. Do not claim that this responsibility ran or that the controller was upgraded.`,
250
+ `Tell the user that the required ${routeRole} model is ${invalidConfig ? "unavailable because its saved configuration is invalid" : "not configured"}. Ask them to name the provider, model, and optional reasoning effort in natural language.`,
251
+ `Example: “把${naturalRole}模型设为 <provider>/<model>,推理档设为 <effort>。”`,
252
+ `When the user specifies that mapping, call the odai_routing_config tool to ${invalidConfig ? "repair and " : ""}persist it. Do not ask the user to edit YAML or JSON, run a command, or add routing terminology to future task prompts.`,
253
+
254
+ ...(requiresFailClosedProtection(decision) ? [
255
+ "High-impact fail-closed protection is active for this turn.",
256
+ "Do not implement, persist, or publish the requested change. Use read-only evidence only until the missing responsibility is configured or the decision gap is otherwise resolved.",
257
+ ] : [
258
+ "Continue only with parts that do not depend on the missing independent responsibility.",
259
+ ]),
260
+ ].join("\n");
261
+ }
262
+
263
+ export function renderRouteFailureNotice(decision, failure) {
264
+ if (!requiresFailClosedProtection(decision)) {
265
+ return `odai ${decision.role} route failed (${failure}); continue directly as controller and do not claim delegated evidence.`;
266
+ }
267
+
268
+ return [
269
+ `odai ${decision.role} route failed (${failure}); no independent evidence was obtained.`,
270
+ "High-impact fail-closed protection is active for this turn.",
271
+ "Do not implement, persist, or publish the requested change. Use read-only evidence only.",
272
+ "Ground the path in capabilities and evidence that actually exist. Treat unavailable environments, tools, owners, thresholds, and protections as missing conditions, not facts.",
273
+ "Explain the unresolved decision-evidence and protection-chain gaps, then provide concrete evidence-gathering steps and explicit decision criteria. Objection alone is not a complete delivery.",
274
+ ].join("\n");
275
+ }
276
+
277
+ export function renderDelegationPrompt(decision, taskText, roleContract) {
278
+ if (typeof roleContract !== "string" || roleContract.trim() === "") {
279
+ throw new TypeError(`canonical ${decision.role} role contract must be a non-empty string`);
280
+ }
281
+
282
+ return [
283
+ `You are the odai ${decision.role}.`,
284
+ roleContract.trim(),
285
+ "Runtime boundary: do not edit files, run shell commands, ask the user, or delegate further. The controller owns all final decisions and delivery.",
286
+ "",
287
+ "Task:",
288
+ taskText,
289
+ ].join("\n");
290
+ }