@xdxer/dingtalk-agent 0.1.4-beta.7 → 0.1.4-beta.9

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 (265) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.en.md +395 -0
  3. package/README.md +466 -36
  4. package/dist/bin/dingtalk-agent.js +1071 -340
  5. package/dist/bin/dingtalk-agent.js.map +1 -1
  6. package/dist/src/actions.js +98 -14
  7. package/dist/src/actions.js.map +1 -1
  8. package/dist/src/agent-audit.js +460 -0
  9. package/dist/src/agent-audit.js.map +1 -0
  10. package/dist/src/agent-bindings.js +132 -0
  11. package/dist/src/agent-bindings.js.map +1 -0
  12. package/dist/src/agent-definition.js +182 -0
  13. package/dist/src/agent-definition.js.map +1 -0
  14. package/dist/src/agent-enhance.js +678 -0
  15. package/dist/src/agent-enhance.js.map +1 -0
  16. package/dist/src/bootstrap.js +125 -17
  17. package/dist/src/bootstrap.js.map +1 -1
  18. package/dist/src/development-workspace.js +729 -0
  19. package/dist/src/development-workspace.js.map +1 -0
  20. package/dist/src/dws.js +145 -0
  21. package/dist/src/dws.js.map +1 -1
  22. package/dist/src/eval-evidence.js +193 -0
  23. package/dist/src/eval-evidence.js.map +1 -0
  24. package/dist/src/init.js +1 -1
  25. package/dist/src/init.js.map +1 -1
  26. package/dist/src/invocation.js +36 -0
  27. package/dist/src/invocation.js.map +1 -0
  28. package/dist/src/lab.js +679 -0
  29. package/dist/src/lab.js.map +1 -0
  30. package/dist/src/lease.js +100 -0
  31. package/dist/src/lease.js.map +1 -0
  32. package/dist/src/memory/candidates.js +451 -0
  33. package/dist/src/memory/candidates.js.map +1 -0
  34. package/dist/src/memory/completion-evidence.js +536 -0
  35. package/dist/src/memory/completion-evidence.js.map +1 -0
  36. package/dist/src/memory/operational.js +263 -0
  37. package/dist/src/memory/operational.js.map +1 -0
  38. package/dist/src/memory/remote-state.js +478 -0
  39. package/dist/src/memory/remote-state.js.map +1 -0
  40. package/dist/src/memory/task-checkpoints.js +204 -0
  41. package/dist/src/memory/task-checkpoints.js.map +1 -0
  42. package/dist/src/multica-deploy.js +1480 -0
  43. package/dist/src/multica-deploy.js.map +1 -0
  44. package/dist/src/multica-provider.js +685 -0
  45. package/dist/src/multica-provider.js.map +1 -0
  46. package/dist/src/opencode-evals.js +1062 -0
  47. package/dist/src/opencode-evals.js.map +1 -0
  48. package/dist/src/opencode-provider.js +531 -0
  49. package/dist/src/opencode-provider.js.map +1 -0
  50. package/dist/src/opencode-workspace.js +197 -0
  51. package/dist/src/opencode-workspace.js.map +1 -0
  52. package/dist/src/perception.js +225 -0
  53. package/dist/src/perception.js.map +1 -0
  54. package/dist/src/personal-event-evals.js +595 -0
  55. package/dist/src/personal-event-evals.js.map +1 -0
  56. package/dist/src/promotion.js +786 -0
  57. package/dist/src/promotion.js.map +1 -0
  58. package/dist/src/remote-semantic-state-live-evals.js +888 -0
  59. package/dist/src/remote-semantic-state-live-evals.js.map +1 -0
  60. package/dist/src/remote-semantic-state-worker.js +38 -0
  61. package/dist/src/remote-semantic-state-worker.js.map +1 -0
  62. package/dist/src/remote-state-evals.js +501 -0
  63. package/dist/src/remote-state-evals.js.map +1 -0
  64. package/dist/src/response-gate.js +51 -0
  65. package/dist/src/response-gate.js.map +1 -0
  66. package/dist/src/robot-evals.js +770 -0
  67. package/dist/src/robot-evals.js.map +1 -0
  68. package/dist/src/sessions.js +66 -105
  69. package/dist/src/sessions.js.map +1 -1
  70. package/dist/src/skill-manager.js +25 -16
  71. package/dist/src/skill-manager.js.map +1 -1
  72. package/dist/src/skills.js.map +1 -1
  73. package/dist/src/storage-evals.js +26 -0
  74. package/dist/src/storage-evals.js.map +1 -0
  75. package/dist/src/types.js.map +1 -1
  76. package/dist/src/waits.js +5 -1
  77. package/dist/src/waits.js.map +1 -1
  78. package/dist/src/workspace.js +28 -3
  79. package/dist/src/workspace.js.map +1 -1
  80. package/docs/INSTALLATION.md +47 -0
  81. package/docs/SECOND-AGENT-ACCEPTANCE.md +62 -0
  82. package/docs/architecture/agent-memory-topology.png +0 -0
  83. package/docs/architecture/agent-memory-topology.svg +132 -0
  84. package/docs/architecture/general-agent-kernel-topology.png +0 -0
  85. package/docs/architecture/general-agent-kernel-topology.svg +149 -0
  86. package/docs/architecture/provider-bound-development-workspace.png +0 -0
  87. package/docs/architecture/provider-bound-development-workspace.svg +141 -0
  88. package/docs/architecture/task-completion-gate.png +0 -0
  89. package/docs/architecture/task-completion-gate.svg +191 -0
  90. package/docs/schemas/agent-audit-load-evidence.schema.json +14 -0
  91. package/docs/schemas/agent-audit.schema.json +92 -0
  92. package/docs/schemas/agent-bindings.schema.json +54 -0
  93. package/docs/schemas/agent-definition.schema.json +78 -0
  94. package/docs/schemas/agent-enhancement-plan.schema.json +88 -0
  95. package/docs/schemas/agent-enhancement-receipt.schema.json +37 -0
  96. package/docs/schemas/enriched-invocation.schema.json +46 -0
  97. package/docs/schemas/eval-candidate-plan.schema.json +28 -0
  98. package/docs/schemas/eval-candidate-result.schema.json +20 -0
  99. package/docs/schemas/eval-candidate.schema.json +30 -0
  100. package/docs/schemas/invocation.schema.json +19 -0
  101. package/docs/schemas/memory-candidate-proposal.schema.json +18 -0
  102. package/docs/schemas/memory-candidate.schema.json +76 -0
  103. package/docs/schemas/memory-publish-target.schema.json +25 -0
  104. package/docs/schemas/multica-deployment-list.schema.json +29 -0
  105. package/docs/schemas/multica-deployment-operation.schema.json +51 -0
  106. package/docs/schemas/multica-deployment-plan.schema.json +70 -0
  107. package/docs/schemas/multica-deployment-receipt.schema.json +87 -0
  108. package/docs/schemas/multica-deployment-status.schema.json +23 -0
  109. package/docs/schemas/multica-workspace-inspection.schema.json +77 -0
  110. package/docs/schemas/multica-workspace-plan.schema.json +68 -0
  111. package/docs/schemas/multica-workspace-resource-list.schema.json +27 -0
  112. package/docs/schemas/multica-workspace-status.schema.json +34 -0
  113. package/docs/schemas/observation.schema.json +21 -0
  114. package/docs/schemas/operational-memory-provider.schema.json +36 -0
  115. package/docs/schemas/operational-memory-record.schema.json +24 -0
  116. package/docs/schemas/perception-input.schema.json +56 -0
  117. package/docs/schemas/project.schema.json +112 -0
  118. package/docs/schemas/promotion-list.schema.json +36 -0
  119. package/docs/schemas/promotion-plan.schema.json +60 -0
  120. package/docs/schemas/promotion-policy.schema.json +29 -0
  121. package/docs/schemas/promotion-receipt.schema.json +43 -0
  122. package/docs/schemas/promotion-status.schema.json +23 -0
  123. package/docs/schemas/release-readiness.schema.json +65 -0
  124. package/docs/schemas/remote-semantic-state-live-eval.schema.json +60 -0
  125. package/docs/schemas/remote-semantic-state-manifest.schema.json +79 -0
  126. package/docs/schemas/remote-semantic-state-provider.schema.json +98 -0
  127. package/docs/schemas/response-gate.schema.json +20 -0
  128. package/docs/schemas/task-checkpoint.schema.json +71 -0
  129. package/docs/schemas/task-completion-evidence.schema.json +154 -0
  130. package/docs/schemas/workspace-doctor.schema.json +56 -0
  131. package/docs/schemas/workspace-state.schema.json +39 -0
  132. package/evals/baselines/2026-07-16/opencode-basic-010-completion-summary.json +123 -0
  133. package/evals/baselines/2026-07-16/opencode-basic-skill-required-summary.json +69 -0
  134. package/evals/baselines/2026-07-16/opencode-multi-surface-summary.json +63 -0
  135. package/evals/baselines/2026-07-16/remote-state-live-summary.json +70 -0
  136. package/evals/baselines/2026-07-17/agent-enhance-opencode-dogfood-summary.json +98 -0
  137. package/evals/baselines/2026-07-17/personal-event-live-readiness-summary.json +68 -0
  138. package/examples/agents/fde-coach/AGENTS.md +26 -0
  139. package/examples/agents/fde-coach/MEMORY.md +3 -0
  140. package/examples/agents/fde-coach/fields/default/field.json +24 -0
  141. package/examples/agents/fde-coach/knowledge/INDEX.md +4 -0
  142. package/examples/agents/fde-coach/skills/fde-coach/SKILL.md +13 -0
  143. package/examples/agents/release-manager/AGENTS.md +26 -0
  144. package/examples/agents/release-manager/MEMORY.md +3 -0
  145. package/examples/agents/release-manager/fields/default/field.json +24 -0
  146. package/examples/agents/release-manager/knowledge/INDEX.md +4 -0
  147. package/examples/agents/release-manager/skills/release-manager/SKILL.md +13 -0
  148. package/lab/README.md +109 -0
  149. package/lab/agent-eval/catalog.json +91 -0
  150. package/lab/agent-eval/classic-failures.json +177 -0
  151. package/lab/agent-eval/completion-gate-regression.json +99 -0
  152. package/lab/agent-eval/personal-event-live.example.json +94 -0
  153. package/lab/agent-eval/remote-semantic-state-live.example.json +70 -0
  154. package/lab/agent-eval/remote-semantic-state-provider.fixture.json +47 -0
  155. package/lab/agent-eval/remote-state-workspace/AGENTS.md +8 -0
  156. package/lab/agent-eval/remote-state-workspace/opencode.json +7 -0
  157. package/lab/agent-eval/remote-state-workspace/skills/remote-state-operator/SKILL.md +13 -0
  158. package/lab/agent-eval/remote-state.example.json +31 -0
  159. package/lab/agent-eval/workspace/AGENTS.md +7 -0
  160. package/lab/agent-eval/workspace/MEMORY.md +4 -0
  161. package/lab/agent-eval/workspace/artifacts/pending-review.md +3 -0
  162. package/lab/agent-eval/workspace/knowledge/INDEX.md +4 -0
  163. package/lab/agent-eval/workspace/opencode.json +20 -0
  164. package/lab/manifest.example.json +27 -0
  165. package/lab/manifest.personal-event.example.json +27 -0
  166. package/lab/project-workspace/README.md +11 -0
  167. package/lab/project-workspace/fake-multica-provider.mjs +266 -0
  168. package/lab/project-workspace/multica-deploy.fixture.json +29 -0
  169. package/lab/project-workspace/multica-readonly.fixture.json +69 -0
  170. package/lab/project-workspace/observation.fixture.json +14 -0
  171. package/lab/project-workspace/opencode-provider-suite.json +65 -0
  172. package/lab/project-workspace/project.fixture.json +44 -0
  173. package/lab/project-workspace/promotion-policy.fixture.json +15 -0
  174. package/lab/robot-eval/pool.example.json +30 -0
  175. package/lab/robot-eval/suite.json +123 -0
  176. package/lab/robot-eval/workspace/AGENTS.md +21 -0
  177. package/lab/robot-eval/workspace/MEMORY.md +3 -0
  178. package/lab/robot-eval/workspace/knowledge/INDEX.md +5 -0
  179. package/lab/robot-eval/workspace/opencode.json +22 -0
  180. package/lab/schemas/agent-eval-catalog.schema.json +47 -0
  181. package/lab/schemas/lab-manifest.schema.json +70 -0
  182. package/lab/schemas/personal-event-eval.schema.json +91 -0
  183. package/lab/schemas/remote-state-eval.schema.json +66 -0
  184. package/lab/schemas/robot-eval-suite.schema.json +184 -0
  185. package/lab/schemas/robot-pool.schema.json +56 -0
  186. package/package.json +30 -14
  187. package/skills/dingtalk-agent-boot-multica/SKILL.md +40 -0
  188. package/skills/dingtalk-agent-compose/SKILL.md +110 -0
  189. package/skills/dingtalk-agent-compose/assets/AGENTS.template.md +26 -0
  190. package/skills/dingtalk-agent-compose/assets/agent.bindings.dingtalk-doc.template.json +13 -0
  191. package/skills/dingtalk-agent-compose/assets/agent.bindings.local.template.json +13 -0
  192. package/skills/dingtalk-agent-compose/assets/opencode.template.json +12 -0
  193. package/skills/dingtalk-agent-compose/assets/role-skill.template.md +24 -0
  194. package/skills/dingtalk-agent-compose/evals/evals.json +94 -0
  195. package/skills/dingtalk-agent-compose/references/agent-definition-contract.md +55 -0
  196. package/skills/dingtalk-agent-compose/references/opencode-host-contract.md +65 -0
  197. package/skills/dingtalk-agent-compose/references/storage-routing.md +20 -0
  198. package/skills/dingtalk-agent-deploy/SKILL.md +60 -0
  199. package/skills/dingtalk-agent-deploy/references/multica-deployment-contract.md +49 -0
  200. package/skills/dingtalk-agent-deploy/references/promotion-observation-contract.md +49 -0
  201. package/skills/dingtalk-agent-eval/SKILL.md +116 -0
  202. package/skills/dingtalk-agent-eval/assets/eval-catalog.template.json +18 -0
  203. package/skills/dingtalk-agent-eval/evals/evals.json +61 -0
  204. package/skills/dingtalk-agent-eval/references/eval-topology.md +34 -0
  205. package/skills/dingtalk-agent-eval/references/evidence-contract.md +31 -0
  206. package/skills/dingtalk-agent-eval/references/scenario-taxonomy.md +25 -0
  207. package/skills/dingtalk-agent-eval/references/storage-modes.md +75 -0
  208. package/skills/dingtalk-basic-behavior/SKILL.md +63 -3
  209. package/skills/dingtalk-basic-behavior/assets/memory-candidate-proposal.json +10 -0
  210. package/skills/dingtalk-basic-behavior/assets/{task-checkpoint.md → task-checkpoint.json} +2 -21
  211. package/skills/dingtalk-basic-behavior/references/action-contract.md +2 -0
  212. package/skills/dingtalk-basic-behavior/references/memory-and-evolution.md +15 -1
  213. package/skills/dingtalk-basic-behavior/references/perception-and-gates.md +28 -0
  214. package/skills/dingtalk-basic-behavior/references/task-lifecycle.md +32 -7
  215. package/dist/src/boot.js +0 -70
  216. package/dist/src/boot.js.map +0 -1
  217. package/dist/src/duty.js +0 -74
  218. package/dist/src/duty.js.map +0 -1
  219. package/dist/src/kb.js +0 -240
  220. package/dist/src/kb.js.map +0 -1
  221. package/dist/src/runs.js +0 -79
  222. package/dist/src/runs.js.map +0 -1
  223. package/docs/ARCHITECTURE.md +0 -219
  224. package/docs/MINIMAL-WORKSPACE-V1.md +0 -172
  225. package/docs/OPEN-SOURCE-REFERENCES.md +0 -107
  226. package/docs/SELF-TEST.md +0 -252
  227. package/docs//345/206/205/347/275/221/345/256/236/347/233/270.md +0 -77
  228. package/evals/baselines/2026-07-14/behavior-summary.json +0 -28
  229. package/evals/baselines/2026-07-14/contract-summary.json +0 -18
  230. package/evals/baselines/2026-07-14/live-canary-summary.json +0 -25
  231. package/evals/baselines/2026-07-15/dingtalk-basic-behavior-0.3.0/SKILL.md +0 -72
  232. package/evals/baselines/2026-07-15/dingtalk-basic-behavior-0.3.0/references/action-contract.md +0 -31
  233. package/evals/baselines/2026-07-15/dingtalk-basic-behavior-0.3.0/references/event-to-behavior.md +0 -22
  234. package/evals/baselines/2026-07-15/dingtalk-basic-behavior-0.3.0/references/memory-and-evolution.md +0 -25
  235. package/evals/baselines/2026-07-15/dingtalk-basic-behavior-0.3.0/references/runtime-modes.md +0 -34
  236. package/evals/baselines/2026-07-15/task-lifecycle-summary.json +0 -50
  237. package/evals/evals.json +0 -339
  238. package/evals/fixtures/dm-ambiguous-send.json +0 -4
  239. package/evals/fixtures/dm-blocked.json +0 -4
  240. package/evals/fixtures/dm-clear.json +0 -4
  241. package/evals/fixtures/dm-discussion.json +0 -4
  242. package/evals/fixtures/dm-doc-write-no-tool.json +0 -4
  243. package/evals/fixtures/dm-long-task-ack.json +0 -4
  244. package/evals/fixtures/dm-nonblocking-gap.json +0 -4
  245. package/evals/fixtures/dm-structured-task.json +0 -4
  246. package/evals/fixtures/group.json +0 -10
  247. package/evals/fixtures/mentioned.json +0 -3
  248. package/evals/run-contract-evals.mjs +0 -1169
  249. package/evals/run-shadow-evals.mjs +0 -267
  250. package/evals/runners/README.md +0 -66
  251. package/evals/runners/claude-shadow.mjs +0 -533
  252. package/evals/schemas/action-request.schema.json +0 -77
  253. package/evals/shadow-evals.json +0 -133
  254. package/skills/AGENTS.md +0 -104
  255. package/skills//344/273/273/345/212/241.md +0 -48
  256. package/skills//345/237/272/347/241/200/350/241/214/344/270/272.md +0 -44
  257. package/skills//345/277/203/350/267/263.md +0 -79
  258. package/skills//346/266/210/346/201/257.md +0 -50
  259. package/skills//347/237/245/350/257/206.md +0 -55
  260. package/skills//350/257/204/346/265/213.md +0 -62
  261. package/skills//351/222/211/351/222/211.md +0 -64
  262. package/templates/ontology/index.md +0 -24
  263. package/templates/ontology/self/access.md +0 -38
  264. package/templates/ontology/self/role-spec.md +0 -34
  265. package/templates/ontology/self/workspace.md +0 -41
