@danceiny/gotry 0.0.1-rc.16 → 0.0.1-rc.17

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 (229) hide show
  1. package/README.md +47 -13
  2. package/README.zh-CN.md +20 -10
  3. package/bin/gotry-booking-copilot.js +53 -0
  4. package/bin/gotry-bootstrap.js +111 -51
  5. package/bin/gotry-inner.js +442 -60
  6. package/bin/gotry-runtime-resolution.d.ts +27 -0
  7. package/bin/gotry-runtime-resolution.js +50 -0
  8. package/bin/gotry.js +1 -1
  9. package/cordis.gotry-patch.yml +5 -1
  10. package/dist/capabilities/agent-reach-deep.js +1 -1
  11. package/dist/capabilities/agent-reach.js +1 -1
  12. package/dist/capabilities/anything.js +1 -1
  13. package/dist/capabilities/artifacts.js +1 -1
  14. package/dist/capabilities/effect.js +1 -1
  15. package/dist/capabilities/fact-log.js +1 -1
  16. package/dist/capabilities/flyai.js +20 -6
  17. package/dist/capabilities/hbcli.js +1 -1
  18. package/dist/capabilities/incident-log.js +1 -1
  19. package/dist/capabilities/model-override.js +18 -0
  20. package/dist/capabilities/opensky.js +1 -1
  21. package/dist/capabilities/resilience.js +1 -1
  22. package/dist/capabilities/session/action-cache.js +1 -1
  23. package/dist/capabilities/session/adapters/ctrip-flight.js +1 -1
  24. package/dist/capabilities/session/adapters/meituan-local.js +1 -1
  25. package/dist/capabilities/session/benchmark.js +1 -1
  26. package/dist/capabilities/session/extension-bridge.js +19 -6
  27. package/dist/capabilities/session/extension-channel.js +3 -3
  28. package/dist/capabilities/session/extension-distribution.js +234 -0
  29. package/dist/capabilities/session/extract.js +1 -1
  30. package/dist/capabilities/session/golden-score.js +92 -0
  31. package/dist/capabilities/session/health-watch.js +1 -1
  32. package/dist/capabilities/session/read-guard.js +1 -1
  33. package/dist/capabilities/session/static-flight-golden.js +137 -0
  34. package/dist/capabilities/session/transport.js +1 -1
  35. package/dist/capabilities/session/wizard.js +21 -251
  36. package/dist/capabilities/session-consent.js +1 -1
  37. package/dist/capabilities/session-login.js +11 -5
  38. package/dist/capabilities/session-search.js +46 -4
  39. package/dist/capabilities/weather.js +168 -46
  40. package/dist/data/session-golden-20.json +25 -0
  41. package/dist/data/sf-golden-manifest.json +102 -0
  42. package/dist/data/sf-static-routes.json +91 -0
  43. package/dist/scripts/action-cache-tests.js +1 -1
  44. package/dist/scripts/agent-planning-budget-e2e.js +227 -0
  45. package/dist/scripts/agent-planning-budget-tests.js +173 -0
  46. package/dist/scripts/agent-reach-deep-tests.js +1 -1
  47. package/dist/scripts/agent-reach-tests.js +1 -1
  48. package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
  49. package/dist/scripts/anything-tests.js +1 -1
  50. package/dist/scripts/async-collect.js +1 -1
  51. package/dist/scripts/benchmark-environment-bridge-e2e.js +981 -0
  52. package/dist/scripts/benchmark-environment-bridge-tests.js +2544 -0
  53. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +335 -0
  54. package/dist/scripts/booking-copilot-availability-policy-v2-tests.js +1355 -0
  55. package/dist/scripts/booking-copilot-bin-proof-tests.js +89 -0
  56. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +113 -0
  57. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +356 -0
  58. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +557 -0
  59. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +140 -0
  60. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +202 -0
  61. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +102 -0
  62. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +374 -0
  63. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +447 -0
  64. package/dist/scripts/booking-copilot-runtime-proof-tests.js +205 -0
  65. package/dist/scripts/booking-copilot-server-proof-tests.js +317 -0
  66. package/dist/scripts/booking-copilot-startup-proof-tests.js +283 -0
  67. package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +3935 -0
  68. package/dist/scripts/booking-saga-tests.js +1 -1
  69. package/dist/scripts/booking-surface-contract-proof-tests.js +393 -0
  70. package/dist/scripts/booking-surface-v2-contract-proof-tests.js +1174 -0
  71. package/dist/scripts/bootstrap-tests.js +30 -10
  72. package/dist/scripts/build-changelog.js +1 -1
  73. package/dist/scripts/changelog-tests.js +1 -1
  74. package/dist/scripts/companion-tests.js +1 -1
  75. package/dist/scripts/diff-test.js +1 -1
  76. package/dist/scripts/dsh-runtime-closure-tests.js +225 -0
  77. package/dist/scripts/dsh-runtime-closure.js +150 -0
  78. package/dist/scripts/effect-tests.js +1 -1
  79. package/dist/scripts/engine-run.js +1 -1
  80. package/dist/scripts/engine-tests.js +1 -1
  81. package/dist/scripts/evaluation-cadence-tests.js +347 -0
  82. package/dist/scripts/evaluation-contract-tests.js +574 -0
  83. package/dist/scripts/extension-distribution-cli.js +35 -0
  84. package/dist/scripts/extension-distribution-tests.js +341 -0
  85. package/dist/scripts/extension-tests.js +128 -23
  86. package/dist/scripts/fact-gate-tests.js +1 -1
  87. package/dist/scripts/flyai-tests.js +1 -1
  88. package/dist/scripts/hbcli-e2e-tests.js +1 -1
  89. package/dist/scripts/hbcli-tests.js +1 -1
  90. package/dist/scripts/health-watch-cli.js +1 -1
  91. package/dist/scripts/i18n-tests.js +1 -1
  92. package/dist/scripts/incident-tests.js +1 -1
  93. package/dist/scripts/journey-tests.js +1 -1
  94. package/dist/scripts/ledger-tests.js +1 -1
  95. package/dist/scripts/ledger-workflow-crash.js +1 -1
  96. package/dist/scripts/memory-capture-tests.js +1 -1
  97. package/dist/scripts/memory-decay-tests.js +1 -1
  98. package/dist/scripts/memory-metrics.js +1 -1
  99. package/dist/scripts/memory-value-report.js +1 -1
  100. package/dist/scripts/model-override-e2e.js +176 -0
  101. package/dist/scripts/nightly-evidence-tests.js +1 -1
  102. package/dist/scripts/nightly-evidence.js +1 -1
  103. package/dist/scripts/nudge-digest.js +1 -1
  104. package/dist/scripts/onboarding-tests.js +21 -53
  105. package/dist/scripts/opensky-check.js +1 -1
  106. package/dist/scripts/opensky-tests.js +1 -1
  107. package/dist/scripts/pnpm-dsh-closure-proof.js +20 -0
  108. package/dist/scripts/price-drift-tests.js +1 -1
  109. package/dist/scripts/price-drift-watch.js +1 -1
  110. package/dist/scripts/probe-poi-tests.js +1 -1
  111. package/dist/scripts/product-metrics.js +1 -1
  112. package/dist/scripts/publish-preverify.js +40 -4
  113. package/dist/scripts/realtime-pricing-tests.js +1 -1
  114. package/dist/scripts/replay-async.js +1 -1
  115. package/dist/scripts/replay-real.js +1 -1
  116. package/dist/scripts/replay.js +1 -1
  117. package/dist/scripts/session-attach-diagnose.js +1 -1
  118. package/dist/scripts/session-attach-poc.js +1 -1
  119. package/dist/scripts/session-benchmark.js +1 -1
  120. package/dist/scripts/session-extract-tests.js +1 -1
  121. package/dist/scripts/session-login.js +1 -1
  122. package/dist/scripts/session-tests.js +70 -20
  123. package/dist/scripts/sf-live-benchmark.js +338 -0
  124. package/dist/scripts/sf-live-cli-tests.js +21 -0
  125. package/dist/scripts/sf-soft-score-tests.js +108 -0
  126. package/dist/scripts/sf-summary.js +93 -0
  127. package/dist/scripts/skeleton-check.js +1 -1
  128. package/dist/scripts/skeleton-integration-test.js +1 -1
  129. package/dist/scripts/skills-contract-tests.js +1 -1
  130. package/dist/scripts/smoke-session-gate-tests.js +29 -0
  131. package/dist/scripts/smoke.js +79 -33
  132. package/dist/scripts/state-cli-tests.js +1 -1
  133. package/dist/scripts/state-cli.js +1 -1
  134. package/dist/scripts/static-golden-tests.js +299 -0
  135. package/dist/scripts/time-eval-tests.js +1 -1
  136. package/dist/scripts/travel-timeline-tests.js +1 -1
  137. package/dist/scripts/unified-tests.js +1 -1
  138. package/dist/scripts/weather-tests.js +694 -44
  139. package/dist/scripts/z3-race-tests.js +1 -1
  140. package/dist/src/artifact-gate.js +1 -1
  141. package/dist/src/benchmark-agent-conformance.js +370 -0
  142. package/dist/src/benchmark-environment-bridge.js +384 -0
  143. package/dist/src/benchmark-headless-child-diagnostics.js +173 -0
  144. package/dist/src/benchmark-tool-isolation.js +124 -0
  145. package/dist/src/bookable-facts.js +1 -1
  146. package/dist/src/booking-saga.js +1 -1
  147. package/dist/src/booking-surface/availability-policy-v2.js +830 -0
  148. package/dist/src/booking-surface/canonical-schema.js +113 -0
  149. package/dist/src/booking-surface/contracts-v2.js +89 -0
  150. package/dist/src/booking-surface/contracts.js +46 -0
  151. package/dist/src/booking-surface/dsh-planner.js +453 -0
  152. package/dist/src/booking-surface/dsh-plugin.js +93 -0
  153. package/dist/src/booking-surface/error-codes.js +94 -0
  154. package/dist/src/booking-surface/index.js +15 -0
  155. package/dist/src/booking-surface/profile.js +68 -0
  156. package/dist/src/booking-surface/runtime-v2.js +1771 -0
  157. package/dist/src/booking-surface/runtime.js +351 -0
  158. package/dist/src/booking-surface/server-v2.js +334 -0
  159. package/dist/src/booking-surface/server.js +302 -0
  160. package/dist/src/booking-surface/startup.js +159 -0
  161. package/dist/src/booking-surface/validation-v2.js +319 -0
  162. package/dist/src/booking-surface/validation.js +809 -0
  163. package/dist/src/bridge.js +1 -1
  164. package/dist/src/companions.js +1 -1
  165. package/dist/src/contracts.js +1 -1
  166. package/dist/src/dsh-llm.js +1 -1
  167. package/dist/src/engine.js +1 -1
  168. package/dist/src/evaluation-cadence.js +234 -0
  169. package/dist/src/evaluation-contracts.js +906 -0
  170. package/dist/src/i18n.js +1 -1
  171. package/dist/src/index.js +50 -19
  172. package/dist/src/journey.js +1 -1
  173. package/dist/src/loop.js +1 -1
  174. package/dist/src/memory-capture.js +1 -1
  175. package/dist/src/memory-decay.js +1 -1
  176. package/dist/src/memory-utility.js +1 -1
  177. package/dist/src/mock-llm.js +1 -1
  178. package/dist/src/model.js +1 -1
  179. package/dist/src/realtime-pricing.js +1 -1
  180. package/dist/src/slot-spec.js +1 -1
  181. package/dist/src/state-ledger.js +2 -1
  182. package/dist/src/time-anchor.js +1 -1
  183. package/dist/src/tool-budget.js +136 -0
  184. package/dist/src/tool-packet.js +1 -1
  185. package/dist/src/travel-slots.js +1 -1
  186. package/dist/src/travel-timeline.js +1 -1
  187. package/dist/src/unified.js +1 -1
  188. package/dist/src/wish-pool.js +1 -1
  189. package/dist/src/z3-shared.js +1 -1
  190. package/extension/README.md +31 -7
  191. package/package.json +286 -11
  192. package/schemas/booking.surface.v1.schema.json +927 -0
  193. package/schemas/booking.surface.v2.schema.json +61 -0
  194. package/ts/capabilities/flyai.ts +16 -3
  195. package/ts/capabilities/session/extension-bridge.ts +37 -14
  196. package/ts/capabilities/session/extension-channel.ts +6 -3
  197. package/ts/capabilities/session/extension-distribution.ts +264 -0
  198. package/ts/capabilities/session/golden-score.ts +139 -0
  199. package/ts/capabilities/session/health-watch.ts +1 -1
  200. package/ts/capabilities/session/static-flight-golden.ts +209 -0
  201. package/ts/capabilities/session/wizard.ts +34 -176
  202. package/ts/capabilities/session-login.ts +15 -5
  203. package/ts/capabilities/session-search.ts +40 -3
  204. package/ts/capabilities/weather.ts +141 -52
  205. package/ts/package.json +3 -3
  206. package/ts/src/benchmark-agent-conformance.ts +448 -0
  207. package/ts/src/benchmark-environment-bridge.ts +348 -0
  208. package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
  209. package/ts/src/benchmark-tool-isolation.ts +166 -0
  210. package/ts/src/booking-surface/availability-policy-v2.ts +523 -0
  211. package/ts/src/booking-surface/canonical-schema.js +113 -0
  212. package/ts/src/booking-surface/contracts-v2.ts +118 -0
  213. package/ts/src/booking-surface/contracts.ts +380 -0
  214. package/ts/src/booking-surface/dsh-planner.ts +452 -0
  215. package/ts/src/booking-surface/dsh-plugin.js +93 -0
  216. package/ts/src/booking-surface/error-codes.ts +101 -0
  217. package/ts/src/booking-surface/index.ts +12 -0
  218. package/ts/src/booking-surface/profile.ts +42 -0
  219. package/ts/src/booking-surface/runtime-v2.ts +1466 -0
  220. package/ts/src/booking-surface/runtime.ts +483 -0
  221. package/ts/src/booking-surface/server-v2.ts +247 -0
  222. package/ts/src/booking-surface/server.ts +324 -0
  223. package/ts/src/booking-surface/startup.ts +196 -0
  224. package/ts/src/booking-surface/validation-v2.ts +205 -0
  225. package/ts/src/booking-surface/validation.ts +453 -0
  226. package/ts/src/index.ts +64 -11
  227. package/ts/src/state-ledger.ts +1 -0
  228. package/ts/src/tool-budget.ts +165 -0
  229. package/dist/scripts/wizard-bootstrap.js +0 -32