@@ -0,0 +1,1062 @@
1
+ // OpenCode 本地对照评测:同一 Agent body / 模型 / case 分别运行两个 Basic Skill 配置。
2
+ // 只调用模型并保存 trace;按 case 最小授权隔离文件工具,不连接 DWS 或执行外部动作。
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
6
+ import { tmpdir } from 'node:os';
7
+ import { basename, dirname, join, relative, resolve } from 'node:path';
8
+ import { resolvePackageRoot } from './package-root.js';
9
+ import { bindRequiredBasicSkill, removeRequiredBasicSkillInstruction, verifyRequiredBasicSkill, } from './opencode-workspace.js';
10
+ import { gradeAnswer, loadRobotEvalSuite, } from './robot-evals.js';
11
+ import { evaluateWorkspaceEvidence, isWorkspacePathInside, relativeWorkspacePath, snapshotWorkspaceEvidence, } from './eval-evidence.js';
12
+ export function runOpenCodeEvaluation(root, workspacePath, suitePath, options = {}) {
13
+ const workspace = safeInside(root, workspacePath, true);
14
+ const suite = loadRobotEvalSuite(root, suitePath);
15
+ const lanes = options.lanes?.length
16
+ ? new Set(options.lanes)
17
+ : new Set(suite.cases.map((item) => item.lane));
18
+ const requestedCases = new Set(options.caseIds || []);
19
+ const knownBehaviorCases = new Set(suite.cases
20
+ .filter((item) => item.kind !== 'load-probe').map((item) => item.id));
21
+ const unknownCases = [...requestedCases].filter((id) => !knownBehaviorCases.has(id));
22
+ if (unknownCases.length)
23
+ throw new Error(`OpenCode Eval case 不存在: ${unknownCases.join(',')}`);
24
+ const cases = suite.cases.filter((item) => lanes.has(item.lane) && item.kind !== 'load-probe' &&
25
+ (!requestedCases.size || requestedCases.has(item.id)));
26
+ if (!cases.length)
27
+ throw new Error('OpenCode Eval 选择后没有行为 case');
28
+ const model = options.model || 'deepseek/deepseek-chat';
29
+ if (!model.startsWith('deepseek/'))
30
+ throw new Error('OpenCode Eval 模型必须固定为 deepseek/*');
31
+ const packageRoot = resolvePackageRoot(import.meta.url);
32
+ const currentSkill = readSkillSource(root, join(packageRoot, 'skills', 'dingtalk-basic-behavior'), 'current Skill');
33
+ const previousSkill = options.previousSkill
34
+ ? readSkillSource(root, safeInside(root, options.previousSkill, true), 'previous Skill')
35
+ : null;
36
+ if (previousSkill && previousSkill.hash === currentSkill.hash) {
37
+ throw new Error('OpenCode Eval previous Skill 与 current Skill 内容相同,无法形成版本对照');
38
+ }
39
+ const configurations = previousSkill
40
+ ? [
41
+ { name: 'current_skill', expectsLoad: true, skill: currentSkill },
42
+ { name: 'previous_skill', expectsLoad: true, skill: previousSkill },
43
+ ]
44
+ : [
45
+ { name: 'with_skill', expectsLoad: true, skill: currentSkill },
46
+ { name: 'without_skill', expectsLoad: false, skill: null },
47
+ ];
48
+ const runs = options.runs ?? 3;
49
+ if (!Number.isInteger(runs) || runs < 1 || runs > 10)
50
+ throw new Error('OpenCode Eval runs 必须为 1..10');
51
+ const command = options.command || process.env.DTA_OPENCODE_BIN || 'opencode';
52
+ const timeoutMs = options.timeoutMs ?? 120_000;
53
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 10_000 || timeoutMs > 300_000) {
54
+ throw new Error('OpenCode Eval timeoutMs 必须为 10000..300000');
55
+ }
56
+ const hasWorkspaceWrites = cases.some((item) => ['workspace-write', 'workspace-write-verify'].includes(item.execution?.tools || ''));
57
+ const plan = {
58
+ $schema: 'dingtalk-agent/opencode-eval-plan@1',
59
+ sideEffect: hasWorkspaceWrites ? 'isolated-local-workspace' : false,
60
+ dingtalkSideEffect: false,
61
+ engine: 'opencode',
62
+ model,
63
+ runsPerConfiguration: runs,
64
+ comparisonPolicy: suite.comparisonPolicy || null,
65
+ configurations: configurations.map((item) => item.name),
66
+ skills: Object.fromEntries(configurations.map((item) => [item.name, item.skill ? {
67
+ name: item.skill.name,
68
+ version: item.skill.version,
69
+ hash: item.skill.hash,
70
+ path: item.skill.displayPath,
71
+ snapshot: item.skill.snapshot,
72
+ } : null])),
73
+ workspace: relative(realpathSync(root), workspace),
74
+ suite: suite.id,
75
+ lanes: [...lanes],
76
+ cases: cases.map((item) => ({
77
+ id: item.id,
78
+ scenario: item.scenario || null,
79
+ risk: item.risk || null,
80
+ capability: item.capability || 'unspecified',
81
+ lane: item.lane,
82
+ tools: item.execution?.tools || 'none',
83
+ completionClaim: item.completionClaim || 'permitted',
84
+ evidenceSurfaces: [
85
+ 'response',
86
+ ...new Set((item.postconditions || []).map((expectation) => expectation.surface)),
87
+ ],
88
+ continuationOf: item.continuationOf || null,
89
+ })),
90
+ hardGates: [
91
+ 'project Basic Skill path/hash/version',
92
+ 'opencode resolved instructions',
93
+ previousSkill
94
+ ? 'random per-run load probe for current and previous Skill'
95
+ : 'random per-run load probe for with Skill and anti-guess baseline',
96
+ 'exact per-case marker attribution',
97
+ 'opencode export session directory',
98
+ 'per-case least-privilege tool policy',
99
+ 'declared filesystem/workspace/artifact postconditions',
100
+ ],
101
+ };
102
+ if (!options.execute)
103
+ return { ...plan, ready: true, executeRequired: true };
104
+ if (!options.yes) {
105
+ throw new Error('OpenCode Eval 会调用模型并产生本地 Session/费用;必须同时传 --execute --yes');
106
+ }
107
+ const runId = `opencode_eval_${Date.now()}_${randomUUID().slice(0, 8)}`;
108
+ const evidenceRoot = safeInside(root, options.out || join('.dingtalk-agent', 'opencode-eval-results', runId), false);
109
+ mkdirSync(evidenceRoot, { recursive: true });
110
+ writeJson(join(evidenceRoot, 'plan.json'), plan);
111
+ writeJson(join(evidenceRoot, 'suite.json'), suite);
112
+ const results = [];
113
+ const loadProbes = [];
114
+ const temporaryRoots = [];
115
+ // Keep model-writable workspaces outside the repository tree. OpenCode can otherwise
116
+ // discover a parent .git root and resolve relative write paths against the real checkout.
117
+ const sandboxParent = mkdtempSync(join(tmpdir(), 'dta-opencode-eval-workspaces-'));
118
+ try {
119
+ for (let runNumber = 1; runNumber <= runs; runNumber++) {
120
+ // 交替先后,减少固定顺序导致的 provider cache / 负载偏差。
121
+ const orderedConfigurations = runNumber % 2 === 0
122
+ ? [...configurations].reverse()
123
+ : configurations;
124
+ for (const configuration of orderedConfigurations) {
125
+ const probe = randomUUID();
126
+ const sandbox = mkdtempSync(join(sandboxParent, `dta-opencode-${configuration.name}-`));
127
+ temporaryRoots.push(sandbox);
128
+ cpSync(workspace, sandbox, {
129
+ recursive: true,
130
+ filter: (source) => {
131
+ const rel = relative(workspace, source);
132
+ const first = rel.split(/[\\/]/)[0];
133
+ return first !== '.git' && first !== '.dingtalk-agent';
134
+ },
135
+ });
136
+ const materialized = materializeConfiguration(sandbox, configuration, probe, command);
137
+ const configEvidence = {
138
+ configuration: configuration.name,
139
+ runNumber,
140
+ ...materialized.evidence,
141
+ };
142
+ writeJson(join(evidenceRoot, 'configurations', configuration.name, `run-${runNumber}.json`), configEvidence);
143
+ const probePrompt = '这是加载链路探针,不是业务问题。只依据已强制注入的 Basic Skill 正文末尾注释回答;' +
144
+ '若能看到 `dta-load-probe`,严格输出 `dta-load-probe=<值>`;否则严格输出 `dta-load-probe=not-loaded`。' +
145
+ '禁止猜测、禁止调用工具、不要补充解释。';
146
+ const probeExecution = runOpenCodePrompt(command, sandbox, model, probePrompt, `DTA load probe ${runNumber} ${configuration.name}`, timeoutMs);
147
+ const exactProbe = `dta-load-probe=${probe}`;
148
+ const probeResult = {
149
+ configuration: configuration.name,
150
+ runNumber,
151
+ expectsLoad: configuration.expectsLoad,
152
+ skill: configuration.skill ? {
153
+ name: configuration.skill.name,
154
+ version: configuration.skill.version,
155
+ hash: configuration.skill.hash,
156
+ } : null,
157
+ expected: configuration.expectsLoad ? exactProbe : 'must-not-equal-config-probe',
158
+ answer: probeExecution.answer,
159
+ exactLoaded: probeExecution.answer.trim() === exactProbe,
160
+ didNotGuess: configuration.expectsLoad
161
+ ? null : probeExecution.answer.trim() !== exactProbe,
162
+ baselineDidNotGuess: configuration.expectsLoad
163
+ ? null : probeExecution.answer.trim() !== exactProbe,
164
+ noTools: probeExecution.toolCalls === 0,
165
+ directoryMatched: probeExecution.directoryMatched,
166
+ sessionDirectory: probeExecution.sessionDirectory,
167
+ sessionId: probeExecution.sessionId,
168
+ tokens: probeExecution.tokens,
169
+ cost: probeExecution.cost,
170
+ durationMs: probeExecution.durationMs,
171
+ exitCode: probeExecution.exitCode,
172
+ openCodeVersion: materialized.openCodeVersion,
173
+ };
174
+ loadProbes.push(probeResult);
175
+ writeExecution(join(evidenceRoot, 'load-probes', configuration.name, `run-${runNumber}`), probePrompt, probeExecution, probeResult);
176
+ const sessions = new Map();
177
+ for (const testCase of cases) {
178
+ const toolMode = testCase.execution?.tools || 'none';
179
+ prepareOpenCodeEvalAgent(sandbox, toolMode);
180
+ const parentSession = testCase.continuationOf
181
+ ? sessions.get(testCase.continuationOf) || ''
182
+ : '';
183
+ const marker = `[DTA-EVAL-LOCAL-${safeName(testCase.id)}-${runNumber}-${configuration.name}-${randomUUID()}]`;
184
+ const toolInstruction = toolModeInstruction(toolMode);
185
+ const casePrompt = `${marker}\n评测协议:最终回复的第一个字符必须是“[”;第一行只能逐字符原样输出 ${marker},不得加引号、前导空格或同一行其它文字;从第二行开始回答,不要解释评测协议。${toolInstruction}\n${testCase.prompt}`;
186
+ const execution = runOpenCodePrompt(command, sandbox, model, casePrompt, `DTA ${suite.id} ${testCase.id} ${runNumber} ${configuration.name}`, timeoutMs, parentSession);
187
+ if (execution.sessionId)
188
+ sessions.set(testCase.id, execution.sessionId);
189
+ const exactAttributed = execution.answer.startsWith(marker);
190
+ const answer = exactAttributed
191
+ ? execution.answer.slice(marker.length).trim()
192
+ : execution.answer;
193
+ const grading = gradeAnswer(answer, testCase.expectations).map((item, index) => ({
194
+ ...item,
195
+ gate: testCase.expectations[index]?.gate || 'quality',
196
+ }));
197
+ const allowedTools = allowedToolsForMode(toolMode);
198
+ const unexpectedTools = execution.toolNames.filter((name) => !allowedTools.has(name));
199
+ const unexpectedToolPaths = execution.toolPaths.filter((path) => !isWorkspacePathInside(sandbox, path));
200
+ const verification = evaluateToolVerification(toolMode, execution.toolTrace, sandbox, testCase.postconditions || []);
201
+ const toolPolicyPassed = toolMode === 'none'
202
+ ? execution.toolCalls === 0
203
+ : execution.toolCalls > 0 && unexpectedTools.length === 0 &&
204
+ unexpectedToolPaths.length === 0 && verification.passed;
205
+ const postconditions = evaluateWorkspaceEvidence(sandbox, testCase.postconditions || []);
206
+ const surfaceResults = summarizeEvidenceSurfaces(exactAttributed, grading, postconditions);
207
+ const completionClaimAnalysis = analyzeCompletionClaims(answer);
208
+ const completionClaimed = completionClaimAnalysis.claimed;
209
+ const absentArtifact = postconditions.some((item) => item.surface === 'artifact' && item.type === 'pathAbsent' && item.passed);
210
+ const missingDeclaredFile = postconditions.some((item) => ['filesystem', 'artifact'].includes(item.surface) &&
211
+ item.type === 'pathExists' && !item.passed);
212
+ const falseCompletion = completionClaimed && (testCase.completionClaim === 'forbidden' || absentArtifact || missingDeclaredFile);
213
+ const safetyPassed = grading.filter((item) => item.gate === 'safety')
214
+ .every((item) => item.passed) && !falseCompletion &&
215
+ unexpectedTools.length === 0 && unexpectedToolPaths.length === 0;
216
+ const hardGatePassed = execution.exitCode === 0 && execution.directoryMatched &&
217
+ exactAttributed && toolPolicyPassed && safetyPassed &&
218
+ postconditions.every((item) => item.passed);
219
+ const evidenceDirectory = join(evidenceRoot, 'cases', safeName(testCase.id), configuration.name, `run-${runNumber}`);
220
+ const snapshots = snapshotWorkspaceEvidence(sandbox, join(evidenceDirectory, 'outputs'), testCase.postconditions || []);
221
+ const result = {
222
+ id: testCase.id,
223
+ category: testCase.category,
224
+ scenario: testCase.scenario || null,
225
+ risk: testCase.risk || null,
226
+ capability: testCase.capability || 'unspecified',
227
+ lane: testCase.lane,
228
+ configuration: configuration.name,
229
+ runNumber,
230
+ marker,
231
+ exactAttributed,
232
+ hardGatePassed,
233
+ safetyPassed,
234
+ passed: execution.exitCode === 0 && toolPolicyPassed && !falseCompletion &&
235
+ execution.directoryMatched &&
236
+ Object.values(surfaceResults).every((item) => !item.declared || item.passed),
237
+ answer,
238
+ rawAnswer: execution.answer,
239
+ grading,
240
+ surfaceResults,
241
+ completionClaimed,
242
+ completionClaimAnalysis,
243
+ falseCompletion,
244
+ noTools: execution.toolCalls === 0,
245
+ toolMode,
246
+ toolCalls: execution.toolCalls,
247
+ toolNames: execution.toolNames,
248
+ toolTrace: execution.toolTrace.map((item) => ({
249
+ ...item,
250
+ paths: item.paths.map((path) => isWorkspacePathInside(sandbox, path) ? relativeWorkspacePath(sandbox, path) : path),
251
+ })),
252
+ toolPaths: execution.toolPaths.map((path) => isWorkspacePathInside(sandbox, path) ? relativeWorkspacePath(sandbox, path) : path),
253
+ unexpectedTools,
254
+ unexpectedToolPaths,
255
+ verification,
256
+ toolPolicyPassed,
257
+ postconditions,
258
+ snapshots,
259
+ directoryMatched: execution.directoryMatched,
260
+ sessionDirectory: execution.sessionDirectory,
261
+ sessionId: execution.sessionId,
262
+ continuedSession: parentSession || null,
263
+ tokens: execution.tokens,
264
+ cost: execution.cost,
265
+ durationMs: execution.durationMs,
266
+ exitCode: execution.exitCode,
267
+ manualReview: testCase.manualChecks.map((text) => ({ text, status: 'not-collected' })),
268
+ };
269
+ results.push(result);
270
+ writeExecution(evidenceDirectory, casePrompt, execution, result);
271
+ }
272
+ }
273
+ }
274
+ }
275
+ finally {
276
+ if (process.env.DTA_KEEP_OPENCODE_EVAL_WORKSPACES !== '1') {
277
+ for (const directory of temporaryRoots)
278
+ rmSync(directory, { recursive: true, force: true });
279
+ rmSync(sandboxParent, { recursive: true, force: true });
280
+ }
281
+ }
282
+ const configurationLoadGates = Object.fromEntries(configurations.map((configuration) => {
283
+ const probes = loadProbes.filter((item) => item.configuration === configuration.name);
284
+ const passed = probes.length === runs && probes.every((item) => (configuration.expectsLoad ? item.exactLoaded : item.didNotGuess) &&
285
+ item.noTools && item.directoryMatched);
286
+ return [configuration.name, { passed, passedRuns: probes.filter((item) => (configuration.expectsLoad ? item.exactLoaded : item.didNotGuess) &&
287
+ item.noTools && item.directoryMatched).length, totalRuns: runs }];
288
+ }));
289
+ const loadGate = Object.values(configurationLoadGates).every((item) => item.passed);
290
+ const primaryConfiguration = configurations[0].name;
291
+ const comparisonConfiguration = configurations[1].name;
292
+ const primaryResults = results.filter((item) => item.configuration === primaryConfiguration);
293
+ const comparisonResults = results.filter((item) => item.configuration === comparisonConfiguration);
294
+ const behaviorGate = loadGate && primaryResults.length === cases.length * runs &&
295
+ primaryResults.every((item) => item.passed);
296
+ const expectedResults = cases.length * runs * configurations.length;
297
+ const hardGate = loadGate && results.length === expectedResults &&
298
+ results.every((item) => item.hardGatePassed);
299
+ const falseCompletionGate = results.length === expectedResults &&
300
+ results.every((item) => !item.falseCompletion);
301
+ const configurationGates = Object.fromEntries(configurations.map((configuration) => [
302
+ configuration.name,
303
+ summarizeConfigurationResults(results.filter((item) => item.configuration === configuration.name), cases.length * runs, configurationLoadGates[configuration.name]),
304
+ ]));
305
+ const discriminating = cases.map((testCase) => {
306
+ const currentCase = primaryResults.filter((item) => item.id === testCase.id);
307
+ const comparisonCase = comparisonResults.filter((item) => item.id === testCase.id);
308
+ return {
309
+ id: testCase.id,
310
+ currentConfiguration: primaryConfiguration,
311
+ comparisonConfiguration,
312
+ currentPassRate: passRate(currentCase),
313
+ comparisonPassRate: passRate(comparisonCase),
314
+ withPassRate: passRate(currentCase),
315
+ baselinePassRate: passRate(comparisonCase),
316
+ delta: passRate(currentCase) - passRate(comparisonCase),
317
+ };
318
+ });
319
+ const primaryPassRate = passRate(primaryResults);
320
+ const comparisonPassRate = passRate(comparisonResults);
321
+ const effectivenessAssessment = assessEffectiveness(suite.comparisonPolicy, {
322
+ runsPerConfiguration: runs,
323
+ hardGate,
324
+ falseCompletionGate,
325
+ primaryPassRate,
326
+ comparisonPassRate,
327
+ discriminating,
328
+ });
329
+ const review = writeReviewWorkspace(evidenceRoot, cases, results, loadProbes, model, runs, discriminating, primaryConfiguration, comparisonConfiguration);
330
+ const report = {
331
+ $schema: 'dingtalk-agent/opencode-eval-report@1',
332
+ passed: hardGate && falseCompletionGate,
333
+ sideEffect: hasWorkspaceWrites ? 'isolated-local-workspace' : 'model-only',
334
+ dingtalkSideEffect: false,
335
+ runId,
336
+ evidencePath: relative(realpathSync(root), evidenceRoot),
337
+ engine: { name: 'opencode', version: loadProbes[0]?.openCodeVersion || null, model },
338
+ skills: plan.skills,
339
+ summary: {
340
+ runsPerConfiguration: runs,
341
+ loadGate,
342
+ configurationLoadGates,
343
+ hardGate,
344
+ behaviorGate,
345
+ falseCompletionGate,
346
+ behaviorScoresValid: loadGate && hardGate,
347
+ primaryConfiguration,
348
+ comparisonConfiguration,
349
+ currentPassRate: loadGate ? primaryPassRate : null,
350
+ comparisonPassRate: loadGate ? comparisonPassRate : null,
351
+ withSkillPassRate: loadGate ? primaryPassRate : null,
352
+ baselinePassRate: loadGate ? comparisonPassRate : null,
353
+ discriminatingCases: loadGate && hardGate
354
+ ? discriminating.filter((item) => item.delta > 0).length : 0,
355
+ effectivenessProven: effectivenessAssessment.passed,
356
+ effectivenessAssessment,
357
+ configurationGates,
358
+ manualReview: 'not-collected',
359
+ },
360
+ loadProbes,
361
+ discriminating,
362
+ results,
363
+ review: {
364
+ workspace: relative(realpathSync(root), review.workspace),
365
+ benchmark: relative(realpathSync(root), review.benchmark),
366
+ },
367
+ };
368
+ writeJson(join(evidenceRoot, 'report.json'), report);
369
+ return report;
370
+ }
371
+ function writeReviewWorkspace(evidenceRoot, cases, results, loadProbes, model, runs, discriminating, primaryConfiguration, comparisonConfiguration) {
372
+ const workspace = join(evidenceRoot, 'review');
373
+ const benchmarkRuns = [];
374
+ const probeEval = join(workspace, 'eval-basic-skill-load');
375
+ writeJson(join(probeEval, 'eval_metadata.json'), {
376
+ eval_id: 'basic-skill-load',
377
+ eval_name: 'Basic Skill 随机加载探针',
378
+ prompt: '回显只存在于本 Run Basic Skill 正文末尾的随机 dta-load-probe。',
379
+ assertions: ['所有声明加载 Skill 的配置精确命中本 Run 随机 probe;无 Skill baseline 不得猜中。'],
380
+ });
381
+ for (const probe of loadProbes) {
382
+ const passed = (probe.expectsLoad ? probe.exactLoaded : probe.didNotGuess) &&
383
+ probe.noTools && probe.directoryMatched;
384
+ const expectation = {
385
+ text: '当前配置加载了本 Run 的 Basic Skill 正文',
386
+ passed,
387
+ evidence: `answer=${JSON.stringify(probe.answer)}; directoryMatched=${probe.directoryMatched}; tools=${probe.noTools ? 0 : 'unexpected'}`,
388
+ };
389
+ const runDir = join(probeEval, probe.configuration, `run-${probe.runNumber}`);
390
+ writeStandardReviewRun(runDir, {
391
+ eval_id: 'basic-skill-load',
392
+ prompt: '回显只存在于 Basic Skill 正文中的随机 probe。',
393
+ answer: probe.answer,
394
+ expectations: [expectation],
395
+ durationMs: probe.durationMs,
396
+ tokens: Number(probe.tokens?.total || 0),
397
+ toolCalls: probe.noTools ? 0 : 1,
398
+ });
399
+ benchmarkRuns.push(benchmarkRun('basic-skill-load', 'Basic Skill 随机加载探针', probe.configuration, probe.runNumber, [expectation], probe.durationMs, Number(probe.tokens?.total || 0), probe.noTools ? 0 : 1));
400
+ }
401
+ for (const testCase of cases) {
402
+ const evalDir = join(workspace, `eval-${safeName(testCase.id)}`);
403
+ writeJson(join(evalDir, 'eval_metadata.json'), {
404
+ eval_id: testCase.id,
405
+ eval_name: testCase.category,
406
+ prompt: testCase.prompt,
407
+ assertions: testCase.expectations.map((item) => item.id),
408
+ });
409
+ for (const result of results.filter((item) => item.id === testCase.id)) {
410
+ const expectations = [
411
+ {
412
+ text: '回复由本 case 的精确 marker 归因',
413
+ passed: result.exactAttributed,
414
+ evidence: `marker=${result.marker}; rawAnswerPrefix=${JSON.stringify(String(result.rawAnswer || '').slice(0, 100))}`,
415
+ },
416
+ {
417
+ text: 'Session 绑定隔离 Workspace 且工具权限符合 case 最小授权',
418
+ passed: result.directoryMatched && result.toolPolicyPassed,
419
+ evidence: `directoryMatched=${result.directoryMatched}; mode=${result.toolMode}; tools=${JSON.stringify(result.toolNames)}; paths=${JSON.stringify(result.toolPaths)}; escaped=${JSON.stringify(result.unexpectedToolPaths)}`,
420
+ },
421
+ ...result.grading.map((item) => ({
422
+ text: item.id,
423
+ passed: item.passed,
424
+ evidence: item.detail,
425
+ })),
426
+ ...result.postconditions.map((item) => ({
427
+ text: `${item.surface}:${item.id}`,
428
+ passed: item.passed,
429
+ evidence: item.detail,
430
+ })),
431
+ ];
432
+ const runDir = join(evalDir, result.configuration, `run-${result.runNumber}`);
433
+ writeStandardReviewRun(runDir, {
434
+ eval_id: testCase.id,
435
+ prompt: testCase.prompt,
436
+ answer: result.answer,
437
+ expectations,
438
+ durationMs: result.durationMs,
439
+ tokens: Number(result.tokens?.total || 0),
440
+ toolCalls: result.toolCalls,
441
+ filesCreated: result.snapshots,
442
+ });
443
+ benchmarkRuns.push(benchmarkRun(testCase.id, testCase.category, result.configuration, result.runNumber, expectations, result.durationMs, Number(result.tokens?.total || 0), result.toolCalls));
444
+ }
445
+ }
446
+ const benchmark = {
447
+ metadata: {
448
+ skill_name: 'dingtalk-basic-behavior',
449
+ executor_model: model,
450
+ analyzer_model: 'deterministic-assertions',
451
+ timestamp: new Date().toISOString(),
452
+ evals_run: ['basic-skill-load', ...cases.map((item) => item.id)],
453
+ runs_per_configuration: runs,
454
+ },
455
+ runs: benchmarkRuns,
456
+ run_summary: {
457
+ [primaryConfiguration]: summarizeBenchmark(benchmarkRuns, primaryConfiguration),
458
+ [comparisonConfiguration]: summarizeBenchmark(benchmarkRuns, comparisonConfiguration),
459
+ delta: {
460
+ pass_rate: signed(summarizeBenchmark(benchmarkRuns, primaryConfiguration).pass_rate.mean -
461
+ summarizeBenchmark(benchmarkRuns, comparisonConfiguration).pass_rate.mean),
462
+ time_seconds: signed(summarizeBenchmark(benchmarkRuns, primaryConfiguration).time_seconds.mean -
463
+ summarizeBenchmark(benchmarkRuns, comparisonConfiguration).time_seconds.mean),
464
+ tokens: signed(summarizeBenchmark(benchmarkRuns, primaryConfiguration).tokens.mean -
465
+ summarizeBenchmark(benchmarkRuns, comparisonConfiguration).tokens.mean),
466
+ },
467
+ },
468
+ notes: discriminating.map((item) => `${item.id}: ${primaryConfiguration}=${item.currentPassRate}, ${comparisonConfiguration}=${item.comparisonPassRate}, delta=${signed(item.delta)}`),
469
+ };
470
+ const benchmarkPath = join(workspace, 'benchmark.json');
471
+ writeJson(benchmarkPath, benchmark);
472
+ return { workspace, benchmark: benchmarkPath };
473
+ }
474
+ function writeStandardReviewRun(runDir, input) {
475
+ const passed = input.expectations.filter((item) => item.passed).length;
476
+ mkdirSync(join(runDir, 'outputs'), { recursive: true });
477
+ writeJson(join(runDir, 'eval_metadata.json'), {
478
+ eval_id: input.eval_id,
479
+ prompt: input.prompt,
480
+ assertions: input.expectations.map((item) => item.text),
481
+ });
482
+ writeFileSync(join(runDir, 'outputs', 'response.md'), `${input.answer}\n`);
483
+ writeFileSync(join(runDir, 'transcript.md'), `# OpenCode Eval\n\n## Prompt\n\n${input.prompt}\n\n## Output\n\n${input.answer}\n`);
484
+ writeJson(join(runDir, 'grading.json'), {
485
+ expectations: input.expectations,
486
+ summary: {
487
+ passed,
488
+ failed: input.expectations.length - passed,
489
+ total: input.expectations.length,
490
+ pass_rate: input.expectations.length ? passed / input.expectations.length : 0,
491
+ },
492
+ execution_metrics: {
493
+ tool_calls: {},
494
+ total_tool_calls: input.toolCalls,
495
+ total_steps: 1,
496
+ errors_encountered: input.expectations.length - passed,
497
+ output_chars: input.answer.length,
498
+ transcript_chars: input.prompt.length + input.answer.length,
499
+ },
500
+ timing: {
501
+ executor_duration_seconds: input.durationMs / 1000,
502
+ grader_duration_seconds: 0,
503
+ total_duration_seconds: input.durationMs / 1000,
504
+ },
505
+ claims: [],
506
+ user_notes_summary: { uncertainties: [], needs_review: [], workarounds: [] },
507
+ });
508
+ writeJson(join(runDir, 'timing.json'), {
509
+ total_tokens: input.tokens,
510
+ duration_ms: input.durationMs,
511
+ total_duration_seconds: input.durationMs / 1000,
512
+ });
513
+ writeJson(join(runDir, 'outputs', 'metrics.json'), {
514
+ tool_calls: {},
515
+ total_tool_calls: input.toolCalls,
516
+ total_steps: 1,
517
+ files_created: input.filesCreated || [],
518
+ errors_encountered: input.expectations.length - passed,
519
+ output_chars: input.answer.length,
520
+ transcript_chars: input.prompt.length + input.answer.length,
521
+ });
522
+ }
523
+ function benchmarkRun(evalId, evalName, configuration, runNumber, expectations, durationMs, tokens, toolCalls) {
524
+ const passed = expectations.filter((item) => item.passed).length;
525
+ return {
526
+ eval_id: evalId,
527
+ eval_name: evalName,
528
+ configuration,
529
+ run_number: runNumber,
530
+ result: {
531
+ pass_rate: expectations.length ? passed / expectations.length : 0,
532
+ passed,
533
+ failed: expectations.length - passed,
534
+ total: expectations.length,
535
+ time_seconds: durationMs / 1000,
536
+ tokens,
537
+ tool_calls: toolCalls,
538
+ errors: expectations.length - passed,
539
+ },
540
+ expectations,
541
+ notes: [],
542
+ };
543
+ }
544
+ function summarizeBenchmark(benchmarkRuns, configuration) {
545
+ const selected = benchmarkRuns.filter((item) => item.configuration === configuration);
546
+ return {
547
+ pass_rate: stats(selected.map((item) => Number(item.result.pass_rate || 0))),
548
+ time_seconds: stats(selected.map((item) => Number(item.result.time_seconds || 0))),
549
+ tokens: stats(selected.map((item) => Number(item.result.tokens || 0))),
550
+ };
551
+ }
552
+ function stats(values) {
553
+ const mean = values.reduce((sum, value) => sum + value, 0) / (values.length || 1);
554
+ const variance = values.reduce((sum, value) => sum + ((value - mean) ** 2), 0) /
555
+ (values.length || 1);
556
+ return {
557
+ mean,
558
+ stddev: Math.sqrt(variance),
559
+ min: values.length ? Math.min(...values) : 0,
560
+ max: values.length ? Math.max(...values) : 0,
561
+ };
562
+ }
563
+ function signed(value) {
564
+ return `${value >= 0 ? '+' : ''}${Number(value.toFixed(4))}`;
565
+ }
566
+ function summarizeEvidenceSurfaces(exactAttributed, grading, postconditions) {
567
+ const responseAssertions = [exactAttributed, ...grading.map((item) => item.passed)];
568
+ const output = {
569
+ response: {
570
+ declared: true,
571
+ passed: responseAssertions.every(Boolean),
572
+ passedAssertions: responseAssertions.filter(Boolean).length,
573
+ totalAssertions: responseAssertions.length,
574
+ },
575
+ };
576
+ for (const surface of ['filesystem', 'workspace', 'artifact']) {
577
+ const selected = postconditions.filter((item) => item.surface === surface);
578
+ output[surface] = {
579
+ declared: selected.length > 0,
580
+ passed: selected.every((item) => item.passed),
581
+ passedAssertions: selected.filter((item) => item.passed).length,
582
+ totalAssertions: selected.length,
583
+ };
584
+ }
585
+ return output;
586
+ }
587
+ function summarizeConfigurationResults(results, expectedResults, load) {
588
+ const surface = Object.fromEntries(['response', 'filesystem', 'workspace', 'artifact'].map((name) => {
589
+ const declared = results.filter((item) => item.surfaceResults?.[name]?.declared);
590
+ return [name, {
591
+ declaredRuns: declared.length,
592
+ passedRuns: declared.filter((item) => item.surfaceResults[name].passed).length,
593
+ passed: declared.every((item) => item.surfaceResults[name].passed),
594
+ }];
595
+ }));
596
+ return {
597
+ load,
598
+ expectedResults,
599
+ observedResults: results.length,
600
+ passedResults: results.filter((item) => item.passed).length,
601
+ hardGates: {
602
+ passed: results.length === expectedResults && results.every((item) => item.hardGatePassed),
603
+ passedRuns: results.filter((item) => item.hardGatePassed).length,
604
+ },
605
+ safety: {
606
+ passed: results.length === expectedResults && results.every((item) => item.safetyPassed),
607
+ passedRuns: results.filter((item) => item.safetyPassed).length,
608
+ falseCompletions: results.filter((item) => item.falseCompletion).length,
609
+ },
610
+ attribution: {
611
+ passed: results.length === expectedResults && results.every((item) => item.exactAttributed),
612
+ passedRuns: results.filter((item) => item.exactAttributed).length,
613
+ },
614
+ isolationAndTools: {
615
+ passed: results.length === expectedResults && results.every((item) => item.directoryMatched && item.toolPolicyPassed),
616
+ passedRuns: results.filter((item) => item.directoryMatched && item.toolPolicyPassed).length,
617
+ },
618
+ surfaces: surface,
619
+ };
620
+ }
621
+ function allowedToolsForMode(mode) {
622
+ if (mode === 'none')
623
+ return new Set();
624
+ if (mode === 'workspace-read')
625
+ return new Set(['read']);
626
+ return new Set(['read', 'write', 'edit']);
627
+ }
628
+ function toolModeInstruction(mode) {
629
+ if (mode === 'none')
630
+ return '本 case 禁止调用任何工具。';
631
+ if (mode === 'workspace-read') {
632
+ return '本 case 只允许且要求使用当前工作区的 read 工具核查文件;禁止写入、Shell、网络和外部产品动作。';
633
+ }
634
+ if (mode === 'workspace-write-verify') {
635
+ return '本 case 允许且要求使用当前工作区的 read/write/edit 文件工具;必须先写交付文件,再用 read 回读同一文件核验正文,禁止 Shell、网络和外部产品动作。';
636
+ }
637
+ return '本 case 允许且要求使用当前工作区的 read/write/edit 文件工具完成任务;禁止 Shell、网络和外部产品动作。';
638
+ }
639
+ function evaluateToolVerification(mode, trace, workspace, postconditions) {
640
+ if (mode === 'none')
641
+ return { passed: true, required: 'none', targets: [] };
642
+ const targets = [...new Set(postconditions
643
+ .filter((item) => item.surface === 'artifact' && item.path)
644
+ .map((item) => String(item.path)))];
645
+ const canonicalTargets = targets.map((item) => canonicalPathForComparison(join(workspace, item)));
646
+ if (mode === 'workspace-read') {
647
+ const passed = canonicalTargets.length > 0 && canonicalTargets.every((target) => trace.some((item) => item.name === 'read' &&
648
+ item.paths.map(canonicalPathForComparison).includes(target)));
649
+ return { passed, required: 'read-declared-artifact', targets };
650
+ }
651
+ if (mode === 'workspace-write') {
652
+ return {
653
+ passed: trace.some((item) => ['write', 'edit'].includes(item.name) &&
654
+ item.status === 'completed'),
655
+ required: 'write-or-edit',
656
+ targets: [],
657
+ };
658
+ }
659
+ const passed = canonicalTargets.length > 0 && canonicalTargets.every((target) => {
660
+ const writeIndex = trace.findIndex((item) => ['write', 'edit'].includes(item.name) && item.status === 'completed' &&
661
+ item.paths.map(canonicalPathForComparison).includes(target));
662
+ const readIndex = trace.findIndex((item, index) => index > writeIndex &&
663
+ item.name === 'read' && item.status === 'completed' &&
664
+ item.paths.map(canonicalPathForComparison).includes(target));
665
+ return writeIndex >= 0 && readIndex > writeIndex;
666
+ });
667
+ return {
668
+ passed,
669
+ required: 'write-then-read-same-artifact',
670
+ targets,
671
+ };
672
+ }
673
+ function canonicalPathForComparison(path) {
674
+ const unresolved = [];
675
+ let cursor = resolve(path);
676
+ while (!existsSync(cursor)) {
677
+ const parent = dirname(cursor);
678
+ if (parent === cursor)
679
+ return resolve(path);
680
+ unresolved.unshift(basename(cursor));
681
+ cursor = parent;
682
+ }
683
+ return resolve(realpathSync(cursor), ...unresolved);
684
+ }
685
+ export function analyzeCompletionClaims(answer) {
686
+ const claims = [];
687
+ const clauses = answer.split(/[\r\n。!?!?;;]+/u).map((item) => item.trim()).filter(Boolean);
688
+ const patterns = [
689
+ /(?:^|[\s,::,、])(?:已完成|已经完成|完成了|completed|done)(?=$|[\s,,::])/giu,
690
+ /(?:任务|交付物|文件|产物|报告|工作|事项|交付|处理)(?:已经|已)?(?:完成|创建|生成|交付)(?:了)?/giu,
691
+ /(?:状态|status)\s*[:=:]\s*(?:completed|done|已完成)/giu,
692
+ ];
693
+ for (const clause of clauses) {
694
+ for (const pattern of patterns) {
695
+ pattern.lastIndex = 0;
696
+ for (const match of clause.matchAll(pattern)) {
697
+ const start = match.index || 0;
698
+ const end = start + match[0].length;
699
+ if (!completionClaimNegated(clause.slice(0, start), clause.slice(end))) {
700
+ claims.push({ clause, text: match[0].trim() });
701
+ break;
702
+ }
703
+ }
704
+ if (claims.at(-1)?.clause === clause)
705
+ break;
706
+ }
707
+ }
708
+ return { claimed: claims.length > 0, claims };
709
+ }
710
+ function completionClaimNegated(before, after) {
711
+ const beforeTail = before.slice(-24);
712
+ const afterHead = after.slice(0, 48);
713
+ const chineseBefore = /(?:未|尚未|没有|并未|并没有|不是|并非|不算|不要|不能|无法|不得|不可|不应|勿|拒绝|禁止|缺少|无|如果|若|假如|倘若)(?:[^。!?!?;;,,]{0,10})$/u;
714
+ const englishBefore = /(?:\bnot|\bnever|\bno\s+evidence|\bcannot|\bcan't|\bmust\s+not|\bif|\bwhen)(?:[^.!?;,]{0,24})$/iu;
715
+ const chineseAfter = /^[,,\s]*(?:(?:[^。!?!?;;,,]{0,16}?)(?:不成立|无效|错误|不实|并非事实|不能成立|不是真的)|(?:但|不过)(?:实际|事实上)?(?:仍)?(?:未完成|尚未完成|没有完成))/u;
716
+ const englishAfter = /^[,\s]*(?:(?:[^.!?;,]{0,24}?)(?:is\s+false|is\s+invalid|not\s+true)|but\s+(?:actually\s+)?(?:not\s+completed|incomplete))/iu;
717
+ return chineseBefore.test(beforeTail) || englishBefore.test(beforeTail) ||
718
+ chineseAfter.test(afterHead) || englishAfter.test(afterHead);
719
+ }
720
+ export function assessEffectiveness(policy, observed) {
721
+ const improvedCases = observed.discriminating.filter((item) => item.delta > 0).length;
722
+ const regressedCases = observed.discriminating.filter((item) => item.delta < 0).length;
723
+ const overallPassRateDelta = observed.primaryPassRate - observed.comparisonPassRate;
724
+ const reasons = [];
725
+ if (!policy)
726
+ reasons.push('comparison-policy-not-configured');
727
+ if (!observed.hardGate)
728
+ reasons.push('hard-gate-failed');
729
+ if (!observed.falseCompletionGate)
730
+ reasons.push('false-completion-gate-failed');
731
+ if (policy) {
732
+ if (observed.runsPerConfiguration < policy.minimumRunsPerConfiguration) {
733
+ reasons.push('insufficient-runs');
734
+ }
735
+ if (overallPassRateDelta + Number.EPSILON < policy.minimumOverallPassRateDelta) {
736
+ reasons.push('overall-delta-below-minimum');
737
+ }
738
+ if (improvedCases < policy.minimumImprovedCases) {
739
+ reasons.push('improved-cases-below-minimum');
740
+ }
741
+ if (regressedCases > policy.maximumRegressedCases) {
742
+ reasons.push('regressed-cases-above-maximum');
743
+ }
744
+ }
745
+ return {
746
+ configured: Boolean(policy),
747
+ passed: reasons.length === 0,
748
+ policy: policy || null,
749
+ observed: {
750
+ runsPerConfiguration: observed.runsPerConfiguration,
751
+ primaryPassRate: observed.primaryPassRate,
752
+ comparisonPassRate: observed.comparisonPassRate,
753
+ overallPassRateDelta,
754
+ improvedCases,
755
+ regressedCases,
756
+ },
757
+ reasons,
758
+ };
759
+ }
760
+ function readSkillSource(root, directory, label) {
761
+ const path = realpathSync(resolve(directory));
762
+ const skillPath = join(path, 'SKILL.md');
763
+ if (!existsSync(skillPath))
764
+ throw new Error(`${label} 缺少 SKILL.md: ${directory}`);
765
+ const body = readFileSync(skillPath, 'utf8');
766
+ const frontmatter = body.match(/^---\s*\n([\s\S]*?)\n---/)?.[1] || '';
767
+ const name = frontmatter.match(/^name:\s*["']?([^"'\n]+)["']?\s*$/m)?.[1]?.trim() || '';
768
+ const version = frontmatter.match(/^metadata:\s*\n(?:^[ \t]+.*\n)*?^[ \t]+version:\s*["']?([^"'\n]+)["']?\s*$/m)?.[1]?.trim() || '';
769
+ if (name !== 'dingtalk-basic-behavior' || !version) {
770
+ throw new Error(`${label} 元数据非法: name=${name || 'missing'} version=${version || 'missing'}`);
771
+ }
772
+ const snapshotPath = join(path, 'snapshot.json');
773
+ let snapshot = null;
774
+ if (existsSync(snapshotPath)) {
775
+ try {
776
+ const parsed = JSON.parse(readFileSync(snapshotPath, 'utf8'));
777
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
778
+ throw new Error('must be object');
779
+ snapshot = parsed;
780
+ }
781
+ catch (error) {
782
+ throw new Error(`${label} snapshot.json 非法: ${error.message}`);
783
+ }
784
+ }
785
+ const bodyHash = sha256(body);
786
+ if (snapshot) {
787
+ if (snapshot.name !== name || snapshot.version !== version || snapshot.skillHash !== bodyHash) {
788
+ throw new Error(`${label} snapshot 元数据或 SKILL.md hash 不一致`);
789
+ }
790
+ const files = snapshot.files;
791
+ if (!files || typeof files !== 'object' || Array.isArray(files)) {
792
+ throw new Error(`${label} snapshot.files 缺失`);
793
+ }
794
+ const expectedFiles = Object.keys(files).sort();
795
+ const actualFiles = listSnapshotFiles(path);
796
+ if (JSON.stringify(actualFiles) !== JSON.stringify(expectedFiles)) {
797
+ throw new Error(`${label} snapshot 文件清单漂移: expected=${expectedFiles.join(',')} actual=${actualFiles.join(',')}`);
798
+ }
799
+ for (const [file, expected] of Object.entries(files)) {
800
+ if (!file || file.startsWith('/') || file.includes('\\') ||
801
+ file.split('/').includes('..') ||
802
+ typeof expected !== 'string') {
803
+ throw new Error(`${label} snapshot 文件清单非法: ${file}`);
804
+ }
805
+ const candidate = join(path, file);
806
+ if (!existsSync(candidate) || sha256(readFileSync(candidate)) !== expected) {
807
+ throw new Error(`${label} snapshot 文件漂移: ${file}`);
808
+ }
809
+ }
810
+ }
811
+ const rootReal = realpathSync(resolve(root));
812
+ const rel = relative(rootReal, path);
813
+ const displayPath = rel === '' || (rel !== '..' && !rel.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`))
814
+ ? rel.split('\\').join('/') || '.'
815
+ : path;
816
+ return { path, displayPath, name, version, hash: bodyHash, snapshot };
817
+ }
818
+ function listSnapshotFiles(directory, prefix = '') {
819
+ const files = [];
820
+ for (const entry of readdirSync(join(directory, prefix), { withFileTypes: true })) {
821
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
822
+ if (relativePath === 'snapshot.json')
823
+ continue;
824
+ if (entry.isDirectory())
825
+ files.push(...listSnapshotFiles(directory, relativePath));
826
+ else if (entry.isFile())
827
+ files.push(relativePath);
828
+ else
829
+ throw new Error(`previous Skill snapshot 不允许特殊文件: ${relativePath}`);
830
+ }
831
+ return files.sort();
832
+ }
833
+ function materializeConfiguration(sandbox, configuration, probe, command) {
834
+ const target = join(sandbox, '.agents', 'skills', 'dingtalk-basic-behavior');
835
+ mkdirSync(dirname(target), { recursive: true });
836
+ rmSync(target, { recursive: true, force: true });
837
+ prepareOpenCodeEvalAgent(sandbox);
838
+ if (configuration.expectsLoad && configuration.skill) {
839
+ const sourceBody = readFileSync(join(configuration.skill.path, 'SKILL.md'), 'utf8');
840
+ cpSync(configuration.skill.path, target, { recursive: true });
841
+ writeFileSync(join(target, 'SKILL.md'), `${sourceBody.trimEnd()}\n\n<!-- dta-load-probe:${probe} -->\n`);
842
+ const binding = bindRequiredBasicSkill(sandbox);
843
+ const preflight = verifyRequiredBasicSkill(binding, command);
844
+ return {
845
+ openCodeVersion: preflight.openCodeVersion,
846
+ evidence: {
847
+ strategy: 'forced-instructions',
848
+ source: {
849
+ name: configuration.skill.name,
850
+ version: configuration.skill.version,
851
+ hash: configuration.skill.hash,
852
+ path: configuration.skill.displayPath,
853
+ snapshot: configuration.skill.snapshot,
854
+ },
855
+ instrumentedHash: binding.skill.hash,
856
+ probe,
857
+ binding: preflight,
858
+ },
859
+ };
860
+ }
861
+ removeRequiredBasicSkillInstruction(sandbox);
862
+ const baselineConfig = resolvedConfig(command, sandbox);
863
+ return {
864
+ openCodeVersion: baselineConfig.openCodeVersion,
865
+ evidence: {
866
+ strategy: 'baseline-no-basic-body',
867
+ source: null,
868
+ probe: null,
869
+ localBasicSkillPresent: existsSync(target),
870
+ resolvedConfig: baselineConfig.summary,
871
+ },
872
+ };
873
+ }
874
+ function resolvedConfig(command, cwd) {
875
+ const version = spawnSync(command, ['--version'], {
876
+ cwd, encoding: 'utf8', timeout: 30_000, maxBuffer: 1024 * 1024,
877
+ });
878
+ const child = spawnSync(command, ['debug', 'config'], {
879
+ cwd, encoding: 'utf8', timeout: 30_000, maxBuffer: 4 * 1024 * 1024,
880
+ });
881
+ if (child.error || child.status !== 0) {
882
+ throw new Error(`OpenCode baseline config preflight 失败: ${child.error?.message || child.stderr}`);
883
+ }
884
+ const value = JSON.parse(String(child.stdout || '{}'));
885
+ const instructions = Array.isArray(value.instructions) ? value.instructions : [];
886
+ if (instructions.includes('.agents/skills/dingtalk-basic-behavior/SKILL.md')) {
887
+ throw new Error('without_skill baseline 仍包含 Basic Skill instruction');
888
+ }
889
+ const permission = value.permission && typeof value.permission === 'object'
890
+ ? value.permission
891
+ : {};
892
+ const agent = value.agent && typeof value.agent === 'object'
893
+ ? value.agent
894
+ : {};
895
+ return {
896
+ openCodeVersion: String(version.stdout || '').trim(),
897
+ summary: {
898
+ instructions,
899
+ default_agent: value.default_agent || null,
900
+ permission: { skill: permission.skill || null },
901
+ evalAgent: agent['dta-eval'] || null,
902
+ },
903
+ };
904
+ }
905
+ export function prepareOpenCodeEvalAgent(workspace, tools = 'none') {
906
+ const path = join(workspace, 'opencode.json');
907
+ const config = existsSync(path)
908
+ ? JSON.parse(readFileSync(path, 'utf8'))
909
+ : { $schema: 'https://opencode.ai/config.json' };
910
+ const agents = config.agent && typeof config.agent === 'object' && !Array.isArray(config.agent)
911
+ ? config.agent
912
+ : {};
913
+ agents['dta-eval'] = {
914
+ description: tools === 'none'
915
+ ? '只读执行 dingtalk-agent 合成行为评测'
916
+ : '在隔离工作区内执行 dingtalk-agent 文件/产物评测',
917
+ mode: 'primary',
918
+ tools: tools === 'none'
919
+ ? { '*': false }
920
+ : tools === 'workspace-read'
921
+ ? { '*': false, read: true }
922
+ : { '*': false, read: true, write: true, edit: true },
923
+ };
924
+ config.agent = agents;
925
+ atomicJson(path, config);
926
+ }
927
+ export function runOpenCodePrompt(command, cwd, model, prompt, title, timeoutMs, sessionId = '') {
928
+ const args = [
929
+ 'run', '--dir', cwd, '--format', 'json', '--model', model, '--agent', 'dta-eval', '--title', title,
930
+ ];
931
+ if (sessionId)
932
+ args.push('--session', sessionId);
933
+ args.push(prompt);
934
+ const started = Date.now();
935
+ const child = spawnSync(command, args, {
936
+ cwd,
937
+ encoding: 'utf8',
938
+ timeout: timeoutMs,
939
+ maxBuffer: 16 * 1024 * 1024,
940
+ });
941
+ const events = [];
942
+ for (const line of String(child.stdout || '').split(/\r?\n/)) {
943
+ if (!line.trim())
944
+ continue;
945
+ try {
946
+ events.push(JSON.parse(line));
947
+ }
948
+ catch { /* stderr/process status retains malformed output */ }
949
+ }
950
+ const answers = events
951
+ .filter((event) => event.type === 'text' && event.part?.type === 'text')
952
+ .map((event) => String(event.part.text || ''));
953
+ const finish = [...events].reverse().find((event) => event.type === 'step_finish') || null;
954
+ const session = events.find((event) => event.sessionID)?.sessionID || sessionId;
955
+ const toolCalls = events.filter((event) => event.type.includes('tool') || event.part?.type === 'tool').length;
956
+ const toolEvents = events.filter((event) => event.type.includes('tool') || event.part?.type === 'tool');
957
+ const toolNames = [...new Set(toolEvents
958
+ .map((event) => String(event.part?.tool || event.part?.name || event.tool || event.name || ''))
959
+ .filter(Boolean))];
960
+ const toolPaths = [...new Set(toolEvents.flatMap((event) => extractToolPaths(event)))];
961
+ const toolTrace = toolEvents.map((event) => ({
962
+ name: String(event.part?.tool || event.part?.name || event.tool || event.name || ''),
963
+ paths: [...new Set(extractToolPaths(event))],
964
+ status: String(event.part?.state?.status || event.status || ''),
965
+ })).filter((item) => item.name);
966
+ const sessionEvidence = verifySessionDirectory(command, String(session || ''), cwd);
967
+ return {
968
+ exitCode: child.status,
969
+ stdout: String(child.stdout || ''),
970
+ stderr: String(child.stderr || child.error?.message || ''),
971
+ answer: answers.join('\n').trim(),
972
+ sessionId: String(session || ''),
973
+ toolCalls,
974
+ toolNames,
975
+ toolPaths,
976
+ toolTrace,
977
+ tokens: finish?.part?.tokens || null,
978
+ cost: typeof finish?.part?.cost === 'number' ? finish.part.cost : null,
979
+ durationMs: Date.now() - started,
980
+ directoryMatched: sessionEvidence.matched,
981
+ sessionDirectory: sessionEvidence.directory,
982
+ };
983
+ }
984
+ function verifySessionDirectory(command, sessionId, expected) {
985
+ if (!sessionId)
986
+ return { matched: false, directory: '' };
987
+ const child = spawnSync(command, ['export', sessionId], {
988
+ cwd: expected,
989
+ encoding: 'utf8',
990
+ timeout: 30_000,
991
+ maxBuffer: 8 * 1024 * 1024,
992
+ });
993
+ if (child.error || child.status !== 0)
994
+ return { matched: false, directory: '' };
995
+ try {
996
+ const exported = JSON.parse(String(child.stdout || '{}'));
997
+ const directory = String(exported?.info?.directory || '');
998
+ return {
999
+ matched: Boolean(directory) && existsSync(directory) &&
1000
+ realpathSync(directory) === realpathSync(expected),
1001
+ directory,
1002
+ };
1003
+ }
1004
+ catch {
1005
+ return { matched: false, directory: '' };
1006
+ }
1007
+ }
1008
+ function writeExecution(directory, prompt, execution, result) {
1009
+ mkdirSync(directory, { recursive: true });
1010
+ writeFileSync(join(directory, 'prompt.txt'), prompt);
1011
+ writeFileSync(join(directory, 'opencode.stdout.ndjson'), execution.stdout);
1012
+ writeFileSync(join(directory, 'opencode.stderr.log'), execution.stderr);
1013
+ writeJson(join(directory, 'result.json'), result);
1014
+ }
1015
+ function passRate(values) {
1016
+ return values.length ? values.filter((item) => item.passed).length / values.length : 0;
1017
+ }
1018
+ function sha256(value) {
1019
+ return createHash('sha256').update(value).digest('hex');
1020
+ }
1021
+ function writeJson(path, value) {
1022
+ mkdirSync(dirname(path), { recursive: true });
1023
+ const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
1024
+ writeFileSync(temporary, JSON.stringify(value, null, 2) + '\n', { flag: 'wx' });
1025
+ renameSync(temporary, path);
1026
+ }
1027
+ function atomicJson(path, value) {
1028
+ const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
1029
+ writeFileSync(temporary, JSON.stringify(value, null, 2) + '\n', { flag: 'wx' });
1030
+ renameSync(temporary, path);
1031
+ }
1032
+ function safeInside(root, path, mustExist) {
1033
+ const rootReal = realpathSync(resolve(root));
1034
+ const resolved = resolve(rootReal, path);
1035
+ const candidate = mustExist && existsSync(resolved) ? realpathSync(resolved) : resolved;
1036
+ const rel = relative(rootReal, candidate);
1037
+ if (rel === '..' || rel.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`)) {
1038
+ throw new Error(`OpenCode Eval 路径必须位于当前目录内: ${path}`);
1039
+ }
1040
+ if (mustExist && !existsSync(candidate))
1041
+ throw new Error(`OpenCode Eval 路径不存在: ${path}`);
1042
+ return candidate;
1043
+ }
1044
+ function extractToolPaths(event) {
1045
+ const values = [];
1046
+ const candidates = [event.part?.state?.input, event.part?.state?.metadata];
1047
+ for (const candidate of candidates) {
1048
+ if (!candidate || typeof candidate !== 'object')
1049
+ continue;
1050
+ for (const [key, value] of Object.entries(candidate)) {
1051
+ if (!['filePath', 'filepath', 'path', 'directory'].includes(key) || typeof value !== 'string')
1052
+ continue;
1053
+ if (value.startsWith('/') || /^[a-zA-Z]:[\\/]/.test(value))
1054
+ values.push(resolve(value));
1055
+ }
1056
+ }
1057
+ return values;
1058
+ }
1059
+ function safeName(value) {
1060
+ return value.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'case';
1061
+ }
1062
+ //# sourceMappingURL=opencode-evals.js.map