@@ -6,10 +6,10 @@
6
6
  * gotry help # help
7
7
  *
8
8
  * 工作流程:
9
- * 1. 解析 argv + .env(provider-neutral → DEEPSEEK_API_KEY)
9
+ * 1. 解析 argv + .env(provider-neutral → DEEPSEEK_API_KEY/DEEPSEEK_BASE_URL)
10
10
  * 2. 定位 dsh runtime:
11
- * a. repo checkout → vendored ts/dsh-runtime/node_modules/...
12
- * b. npm 安装 → createRequire 解析依赖 @deepseek-ai/dsh(不走 npx:
11
+ * a. repo checkout / npm 安装 → root package 解析锁定的 @deepseek-ai/dsh
12
+ * b. 非 benchmark 且 root 缺失时 → legacy vendored node_modules fallback(不走 npx:
13
13
  * dsh cordis-loader 在子 cwd 求值 plugin name,必须绝对路径 patch)
14
14
  * 3. 运行时生成 patch(os.tmpdir):把 gotry-tools 插件路径重写为按本包
15
15
  * 位置解析的绝对路径 —— 仓内 cordis.gotry-patch.yml 里的 name 行只是
@@ -17,20 +17,30 @@
17
17
  */
18
18
 
19
19
  import { spawn, spawnSync } from 'node:child_process'
20
- import { existsSync, readFileSync, writeFileSync } from 'node:fs'
21
- import { fileURLToPath } from 'node:url'
20
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
21
+ import { fileURLToPath, pathToFileURL } from 'node:url'
22
22
  import { dirname, join } from 'node:path'
23
23
  import { tmpdir } from 'node:os'
24
24
  import { createRequire } from 'node:module'
25
+ import {
26
+ benchmarkRuntimeSupported,
27
+ resolveDshPackage,
28
+ selectDshCwd,
29
+ selectDshRuntime,
30
+ supportsNodeVersion,
31
+ } from './gotry-runtime-resolution.js'
25
32
 
26
33
  const here = dirname(fileURLToPath(import.meta.url))
27
34
  const repoRoot = join(here, '..')
35
+ const sourceCheckoutMode = existsSync(join(repoRoot, '.git'))
28
36
  const require_ = createRequire(import.meta.url)
37
+ const rootRequire = createRequire(join(repoRoot, 'package.json'))
29
38
 
30
39
  // --- 环境 .env 加载(provider-neutral) ---
31
40
  // npm 安装模式优先读用户当前目录的 .env(包目录内不该有凭证);repo 检出读仓根。
32
- const vendoredDshEarly = existsSync(join(repoRoot, 'ts/dsh-runtime/node_modules/@deepseek-ai/dsh/lib/bin.js'))
33
- const envCandidates = vendoredDshEarly
41
+ const sourceDshEarly = resolveDshPackage(rootRequire)
42
+ const vendoredDshEarly = !sourceDshEarly && existsSync(join(repoRoot, 'ts/dsh-runtime/node_modules/@deepseek-ai/dsh/lib/bin.js'))
43
+ const envCandidates = sourceCheckoutMode || vendoredDshEarly
34
44
  ? [join(repoRoot, '.env')]
35
45
  : [join(process.cwd(), '.env'), join(repoRoot, '.env')]
36
46
  const envLines = []
@@ -46,6 +56,22 @@ for (const line of envLines) {
46
56
  if (process.env.LLM_API_KEY && !process.env.DEEPSEEK_API_KEY) {
47
57
  process.env.DEEPSEEK_API_KEY = process.env.LLM_API_KEY
48
58
  }
59
+ // base 同点映射(issue #48):dsh 侧 llm-deepseek 读 DEEPSEEK_BASE_URL 并拼
60
+ // `${base}/chat/completions`(与 ts/src/dsh-llm.ts 同语义,自定义端点一般含 /v1);
61
+ // 只映射 key 不映射 base 时,OpenAI 兼容 key 会被发往 DeepSeek 官方端点必然 401。
62
+ if (process.env.LLM_BASE_URL && !process.env.DEEPSEEK_BASE_URL) {
63
+ process.env.DEEPSEEK_BASE_URL = process.env.LLM_BASE_URL
64
+ }
65
+ // model 映射(issue #77):LLM_MODEL 显式存在时让它真正驱动 dsh 会话面——
66
+ // ① 这里映射为 GOTRY_LLM_MODEL 传给 gotry-tools 插件,在 agent/request 瀑布做
67
+ // 内存覆盖(dsh settings 用户层 ~/.dsh 压过 composition 层,单靠 patch 不够;
68
+ // 覆盖零持久化,进程退即散,不改写用户设置);
69
+ // ② 下方运行时 patch 追加两条 by-id 覆盖:agent-default-model 默认模型 +
70
+ // llm-deepseek 目录条目(web UI 模型页可见、元数据可记账)。
71
+ // 不设 LLM_MODEL 则两条都不动——默认路径(组合配置或用户 web 设置)面不变。
72
+ if (process.env.LLM_MODEL && !process.env.GOTRY_LLM_MODEL) {
73
+ process.env.GOTRY_LLM_MODEL = process.env.LLM_MODEL
74
+ }
49
75
 
50
76
  // --- argv 解析 ---
51
77
  const args = process.argv.slice(2)
@@ -61,8 +87,8 @@ Usage:
61
87
  gotry help # this help
62
88
 
63
89
  Prerequisites:
64
- • Node 22+
65
- • \`.env\` 里 LLM_API_KEY (DeepSeek / OpenAI 兼容均可)
90
+ • Node 22.15+
91
+ • \`.env\` 里 LLM_API_KEY (DeepSeek / OpenAI 兼容均可;自定义端点另配 LLM_BASE_URL,一般以 /v1 结尾;指定模型另配 LLM_MODEL,同时驱动 dsh 会话面与仓内脚本)
66
92
 
67
93
  Detail: https://github.com/Danceiny/gotry — README
68
94
  `)
@@ -81,35 +107,55 @@ if (mode === 'setup') {
81
107
  process.exit(r.status ?? (r.error ? 1 : 0))
82
108
  }
83
109
 
84
- // --- dsh runtime 定位:repo checkout(vendored)优先,npm 安装走依赖解析 ---
85
- const vendoredDsh = join(repoRoot, 'ts/dsh-runtime/node_modules/@deepseek-ai/dsh/lib/bin.js')
86
- let dshBin = ''
87
- let dshCwd = ''
88
- if (existsSync(vendoredDsh)) {
89
- dshBin = vendoredDsh
90
- dshCwd = join(repoRoot, 'ts/dsh-runtime')
91
- } else {
92
- try {
93
- dshBin = require_.resolve('@deepseek-ai/dsh/lib/bin.js')
94
- dshCwd = process.cwd() // npm 安装:gotry-state 落在用户调用目录
95
- } catch {
96
- dshBin = ''
97
- }
110
+ const benchmarkEnvironmentConfig = process.env.GOTRY_BENCHMARK_ENV_CONFIG
111
+ if (!supportsNodeVersion(process.versions.node)) {
112
+ console.error('[gotry] Node.js 22.15 or newer is required')
113
+ process.exit(1)
98
114
  }
99
115
 
116
+ // --- dsh runtime 定位:root manifest/require.resolve 优先;旧 vendored 仅作 legacy fallback ---
117
+ const vendoredDsh = join(repoRoot, 'ts/dsh-runtime/node_modules/@deepseek-ai/dsh/lib/bin.js')
118
+ const installedPackageMode = !sourceCheckoutMode
119
+ const selectedDsh = selectDshRuntime({
120
+ repoRoot,
121
+ rootResolver: rootRequire,
122
+ benchmark: Boolean(benchmarkEnvironmentConfig),
123
+ })
124
+ let dshBin = selectedDsh?.bin ?? ''
125
+ const dshSource = selectedDsh?.source ?? ''
126
+ const dshCwd = selectedDsh
127
+ ? selectDshCwd({
128
+ repoRoot,
129
+ invocationCwd: process.cwd(),
130
+ sourceCheckoutMode,
131
+ benchmark: Boolean(benchmarkEnvironmentConfig),
132
+ })
133
+ : ''
134
+
100
135
  if (!dshBin) {
136
+ if (benchmarkEnvironmentConfig) {
137
+ console.error('[gotry] benchmark dsh runtime unavailable')
138
+ process.exit(1)
139
+ }
101
140
  console.error(`[gotry] 找不到 dsh runtime(既无 vendored ${vendoredDsh},依赖里也没有 @deepseek-ai/dsh)。`)
102
- console.error('repo checkout: cd ts/dsh-runtime && pnpm install;npm 安装: npm install(检查 node_modules)。')
141
+ console.error('repo checkout: npm ci && npm --prefix ts ci && node scripts/build-dist.mjs;npm 安装: npm install(检查 node_modules)。')
142
+ process.exit(1)
143
+ }
144
+ if (benchmarkEnvironmentConfig && !benchmarkRuntimeSupported(selectedDsh)) {
145
+ console.error('[gotry] benchmark dsh runtime unavailable')
103
146
  process.exit(1)
104
147
  }
105
148
 
106
149
  // --- 运行时 patch:插件路径按本包位置重写为绝对路径 ---
107
- // npm 模式指向 dist/ 纯 JS(Node 拒绝 strip node_modules 下的 .ts);
108
- // repo 检出指向 .ts 源码(vendored runtime 下天然可行)。
150
+ // 安装包始终指向 dist/ 纯 JS(Node 拒绝 strip node_modules 下的 .ts)。
151
+ // repo 检出仅在 vendored runtime 或显式 tsx loader 可用时走 .ts;否则
152
+ // 使用当前 worktree 已构建的 dist,避免 Node strip-only 拒绝参数属性语法。
109
153
  const distEntry = join(repoRoot, 'dist/src/index.js')
110
154
  const tsEntry = join(repoRoot, 'ts/src/index.ts')
111
- const npmMode = !existsSync(vendoredDsh)
112
- const pluginEntry = npmMode && existsSync(distEntry) ? distEntry : tsEntry
155
+ const tsxLoaderActive = /(?:^|\s)--(?:import|loader)(?:=|\s)(?:"[^"]*tsx[^"]*"|'[^']*tsx[^']*'|\S*tsx\S*)/.test(process.env.NODE_OPTIONS ?? '')
156
+ const sourceTypeScriptMode = !installedPackageMode && (dshSource === 'legacy-vendored' || tsxLoaderActive)
157
+ const pluginEntry = !sourceTypeScriptMode && existsSync(distEntry) ? distEntry : tsEntry
158
+ const distModuleMode = pluginEntry === distEntry
113
159
  const staticPatch = join(repoRoot, 'cordis.gotry-patch.yml')
114
160
  if (!existsSync(staticPatch)) {
115
161
  console.error(`[gotry] 找不到 patch 配置: ${staticPatch}`)
@@ -119,16 +165,214 @@ if (!existsSync(pluginEntry)) {
119
165
  console.error(`[gotry] 找不到插件入口: ${pluginEntry}(包不完整?)`)
120
166
  process.exit(1)
121
167
  }
168
+
169
+ // Benchmark mode projects the patch before any optional host plugin is
170
+ // resolved/imported. Non-GoTry (including unknown/future) insert entries are
171
+ // safely discarded; a missing/duplicate gotry-tools entry fails closed.
172
+ function projectBenchmarkPatch(raw, entryPath, configPath) {
173
+ const lines = raw.split('\n')
174
+ const rootInsertKey = /(?:^|[,{}]\s*)(?:insert|'insert'|"insert")\s*:/
175
+ const rootInsertLines = lines
176
+ .map((line, index) => {
177
+ const rootItem = line.match(/^-\s+(.*)$/)?.[1]?.trim()
178
+ const directProperty = line.match(/^ {2}(.*)$/)?.[1]?.trim()
179
+ return rootInsertKey.test(rootItem ?? '') || /^(?:insert|'insert'|"insert")\s*:/.test(directProperty ?? '') ? index : -1
180
+ })
181
+ .filter(index => index >= 0)
182
+ const canonicalInsertStarts = lines
183
+ .map((line, index) => /^- insert:\s*(?:#.*)?$/.test(line) ? index : -1)
184
+ .filter(index => index >= 0)
185
+ if (rootInsertLines.length !== 1 || canonicalInsertStarts.length !== 1 || rootInsertLines[0] !== canonicalInsertStarts[0]) {
186
+ throw new Error('benchmark insert block violation')
187
+ }
188
+ const start = canonicalInsertStarts[0]
189
+ let end = start + 1
190
+ while (end < lines.length) {
191
+ const line = lines[end]
192
+ if (line.trim() !== '' && !/^\s/.test(line) && !line.trim().startsWith('#')) break
193
+ end += 1
194
+ }
195
+ const body = lines.slice(start + 1, end)
196
+ const firstItem = body.find(line => line.trim() !== '' && !line.trim().startsWith('#'))
197
+ const firstItemMatch = firstItem?.match(/^(\s*)-\s+/)
198
+ if (!firstItemMatch) throw new Error('invalid insert sequence')
199
+ const itemIndent = firstItemMatch[1].length
200
+ const itemStarts = []
201
+ for (let index = 0; index < body.length; index += 1) {
202
+ const line = body[index]
203
+ if (line.trim() === '' || line.trim().startsWith('#')) continue
204
+ const indent = line.match(/^\s*/)?.[0].length ?? 0
205
+ if (indent < itemIndent || (indent === itemIndent && !/^\s*-\s+/.test(line))) {
206
+ throw new Error('invalid insert sequence')
207
+ }
208
+ if (indent === itemIndent) itemStarts.push(index)
209
+ }
210
+ if (itemStarts.length === 0) throw new Error('empty insert sequence')
211
+
212
+ const parseId = value => {
213
+ const quoted = value.trim().match(/^(['"])([A-Za-z0-9._-]+)\1(?:\s+#.*)?$/)
214
+ if (quoted) return quoted[2]
215
+ return value.trim().match(/^([A-Za-z0-9._-]+)(?:\s+#.*)?$/)?.[1] ?? null
216
+ }
217
+ const itemId = item => {
218
+ const header = item[0].slice(itemIndent + 2).trim()
219
+ const canonical = header.match(/^id:\s*(.+)$/)
220
+ if (canonical) return parseId(canonical[1])
221
+ if (header.startsWith('{')) {
222
+ const inline = header.match(/(?:^\{\s*|,\s*)id\s*:\s*(['"]?)([A-Za-z0-9._-]+)\1(?=\s*[,}])/)
223
+ if (inline) return inline[2]
224
+ }
225
+ const propertyIndent = ' '.repeat(itemIndent + 2)
226
+ const directIds = item.slice(1)
227
+ .map(line => line.startsWith(`${propertyIndent}id:`) ? parseId(line.slice(propertyIndent.length + 3)) : null)
228
+ .filter(Boolean)
229
+ if (directIds.length > 1) throw new Error('duplicate insert id')
230
+ return directIds[0] ?? null
231
+ }
232
+ const items = itemStarts.map((itemStart, index) => body.slice(itemStart, itemStarts[index + 1] ?? body.length))
233
+ const gotryItems = items.filter(item => itemId(item) === 'gotry-tools')
234
+ if (gotryItems.length !== 1) throw new Error('benchmark insert allowlist violation')
235
+ let gotry = gotryItems[0].join('\n')
236
+ gotry = rewriteBenchmarkPluginEntry(gotry, entryPath)
237
+ gotry = injectBenchmarkEnvironmentConfig(gotry, configPath)
238
+ const projected = [...lines.slice(0, start + 1), ...gotry.split('\n'), ...lines.slice(end)]
239
+ return projectBenchmarkSystemPrompt(projected).join('\n')
240
+ }
241
+
242
+ // The normal persona contains dynamic variables and product-only tool
243
+ // instructions. They are not meaningful in the minimal benchmark kernel and
244
+ // can make dsh fail while assembling the first request. Require the exact
245
+ // source shape before replacing it; malformed or ambiguous prompt config must
246
+ // fail closed rather than being partially interpreted.
247
+ function projectBenchmarkSystemPrompt(lines) {
248
+ const rootStarts = lines.map((line, index) => /^-\s+/.test(line) ? index : -1).filter(index => index >= 0)
249
+ const rootItems = rootStarts.map((itemStart, index) => lines.slice(itemStart, rootStarts[index + 1] ?? lines.length))
250
+ if (rootItems.length !== 2) throw new Error('benchmark system-prompt root shape violation')
251
+ const insertItems = rootItems.filter(item => /^- insert:\s*(?:#.*)?$/.test(item[0]))
252
+ const systemItems = rootItems.filter(item => /^- id:\s*system-prompt\s*(?:#.*)?$/.test(item[0]))
253
+ if (insertItems.length !== 1 || systemItems.length !== 1) throw new Error('benchmark system-prompt canonical shape violation')
254
+ const start = rootStarts[rootItems.indexOf(systemItems[0])]
255
+ let end = start + 1
256
+ while (end < lines.length && (lines[end].trim() === '' || /^\s/.test(lines[end]) || lines[end].trim().startsWith('#'))) end += 1
257
+ const item = lines.slice(start, end)
258
+ const configIndexes = item.map((line, index) => /^ {2}config:\s*$/.test(line) ? index : -1).filter(index => index >= 0)
259
+ const personaIndexes = item.map((line, index) => /^ {4}persona:\s*>-\s*$/.test(line) ? index : -1).filter(index => index >= 0)
260
+ if (configIndexes.length !== 1 || personaIndexes.length !== 1 || personaIndexes[0] <= configIndexes[0]) {
261
+ throw new Error('benchmark system-prompt config violation')
262
+ }
263
+ const persona = personaIndexes[0]
264
+ const blockEnd = item.slice(persona + 1).findIndex(line => line.trim() !== '' && !/^\s{6,}/.test(line))
265
+ const contentEnd = blockEnd < 0 ? item.length : persona + 1 + blockEnd
266
+ if (contentEnd === persona + 1 || item.slice(persona + 1, contentEnd).some(line => line.trim() !== '' && !/^\s{6,}/.test(line))) {
267
+ throw new Error('benchmark system-prompt persona violation')
268
+ }
269
+ const before = item.slice(1, configIndexes[0]).filter(line => line.trim() !== '' && !line.trim().startsWith('#'))
270
+ const between = item.slice(configIndexes[0] + 1, persona).filter(line => line.trim() !== '' && !line.trim().startsWith('#'))
271
+ const after = item.slice(contentEnd).filter(line => line.trim() !== '' && !line.trim().startsWith('#'))
272
+ if (before.length !== 0 || between.length !== 0 || after.length !== 0) throw new Error('benchmark system-prompt shape violation')
273
+ const replacement = [
274
+ '- id: system-prompt',
275
+ ' config:',
276
+ ' persona: >-',
277
+ ' You are GoTry, a task-agnostic travel planning assistant.',
278
+ ' Use only the current conversation and tools available in this benchmark session.',
279
+ ]
280
+ return [...lines.slice(0, start), ...replacement, ...lines.slice(end)]
281
+ }
282
+
283
+ function rewriteBenchmarkPluginEntry(raw, entryPath) {
284
+ const itemIndent = raw.match(/^(\s*)-\s+/)?.[1].length
285
+ if (itemIndent === undefined) throw new Error('benchmark plugin item unavailable')
286
+ const propertyIndent = ' '.repeat(itemIndent + 2)
287
+ const anchorPattern = new RegExp(`^(${propertyIndent}name:\\s*)'[^'\\r\\n]*ts\\/src\\/index\\.ts'(\\s*(?:#.*)?)$`, 'gm')
288
+ const anchors = [...raw.matchAll(anchorPattern)]
289
+ if (anchors.length !== 1 || anchors[0].index === undefined) throw new Error('benchmark plugin anchor unavailable')
290
+ const anchor = anchors[0]
291
+ const escapedEntry = entryPath.replace(/'/g, "''")
292
+ const replacement = `${anchor[1]}'${escapedEntry}'${anchor[2]}`
293
+ const rewritten = `${raw.slice(0, anchor.index)}${replacement}${raw.slice(anchor.index + anchor[0].length)}`
294
+ if (!rewritten.includes(replacement)) throw new Error('benchmark plugin rewrite unavailable')
295
+ return rewritten
296
+ }
297
+
298
+ function injectBenchmarkEnvironmentConfig(raw, configPath) {
299
+ const itemIndent = raw.match(/^(\s*)-\s+/)?.[1].length
300
+ if (itemIndent === undefined) throw new Error('benchmark config item unavailable')
301
+ const propertyIndent = ' '.repeat(itemIndent + 2)
302
+ const configIndent = ' '.repeat(itemIndent + 4)
303
+ const configAnchors = raw.match(new RegExp(`^${propertyIndent}config:\\s*(?:#.*)?$`, 'gm')) ?? []
304
+ if (configAnchors.length !== 1) throw new Error('benchmark config block unavailable')
305
+ if ((raw.match(new RegExp(`^${configIndent}benchmarkEnvironmentConfigPath\\s*:`, 'gm')) ?? []).length !== 0) {
306
+ throw new Error('duplicate benchmark config path')
307
+ }
308
+ const anchorPattern = new RegExp(`^(${configIndent})hbcliBin:\\s*'[^'\\r\\n]*'\\s*(?:#.*)?$`, 'gm')
309
+ const anchors = [...raw.matchAll(anchorPattern)]
310
+ if (anchors.length !== 1 || anchors[0].index === undefined) throw new Error('benchmark config anchor unavailable')
311
+ const anchor = anchors[0]
312
+ const escapedPath = configPath.replace(/'/g, "''")
313
+ const insertAt = anchor.index + anchor[0].length
314
+ const injected = `${raw.slice(0, insertAt)}\n${anchor[1]}benchmarkEnvironmentConfigPath: '${escapedPath}'${raw.slice(insertAt)}`
315
+ if ((injected.match(new RegExp(`^${configIndent}benchmarkEnvironmentConfigPath\\s*:`, 'gm')) ?? []).length !== 1) {
316
+ throw new Error('benchmark config injection unavailable')
317
+ }
318
+ return injected
319
+ }
320
+
321
+ let benchmarkTerminalConfig = null
322
+ let benchmarkPatchProjection = null
323
+ let benchmarkChildDiagnostics = null
324
+ if (benchmarkEnvironmentConfig !== undefined && benchmarkEnvironmentConfig !== '') {
325
+ let validBenchmarkEnvironmentConfig = false
326
+ try {
327
+ const bridgeModule = distModuleMode && existsSync(join(repoRoot, 'dist/src/benchmark-environment-bridge.js'))
328
+ ? join(repoRoot, 'dist/src/benchmark-environment-bridge.js')
329
+ : join(repoRoot, 'ts/src/benchmark-environment-bridge.ts')
330
+ const { loadBenchmarkEnvironmentConfig } = await import(pathToFileURL(bridgeModule).href)
331
+ const loadedConfig = benchmarkEnvironmentConfig.length > 0
332
+ && benchmarkEnvironmentConfig.length <= 4096
333
+ && !benchmarkEnvironmentConfig.includes('\0')
334
+ && !benchmarkEnvironmentConfig.includes('\r')
335
+ && !benchmarkEnvironmentConfig.includes('\n')
336
+ ? loadBenchmarkEnvironmentConfig(benchmarkEnvironmentConfig)
337
+ : null
338
+ validBenchmarkEnvironmentConfig = loadedConfig !== null
339
+ benchmarkTerminalConfig = loadedConfig?.terminal_output ?? null
340
+ if (validBenchmarkEnvironmentConfig) {
341
+ benchmarkPatchProjection = projectBenchmarkPatch(readFileSync(staticPatch, 'utf-8'), pluginEntry, benchmarkEnvironmentConfig)
342
+ }
343
+ } catch { validBenchmarkEnvironmentConfig = false }
344
+ if (!validBenchmarkEnvironmentConfig) {
345
+ console.error('[gotry] benchmark environment configuration unavailable')
346
+ process.exit(1)
347
+ }
348
+ }
349
+ if (benchmarkEnvironmentConfig && mode !== 'headless') {
350
+ console.error('[gotry] benchmark environment requires headless mode')
351
+ process.exit(1)
352
+ }
353
+ if (benchmarkEnvironmentConfig) {
354
+ try {
355
+ const diagnosticModule = distModuleMode && existsSync(join(repoRoot, 'dist/src/benchmark-headless-child-diagnostics.js'))
356
+ ? join(repoRoot, 'dist/src/benchmark-headless-child-diagnostics.js')
357
+ : join(repoRoot, 'ts/src/benchmark-headless-child-diagnostics.ts')
358
+ benchmarkChildDiagnostics = await import(pathToFileURL(diagnosticModule).href)
359
+ } catch {
360
+ console.error('[gotry] benchmark environment configuration unavailable')
361
+ process.exit(1)
362
+ }
363
+ }
122
364
  // dsh-map-tools 宿主插件(地图/路线/POI,零 key 走 OSM/OSRM):repo 用 vendored,
123
365
  // npm 用依赖解析;都找不到就整块剔除 patch 条目(缺地图不挡旅行规划)
124
366
  let mapEntry = ''
125
- const vendoredMap = join(repoRoot, 'ts/dsh-runtime/node_modules/dsh-map-tools/lib/index.js')
126
- if (existsSync(vendoredMap)) {
127
- mapEntry = vendoredMap
128
- } else {
129
- // npm 布局:子路径可能被 exports 挡(resolve 抛错不能留下旧值),裸包名返回真实入口
130
- try { mapEntry = require_.resolve('dsh-map-tools/lib/index.js') } catch { mapEntry = '' }
131
- if (!mapEntry) { try { mapEntry = require_.resolve('dsh-map-tools') } catch { mapEntry = '' } }
367
+ if (!benchmarkEnvironmentConfig) {
368
+ const vendoredMap = join(repoRoot, 'ts/dsh-runtime/node_modules/dsh-map-tools/lib/index.js')
369
+ if (existsSync(vendoredMap)) {
370
+ mapEntry = vendoredMap
371
+ } else {
372
+ // npm 布局:子路径可能被 exports 挡(resolve 抛错不能留下旧值),裸包名返回真实入口
373
+ try { mapEntry = require_.resolve('dsh-map-tools/lib/index.js') } catch { mapEntry = '' }
374
+ if (!mapEntry) { try { mapEntry = require_.resolve('dsh-map-tools') } catch { mapEntry = '' } }
375
+ }
132
376
  }
133
377
  // 结构化澄清卡(T2):ask_user_question 工具 + user-questions 服务,从 dsh 包
134
378
  // 上下文解析(pnpm 嵌套布局下只有 dsh 自己看得见这些依赖);失败整块剔除
@@ -138,7 +382,7 @@ if (existsSync(vendoredMap)) {
138
382
  const stdioAsk = mode === 'headless' && (process.stdin.isTTY || process.env.GOTRY_ASK_STDIO === '1')
139
383
  ? join(here, 'gotry-stdio-ask.js') : ''
140
384
  let askUserInsert = ''
141
- if (mode === 'web' || stdioAsk) try {
385
+ if (!benchmarkEnvironmentConfig && (mode === 'web' || stdioAsk)) try {
142
386
  const reqFromDsh = createRequire(dshBin)
143
387
  // 裸包名解析(这些包的 exports 不暴露 ./lib/index.js 子路径,但裸名返回真实入口)
144
388
  // userQuestions 服务默认树已注册(重复插入会崩),只插工具消费者
@@ -147,35 +391,74 @@ if (mode === 'web' || stdioAsk) try {
147
391
  + ` - id: dsh-tool-ask-user\n name: '${at}'`
148
392
  } catch { /* 缺件不挡启动 */ }
149
393
 
150
- let patchRaw = readFileSync(staticPatch, 'utf-8')
151
- .replace(/(name:\s*)'[^']*ts\/src\/index\.ts'/, `$1'${pluginEntry}'`)
152
- .replace(/^\s*# \{ask-user-insert\}.*\n(\s*# .*\n)?/m, askUserInsert ? askUserInsert + '\n' : '')
394
+ const unboundPatch = benchmarkPatchProjection ?? readFileSync(staticPatch, 'utf-8')
395
+ let patchRaw = benchmarkPatchProjection ?? unboundPatch.replace(/(name:\s*)'[^']*ts\/src\/index\.ts'/, `$1'${pluginEntry}'`)
396
+ patchRaw = patchRaw.replace(/^\s*# \{ask-user-insert\}.*\n(\s*# .*\n)?/m, askUserInsert ? askUserInsert + '\n' : '')
397
+
398
+ // Owner-local benchmark environment opt-in. Keep the path out of tool input
399
+ // and logs; it is only injected into the local plugin config as YAML data.
153
400
  if (mapEntry) {
154
401
  patchRaw = patchRaw.replace(/(name:\s*)'placeholder\/dsh-map-tools'/, `$1'${mapEntry}'`)
155
402
  } else {
156
403
  patchRaw = patchRaw.replace(/\n\s*- id: dsh-map-tools\n\s*name: 'placeholder\/dsh-map-tools'\n/, '\n')
157
404
  }
158
405
 
159
- // dsh-calendar 宿主插件(CalDAV 工作窗口读取;未配置时工具报错降级,不挡启动)
406
+ // dsh-calendar 宿主插件(CalDAV 工作窗口读取;未配置时工具报错降级,不挡启动)。
407
+ // 离线预算 E2E 显式禁用可选宿主插件,避免测试依赖真实 CalDAV 账号。
160
408
  let calEntry = ''
161
- const vendoredCal = join(repoRoot, 'ts/dsh-runtime/node_modules/dsh-calendar/lib/index.js')
162
- if (existsSync(vendoredCal)) {
163
- calEntry = vendoredCal
164
- } else {
165
- try { calEntry = require_.resolve('dsh-calendar/lib/index.js') } catch { calEntry = '' }
166
- if (!calEntry) { try { calEntry = require_.resolve('dsh-calendar') } catch { calEntry = '' } }
409
+ if (!benchmarkEnvironmentConfig) {
410
+ const vendoredCal = join(repoRoot, 'ts/dsh-runtime/node_modules/dsh-calendar/lib/index.js')
411
+ if (existsSync(vendoredCal)) {
412
+ calEntry = vendoredCal
413
+ } else {
414
+ try { calEntry = require_.resolve('dsh-calendar/lib/index.js') } catch { calEntry = '' }
415
+ if (!calEntry) { try { calEntry = require_.resolve('dsh-calendar') } catch { calEntry = '' } }
416
+ }
167
417
  }
168
- if (calEntry) {
418
+ if (calEntry && process.env.GOTRY_DISABLE_OPTIONAL_CALENDAR !== '1') {
169
419
  patchRaw = patchRaw.replace(/(name:\s*)'placeholder\/dsh-calendar'/, `$1'${calEntry}'`)
170
420
  } else {
171
421
  patchRaw = patchRaw.replace(/\n\s*- id: dsh-calendar\n\s*name: 'placeholder\/dsh-calendar'\n/, '\n')
172
422
  }
173
- const patchPath = join(tmpdir(), `cordis.gotry.${process.pid}.yml`)
174
- writeFileSync(patchPath, patchRaw)
423
+
424
+ // LLM_MODEL 指定模型(issue #77)②:composition 层 by-id 覆盖。cordis patch 语义
425
+ // (dsh-app-boot applyEntryPatches):非 insert 的 {id, ...overrides} 对目标条目做浅层
426
+ // 键赋值,config 整体替换;id 不存在只 warn+skip 不崩(上游改名时优雅退化)。
427
+ // 两条目标在 headless/web profile 均有(dsh-base 声明 llm-deepseek、两 profile 均声明
428
+ // agent-default-model)。llm-deepseek 目录整体替换为单条目:显式指定模型的场景多为
429
+ // 中转/兼容端点,默认 v4-* 目录对其是误导;不硬编码上游 DEFAULT_MODELS 防漂移。
430
+ if (process.env.GOTRY_LLM_MODEL) {
431
+ const modelYaml = `'${process.env.GOTRY_LLM_MODEL.replace(/'/g, "''")}'`
432
+ patchRaw += `\n# LLM_MODEL 指定模型(issue #77):dsh 会话面默认模型 + 模型目录\n- id: agent-default-model\n config:\n provider: deepseek-official\n model: ${modelYaml}\n- id: llm-deepseek\n config:\n models:\n - id: ${modelYaml}\n name: ${modelYaml}\n`
433
+ }
434
+ // Keep the patch private and short-lived. mkdtempSync creates a 0700 directory;
435
+ // wx prevents accidental reuse/races if a process is started concurrently.
436
+ const patchDir = mkdtempSync(join(tmpdir(), 'gotry-cordis-'))
437
+ const patchPath = join(patchDir, 'patch.yml')
438
+ writeFileSync(patchPath, patchRaw, { mode: 0o600, flag: 'wx' })
439
+ let patchCleaned = false
440
+ const cleanupPatch = () => {
441
+ if (patchCleaned) return
442
+ patchCleaned = true
443
+ rmSync(patchDir, { recursive: true, force: true })
444
+ }
445
+ process.once('exit', cleanupPatch)
446
+ let child = null
447
+ const terminateOnSignal = (signal, listener) => {
448
+ cleanupPatch()
449
+ if (child && !child.killed) child.kill(signal)
450
+ process.off(signal, listener)
451
+ process.kill(process.pid, signal)
452
+ }
453
+ const onSigint = () => terminateOnSignal('SIGINT', onSigint)
454
+ const onSigterm = () => terminateOnSignal('SIGTERM', onSigterm)
455
+ process.once('SIGINT', onSigint)
456
+ process.once('SIGTERM', onSigterm)
175
457
  if (!process.env.DEEPSEEK_API_KEY && mode !== 'help') {
176
458
  console.error('[gotry] 缺少 LLM API key —— 两种方式任选其一后重跑:')
177
459
  console.error(` 1) 在当前目录创建 .env 写入一行: LLM_API_KEY=<你的 DeepSeek key>(key 从 https://platform.deepseek.com 获取)`)
178
460
  console.error(' 2) 或临时环境变量: export LLM_API_KEY=<key>')
461
+ cleanupPatch()
179
462
  process.exit(1)
180
463
  }
181
464
 
@@ -185,22 +468,116 @@ const binJs = mode === 'web'
185
468
  ? ['web', '--patch', patchPath, ...(process.argv.includes('--no-open') ? ['--no-open'] : [])]
186
469
  : ['--profile', 'headless', '--patch', patchPath, ...rest]
187
470
 
188
- const child = spawn(process.execPath, [dshBin, ...binJs], {
189
- stdio: 'inherit',
190
- env: process.env,
471
+ const childEnv = { ...process.env }
472
+ // The benchmark config is parent-only metadata. Never make its path visible to
473
+ // dsh or any plugin loaded by the child.
474
+ delete childEnv.GOTRY_BENCHMARK_ENV_CONFIG
475
+ if (benchmarkEnvironmentConfig) {
476
+ for (const key of [
477
+ 'GOTRY_BENCHMARK_BRIDGE_PARENT_SECRET',
478
+ 'DATABASE_URL',
479
+ 'SSH_AUTH_SOCK',
480
+ 'AWS_PROFILE',
481
+ 'HTTPS_PROXY',
482
+ ]) delete childEnv[key]
483
+ }
484
+ const benchmarkStdout = benchmarkEnvironmentConfig ? [] : null
485
+ if (benchmarkEnvironmentConfig) childEnv.GOTRY_BENCHMARK_DIAGNOSTIC_FD = '3'
486
+ const childStdio = mode === 'web'
487
+ ? 'inherit'
488
+ : benchmarkStdout
489
+ ? ['pipe', 'pipe', 'pipe', 'pipe']
490
+ : process.stdin.isTTY
491
+ ? 'inherit'
492
+ : ['pipe', 'inherit', 'inherit']
493
+ let benchmarkCapturedBytes = 0
494
+ let benchmarkDiagnosticBuffer = Buffer.alloc(0)
495
+ let benchmarkOutputTruncated = false
496
+ child = spawn(process.execPath, [dshBin, ...binJs], {
497
+ stdio: childStdio,
498
+ env: childEnv,
191
499
  cwd: dshCwd,
192
500
  })
501
+ if (benchmarkStdout) {
502
+ const maxBytes = Math.max(1, Number(benchmarkTerminalConfig?.max_bytes ?? 1))
503
+ child.stdout?.on('data', chunk => {
504
+ if (benchmarkCapturedBytes + Buffer.byteLength(chunk) > maxBytes) benchmarkOutputTruncated = true
505
+ if (benchmarkCapturedBytes < maxBytes) {
506
+ const bytes = Buffer.from(chunk)
507
+ benchmarkStdout.push(bytes.subarray(0, Math.max(0, maxBytes - benchmarkCapturedBytes)))
508
+ benchmarkCapturedBytes += Math.min(bytes.length, maxBytes - benchmarkCapturedBytes)
509
+ }
510
+ })
511
+ child.stderr?.resume()
512
+ child.stdio?.[3]?.on('data', chunk => {
513
+ benchmarkDiagnosticBuffer = benchmarkChildDiagnostics.appendBoundedChildDiagnostic(
514
+ benchmarkDiagnosticBuffer,
515
+ Buffer.from(chunk),
516
+ )
517
+ })
518
+ }
193
519
  if (process.env.GOTRY_DEBUG) {
194
- console.error('[gotry-debug] exec:', process.execPath, dshBin)
195
- console.error('[gotry-debug] argv:', binJs)
196
- console.error('[gotry-debug] cwd:', dshCwd)
197
- console.error('[gotry-debug] patch exists:', existsSync(patchPath), patchPath)
520
+ if (benchmarkEnvironmentConfig) {
521
+ console.error('[gotry-debug] benchmark headless child configured(details redacted)')
522
+ } else {
523
+ console.error('[gotry-debug] exec:', process.execPath, dshBin)
524
+ console.error('[gotry-debug] argv:', binJs)
525
+ console.error('[gotry-debug] cwd:', dshCwd)
526
+ console.error('[gotry-debug] patch exists:', existsSync(patchPath), patchPath)
527
+ }
528
+ }
529
+ let benchmarkFailureReported = false
530
+ const reportBenchmarkFailure = reason => {
531
+ if (benchmarkFailureReported) return
532
+ benchmarkFailureReported = true
533
+ process.exitCode = 1
534
+ process.stderr.write(`[gotry] benchmark terminal output unavailable (${reason})\n`)
198
535
  }
199
- child.on('exit', (code, signal) => {
536
+
537
+ child.on('close', (code, signal) => {
538
+ cleanupPatch()
539
+ if (benchmarkStdout) {
540
+ const captured = Buffer.concat(benchmarkStdout).toString('utf8')
541
+ if (code !== 0 || signal || benchmarkOutputTruncated) {
542
+ const reason = benchmarkChildDiagnostics.classifyBenchmarkChildFailure({
543
+ code,
544
+ signal,
545
+ diagnostic: benchmarkDiagnosticBuffer.toString('utf8'),
546
+ outputTruncated: benchmarkOutputTruncated,
547
+ })
548
+ reportBenchmarkFailure(reason)
549
+ return
550
+ }
551
+ const parserModule = distModuleMode && existsSync(join(repoRoot, 'dist/src/benchmark-agent-conformance.js'))
552
+ ? join(repoRoot, 'dist/src/benchmark-agent-conformance.js')
553
+ : join(repoRoot, 'ts/src/benchmark-agent-conformance.ts')
554
+ import(pathToFileURL(parserModule).href).then(async ({ parseBenchmarkTerminal }) => {
555
+ const parsed = parseBenchmarkTerminal(captured, benchmarkTerminalConfig)
556
+ if (!parsed || parsed.ok !== true) {
557
+ reportBenchmarkFailure('child_terminal_invalid')
558
+ return
559
+ }
560
+ await new Promise((resolve, reject) => {
561
+ const onError = error => {
562
+ process.stdout.off('error', onError)
563
+ reject(error)
564
+ }
565
+ process.stdout.once('error', onError)
566
+ process.stdout.write(captured, () => {
567
+ process.stdout.off('error', onError)
568
+ resolve()
569
+ })
570
+ })
571
+ process.exitCode = 0
572
+ }).catch(() => {
573
+ reportBenchmarkFailure('child_lifecycle_failure')
574
+ })
575
+ return
576
+ }
200
577
  if (code !== 0 || signal) {
201
578
  // D-NEW 护栏:dsh 异常退出也写一条 incident,留现场而非沉默
202
- // npm 模式 import dist JS(node_modules 下的 .ts 会被 Node 拒 strip)
203
- const incidentModule = npmMode && existsSync(join(repoRoot, 'dist/capabilities/incident-log.js'))
579
+ // dist 模式 import JS(node_modules 下的 .ts 会被 Node 拒 strip)
580
+ const incidentModule = distModuleMode && existsSync(join(repoRoot, 'dist/capabilities/incident-log.js'))
204
581
  ? '../dist/capabilities/incident-log.js'
205
582
  : '../ts/capabilities/incident-log.ts'
206
583
  import(incidentModule).then(({ recordIncident }) => {
@@ -215,7 +592,12 @@ child.on('exit', (code, signal) => {
215
592
  process.exit(code ?? 1)
216
593
  })
217
594
  child.on('error', (e) => {
595
+ cleanupPatch()
596
+ if (benchmarkEnvironmentConfig) {
597
+ reportBenchmarkFailure('child_spawn_failure')
598
+ return
599
+ }
218
600
  console.error(`[gotry] dsh 启动失败: ${e.message}`)
219
- console.error('尝试: node -v 看 Node 版本(需 22+),或查看 ts/dsh-runtime/node_modules/。')
601
+ console.error('尝试: node -v 看 Node 版本(需 22.15+),或查看 ts/dsh-runtime/node_modules/。')
220
602
  process.exit(1)
221
603
  })
@@ -0,0 +1,27 @@
1
+ export type DshResolver = {
2
+ resolve(specifier: string): string
3
+ }
4
+
5
+ export type DshRuntime = {
6
+ bin: string
7
+ version: string
8
+ source: 'root' | 'legacy-vendored'
9
+ }
10
+
11
+ export const REQUIRED_BENCHMARK_DSH_VERSION: '0.1.2-alpha.3'
12
+
13
+ export function readDshPackage(bin: string): Omit<DshRuntime, 'source'> | null
14
+ export function resolveDshPackage(resolver: DshResolver): Omit<DshRuntime, 'source'> | null
15
+ export function supportsNodeVersion(version: string): boolean
16
+ export function selectDshRuntime(options: {
17
+ repoRoot: string
18
+ rootResolver: DshResolver
19
+ benchmark: boolean
20
+ }): DshRuntime | null
21
+ export function benchmarkRuntimeSupported(runtime: { version: string } | null): boolean
22
+ export function selectDshCwd(options: {
23
+ repoRoot: string
24
+ invocationCwd: string
25
+ sourceCheckoutMode: boolean
26
+ benchmark: boolean
27
+ }): string