@danceiny/gotry 0.0.1-rc.21 → 0.0.1-rc.24

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 (327) hide show
  1. package/README.md +102 -194
  2. package/README.zh-CN.md +115 -210
  3. package/bin/gotry-backend.js +53 -0
  4. package/bin/gotry-bootstrap.js +466 -35
  5. package/bin/gotry-inner.js +255 -36
  6. package/bin/gotry-process-liveness.d.ts +27 -0
  7. package/bin/gotry-process-liveness.js +97 -0
  8. package/bin/gotry-runtime-resolution.d.ts +1 -1
  9. package/bin/gotry-runtime-resolution.js +2 -2
  10. package/client/client.js +174 -0
  11. package/cordis.gotry-patch.yml +70 -93
  12. package/dist/capabilities/agent-reach-deep.js +45 -122
  13. package/dist/capabilities/agent-reach.js +63 -148
  14. package/dist/capabilities/anything.js +89 -75
  15. package/dist/capabilities/artifacts.js +123 -104
  16. package/dist/capabilities/channel-health.js +65 -45
  17. package/dist/capabilities/channel-registry.js +108 -87
  18. package/dist/capabilities/doctor.js +357 -235
  19. package/dist/capabilities/effect.js +223 -309
  20. package/dist/capabilities/fact-log.js +28 -14
  21. package/dist/capabilities/flyai.js +245 -156
  22. package/dist/capabilities/ground-transfer.js +509 -0
  23. package/dist/capabilities/hbcli.js +144 -148
  24. package/dist/capabilities/hotelbyte-transaction.js +194 -0
  25. package/dist/capabilities/incident-log.js +131 -61
  26. package/dist/capabilities/model-override.js +30 -10
  27. package/dist/capabilities/opensky.js +54 -43
  28. package/dist/capabilities/resilience.js +50 -32
  29. package/dist/capabilities/session/action-cache.js +50 -44
  30. package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
  31. package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
  32. package/dist/capabilities/session/adapters/dida-portal.js +206 -0
  33. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  34. package/dist/capabilities/session/adapters/rail-12306.js +169 -112
  35. package/dist/capabilities/session/benchmark.js +85 -149
  36. package/dist/capabilities/session/extension-bridge.js +320 -259
  37. package/dist/capabilities/session/extension-channel.js +57 -76
  38. package/dist/capabilities/session/extension-distribution.js +115 -119
  39. package/dist/capabilities/session/extract.js +31 -21
  40. package/dist/capabilities/session/golden-score.js +55 -33
  41. package/dist/capabilities/session/health-watch.js +146 -154
  42. package/dist/capabilities/session/read-guard.js +55 -47
  43. package/dist/capabilities/session/static-flight-golden.js +49 -36
  44. package/dist/capabilities/session/transport.js +54 -61
  45. package/dist/capabilities/session/wizard.js +63 -93
  46. package/dist/capabilities/session-consent.js +143 -47
  47. package/dist/capabilities/session-login.js +95 -126
  48. package/dist/capabilities/session-search.js +391 -253
  49. package/dist/capabilities/sponsor-plugin.js +341 -0
  50. package/dist/capabilities/visa-policy.js +87 -73
  51. package/dist/capabilities/weather.js +163 -235
  52. package/dist/scripts/action-cache-tests.js +24 -59
  53. package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
  54. package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
  55. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  56. package/dist/scripts/agent-reach-tests.js +26 -18
  57. package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
  58. package/dist/scripts/anything-tests.js +58 -62
  59. package/dist/scripts/artifact-client-contract-tests.js +69 -0
  60. package/dist/scripts/artifacts-capability-tests.js +235 -0
  61. package/dist/scripts/async-collect.js +26 -15
  62. package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
  63. package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
  64. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
  65. package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
  66. package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
  67. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
  68. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
  69. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
  70. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
  71. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
  72. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
  73. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
  74. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
  75. package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
  76. package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
  77. package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
  78. package/dist/scripts/booking-executor-tests.js +80 -0
  79. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  80. package/dist/scripts/booking-saga-tests.js +61 -123
  81. package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
  82. package/dist/scripts/bootstrap-tests.js +1383 -128
  83. package/dist/scripts/build-changelog.js +89 -130
  84. package/dist/scripts/build-metrics-report.js +135 -143
  85. package/dist/scripts/changelog-tests.js +35 -82
  86. package/dist/scripts/channel-probe-tests.js +44 -116
  87. package/dist/scripts/channel-probe.js +84 -167
  88. package/dist/scripts/channel-registry-tests.js +97 -185
  89. package/dist/scripts/companion-tests.js +25 -70
  90. package/dist/scripts/dida-cookie-probe.js +87 -0
  91. package/dist/scripts/dida-sw-probe.js +114 -0
  92. package/dist/scripts/diff-test.js +17 -12
  93. package/dist/scripts/doctor-tests.js +428 -120
  94. package/dist/scripts/dsh-artifact-e2e.js +150 -0
  95. package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
  96. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  97. package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
  98. package/dist/scripts/dsh-runtime-closure.js +75 -63
  99. package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
  100. package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
  101. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  102. package/dist/scripts/effect-tests.js +165 -552
  103. package/dist/scripts/engine-run.js +1 -3
  104. package/dist/scripts/engine-tests.js +16 -10
  105. package/dist/scripts/evaluation-cadence-tests.js +69 -242
  106. package/dist/scripts/evaluation-contract-tests.js +193 -499
  107. package/dist/scripts/extension-distribution-cli.js +19 -15
  108. package/dist/scripts/extension-distribution-tests.js +98 -211
  109. package/dist/scripts/extension-tests.js +366 -389
  110. package/dist/scripts/fact-gate-tests.js +725 -263
  111. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  112. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  113. package/dist/scripts/flight-malformed-tests.js +159 -0
  114. package/dist/scripts/flyai-tests.js +325 -68
  115. package/dist/scripts/g5-guard-tests.js +131 -0
  116. package/dist/scripts/g5-guard.js +189 -0
  117. package/dist/scripts/gen-lunar-anchors.js +106 -0
  118. package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
  119. package/dist/scripts/ground-transfer-tests.js +956 -0
  120. package/dist/scripts/hbcli-e2e-tests.js +71 -90
  121. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  122. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  123. package/dist/scripts/hbcli-tests.js +41 -115
  124. package/dist/scripts/health-watch-cli.js +42 -28
  125. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  126. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  127. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  128. package/dist/scripts/i18n-tests.js +16 -9
  129. package/dist/scripts/incident-tests.js +281 -87
  130. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  131. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  132. package/dist/scripts/issue-271-liveness-tests.js +547 -0
  133. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  134. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  135. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  136. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  137. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  138. package/dist/scripts/journey-tests.js +22 -27
  139. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  140. package/dist/scripts/kernel-manifest-gate.js +126 -0
  141. package/dist/scripts/kernel-manifest-tests.js +212 -0
  142. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  143. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  144. package/dist/scripts/kernel-manifest.js +369 -0
  145. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  146. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  147. package/dist/scripts/ledger-tests.js +325 -340
  148. package/dist/scripts/ledger-workflow-crash.js +10 -20
  149. package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
  150. package/dist/scripts/memory-capture-tests.js +242 -61
  151. package/dist/scripts/memory-decay-tests.js +18 -52
  152. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  153. package/dist/scripts/memory-lifecycle.js +196 -0
  154. package/dist/scripts/memory-metrics.js +24 -12
  155. package/dist/scripts/memory-value-report.js +474 -222
  156. package/dist/scripts/memory-value-tests.js +328 -0
  157. package/dist/scripts/metrics-report-tests.js +62 -199
  158. package/dist/scripts/model-override-e2e.js +66 -105
  159. package/dist/scripts/nightly-evidence-tests.js +64 -89
  160. package/dist/scripts/nightly-evidence.js +108 -111
  161. package/dist/scripts/nudge-digest.js +39 -28
  162. package/dist/scripts/onboarding-tests.js +63 -88
  163. package/dist/scripts/opensky-check.js +18 -13
  164. package/dist/scripts/opensky-tests.js +28 -28
  165. package/dist/scripts/persona-surface-guard-tests.js +37 -12
  166. package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
  167. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  168. package/dist/scripts/price-drift-tests.js +126 -227
  169. package/dist/scripts/price-drift-watch.js +209 -253
  170. package/dist/scripts/probe-poi-tests.js +52 -45
  171. package/dist/scripts/product-metrics.js +187 -273
  172. package/dist/scripts/publish-preverify.js +47 -41
  173. package/dist/scripts/realtime-pricing-tests.js +37 -76
  174. package/dist/scripts/replay-async.js +20 -18
  175. package/dist/scripts/replay-real.js +16 -18
  176. package/dist/scripts/replay.js +52 -82
  177. package/dist/scripts/session-attach-diagnose.js +16 -12
  178. package/dist/scripts/session-attach-poc.js +34 -37
  179. package/dist/scripts/session-benchmark.js +66 -147
  180. package/dist/scripts/session-dida-live-e2e.js +151 -0
  181. package/dist/scripts/session-extract-tests.js +14 -35
  182. package/dist/scripts/session-login.js +14 -13
  183. package/dist/scripts/session-tests.js +545 -583
  184. package/dist/scripts/sf-live-benchmark.js +92 -110
  185. package/dist/scripts/sf-live-cli-tests.js +2 -9
  186. package/dist/scripts/sf-soft-score-tests.js +19 -60
  187. package/dist/scripts/sf-summary-tests.js +149 -0
  188. package/dist/scripts/sf-summary.js +466 -84
  189. package/dist/scripts/skeleton-check.js +16 -14
  190. package/dist/scripts/skeleton-integration-test.js +11 -10
  191. package/dist/scripts/skills-contract-tests.js +51 -46
  192. package/dist/scripts/smoke-session-gate-tests.js +12 -20
  193. package/dist/scripts/smoke.js +541 -422
  194. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  195. package/dist/scripts/state-cli-tests.js +226 -101
  196. package/dist/scripts/state-cli.js +549 -212
  197. package/dist/scripts/static-golden-tests.js +46 -180
  198. package/dist/scripts/time-eval-tests.js +210 -192
  199. package/dist/scripts/travel-timeline-tests.js +29 -93
  200. package/dist/scripts/turn-handoff-collect-tests.js +46 -93
  201. package/dist/scripts/turn-handoff-collect.js +71 -99
  202. package/dist/scripts/turn-policy-tests.js +28 -29
  203. package/dist/scripts/typed-contract-canary.js +77 -162
  204. package/dist/scripts/tz-resolver-sanity.js +50 -0
  205. package/dist/scripts/tz-resolver-tests.js +584 -0
  206. package/dist/scripts/unified-tests.js +23 -19
  207. package/dist/scripts/visa-policy-tests.js +31 -44
  208. package/dist/scripts/weather-tests.js +121 -543
  209. package/dist/scripts/wish-channel-gate-tests.js +29 -86
  210. package/dist/scripts/write-gate-tests.js +522 -0
  211. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  212. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  213. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  214. package/dist/scripts/z3-race-tests.js +18 -12
  215. package/dist/src/artifact-gate.js +652 -221
  216. package/dist/src/backend/kernel.js +91 -0
  217. package/dist/src/backend/modules/booking-copilot.js +55 -0
  218. package/dist/src/backend/modules/booking-executor.js +124 -0
  219. package/dist/src/backend/modules/session-search.js +194 -0
  220. package/dist/src/benchmark-agent-conformance.js +296 -262
  221. package/dist/src/benchmark-environment-bridge.js +298 -413
  222. package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
  223. package/dist/src/benchmark-tool-isolation.js +94 -67
  224. package/dist/src/bookable-facts.js +299 -174
  225. package/dist/src/booking-saga.js +64 -120
  226. package/dist/src/booking-surface/availability-policy.js +443 -576
  227. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  228. package/dist/src/booking-surface/contracts.js +24 -80
  229. package/dist/src/booking-surface/dsh-planner.js +510 -281
  230. package/dist/src/booking-surface/dsh-plugin.js +17 -1
  231. package/dist/src/booking-surface/error-codes.js +82 -10
  232. package/dist/src/booking-surface/index.js +0 -3
  233. package/dist/src/booking-surface/profile.js +10 -54
  234. package/dist/src/booking-surface/recovery-chain.js +367 -0
  235. package/dist/src/booking-surface/runtime.js +765 -891
  236. package/dist/src/booking-surface/server.js +200 -251
  237. package/dist/src/booking-surface/startup.js +58 -54
  238. package/dist/src/booking-surface/validation.js +150 -218
  239. package/dist/src/bridge.js +20 -13
  240. package/dist/src/companions.js +48 -74
  241. package/dist/src/contracts.js +21 -43
  242. package/dist/src/dsh-llm.js +82 -120
  243. package/dist/src/engine.js +253 -239
  244. package/dist/src/evaluation-cadence.js +72 -209
  245. package/dist/src/evaluation-contracts.js +220 -843
  246. package/dist/src/flight-pack-adapter.js +54 -0
  247. package/dist/src/flight-pack-contract.js +2 -0
  248. package/dist/src/gotry-backend.js +70 -0
  249. package/dist/src/hotel-date-gate.js +118 -0
  250. package/dist/src/i18n.js +18 -7
  251. package/dist/src/index.js +1425 -1629
  252. package/dist/src/journey.js +132 -81
  253. package/dist/src/ledger-repair-apply.js +538 -0
  254. package/dist/src/ledger-repair-plan.js +463 -0
  255. package/dist/src/loop.js +257 -166
  256. package/dist/src/memory-capture.js +180 -32
  257. package/dist/src/memory-decay.js +30 -14
  258. package/dist/src/memory-lifecycle.js +1310 -0
  259. package/dist/src/memory-utility.js +27 -40
  260. package/dist/src/mock-llm.js +36 -61
  261. package/dist/src/model.js +81 -36
  262. package/dist/src/realtime-pricing.js +63 -72
  263. package/dist/src/slot-spec.js +84 -106
  264. package/dist/src/state-ledger.js +310 -471
  265. package/dist/src/subagent-job-id-guard.js +58 -0
  266. package/dist/src/time-anchor.js +231 -33
  267. package/dist/src/tool-packet.js +20 -8
  268. package/dist/src/travel-slots.js +58 -35
  269. package/dist/src/travel-timeline.js +44 -63
  270. package/dist/src/turn-deadline.js +121 -127
  271. package/dist/src/turn-policy.js +61 -89
  272. package/dist/src/tz-resolver.js +143 -0
  273. package/dist/src/unified.js +504 -381
  274. package/dist/src/wish-pool.js +39 -14
  275. package/dist/src/write-gate.js +495 -0
  276. package/dist/src/z3-shared.js +273 -7
  277. package/extension/README.md +4 -3
  278. package/extension/background.js +125 -14
  279. package/extension/content-bridge.js +23 -1
  280. package/extension/content-main.js +15 -4
  281. package/extension/manifest.json +16 -8
  282. package/package.json +251 -220
  283. package/ts/capabilities/agent-reach.ts +4 -4
  284. package/ts/capabilities/anything.ts +1 -1
  285. package/ts/capabilities/artifacts.ts +46 -9
  286. package/ts/capabilities/flyai.ts +156 -37
  287. package/ts/capabilities/hbcli.ts +1 -1
  288. package/ts/capabilities/incident-log.ts +88 -59
  289. package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
  290. package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
  291. package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
  292. package/ts/capabilities/session/extension-bridge.ts +226 -101
  293. package/ts/capabilities/session/extension-channel.ts +22 -9
  294. package/ts/capabilities/session/wizard.ts +2 -2
  295. package/ts/capabilities/session-consent.ts +98 -14
  296. package/ts/capabilities/session-login.ts +1 -0
  297. package/ts/capabilities/session-search.ts +315 -31
  298. package/ts/dsh-runtime/vendor/README.md +8 -4
  299. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
  300. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
  301. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
  302. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
  303. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
  304. package/ts/package.json +236 -16
  305. package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
  306. package/ts/scripts/state-cli.ts +418 -51
  307. package/ts/src/artifact-gate.ts +561 -15
  308. package/ts/src/benchmark-environment-bridge.ts +37 -20
  309. package/ts/src/bookable-facts.ts +183 -7
  310. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  311. package/ts/src/booking-surface/dsh-planner.ts +240 -32
  312. package/ts/src/booking-surface/dsh-plugin.js +17 -1
  313. package/ts/src/booking-surface/error-codes.ts +75 -0
  314. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  315. package/ts/src/booking-surface/server.ts +84 -51
  316. package/ts/src/booking-surface/validation.ts +2 -0
  317. package/ts/src/contracts.ts +12 -2
  318. package/ts/src/dsh-llm.ts +4 -8
  319. package/ts/src/index.ts +609 -90
  320. package/ts/src/loop.ts +116 -7
  321. package/ts/src/memory-capture.ts +227 -3
  322. package/ts/src/mock-llm.ts +4 -8
  323. package/ts/src/model.ts +88 -2
  324. package/ts/src/state-ledger.ts +47 -29
  325. package/ts/src/subagent-job-id-guard.ts +72 -0
  326. package/ts/src/time-anchor.ts +217 -13
  327. package/ts/src/unified.ts +316 -54
@@ -29,6 +29,8 @@ import {
29
29
  selectDshRuntime,
30
30
  supportsNodeVersion,
31
31
  } from './gotry-runtime-resolution.js'
32
+ import { onboardingSkipReason, orchestrateWebLaunch } from './gotry-bootstrap.js'
33
+ import { spawnOwnedChild, terminateOwnedChild } from './gotry-process-liveness.js'
32
34
 
33
35
  const here = dirname(fileURLToPath(import.meta.url))
34
36
  const repoRoot = join(here, '..')
@@ -81,6 +83,9 @@ if (help) {
81
83
 
82
84
  Usage:
83
85
  gotry web # dsh Web UI on http://127.0.0.1:3080
86
+ # (interactive TTY) 启动前若发现可自动安装的可选能力,问一次是否配置;
87
+ # 确认复用 doctor --fix 幂等安装,拒绝立即继续。CI/非 TTY/全健康不弹问。
88
+ # 跳过该问: --no-onboarding 或 GOTRY_ONBOARDING_SKIP=1
84
89
  gotry setup # 扩展就位检查/指引(商店一键装)
85
90
  gotry setup calendar # 可选日历(CalDAV 工作窗口)挂载开关:默认关;--off 关闭;--status 查看
86
91
  gotry doctor # 可选依赖体检:扩展/agent-reach/hbcli/flyai/sidebar 状态 + 补装指引
@@ -248,6 +253,9 @@ function projectBenchmarkPatch(raw, entryPath, configPath) {
248
253
  // can make dsh fail while assembling the first request. Require the exact
249
254
  // source shape before replacing it; malformed or ambiguous prompt config must
250
255
  // fail closed rather than being partially interpreted.
256
+ // Target dsh-system-prompt Config exposes only personaPrefix / personaSuffix;
257
+ // the legacy `persona:` key does not project through. Validate against the
258
+ // current personaPrefix key.
251
259
  function projectBenchmarkSystemPrompt(lines) {
252
260
  const rootStarts = lines.map((line, index) => /^-\s+/.test(line) ? index : -1).filter(index => index >= 0)
253
261
  const rootItems = rootStarts.map((itemStart, index) => lines.slice(itemStart, rootStarts[index + 1] ?? lines.length))
@@ -260,7 +268,12 @@ function projectBenchmarkSystemPrompt(lines) {
260
268
  while (end < lines.length && (lines[end].trim() === '' || /^\s/.test(lines[end]) || lines[end].trim().startsWith('#'))) end += 1
261
269
  const item = lines.slice(start, end)
262
270
  const configIndexes = item.map((line, index) => /^ {2}config:\s*$/.test(line) ? index : -1).filter(index => index >= 0)
263
- const personaIndexes = item.map((line, index) => /^ {4}persona:\s*>-\s*$/.test(line) ? index : -1).filter(index => index >= 0)
271
+ const personaIndexes = item.map((line, index) => /^ {4}personaPrefix:\s*>-\s*$/.test(line) ? index : -1).filter(index => index >= 0)
272
+ // Reject any leftover legacy `persona:` field under config: — it would be
273
+ // silently dropped by the target Config and leave the kernel persona-less.
274
+ // The minimal kernel must project through personaPrefix only.
275
+ const legacyPersonaIndexes = item.map((line, index) => /^ {4}persona:\s*>-\s*$/.test(line) ? index : -1).filter(index => index >= 0)
276
+ if (legacyPersonaIndexes.length !== 0) throw new Error('benchmark system-prompt legacy persona key not allowed under target 0.1.5-rc.1')
264
277
  if (configIndexes.length !== 1 || personaIndexes.length !== 1 || personaIndexes[0] <= configIndexes[0]) {
265
278
  throw new Error('benchmark system-prompt config violation')
266
279
  }
@@ -268,7 +281,7 @@ function projectBenchmarkSystemPrompt(lines) {
268
281
  const blockEnd = item.slice(persona + 1).findIndex(line => line.trim() !== '' && !/^\s{6,}/.test(line))
269
282
  const contentEnd = blockEnd < 0 ? item.length : persona + 1 + blockEnd
270
283
  if (contentEnd === persona + 1 || item.slice(persona + 1, contentEnd).some(line => line.trim() !== '' && !/^\s{6,}/.test(line))) {
271
- throw new Error('benchmark system-prompt persona violation')
284
+ throw new Error('benchmark system-prompt personaPrefix block violation')
272
285
  }
273
286
  const before = item.slice(1, configIndexes[0]).filter(line => line.trim() !== '' && !line.trim().startsWith('#'))
274
287
  const between = item.slice(configIndexes[0] + 1, persona).filter(line => line.trim() !== '' && !line.trim().startsWith('#'))
@@ -277,7 +290,7 @@ function projectBenchmarkSystemPrompt(lines) {
277
290
  const replacement = [
278
291
  '- id: system-prompt',
279
292
  ' config:',
280
- ' persona: >-',
293
+ ' personaPrefix: >-',
281
294
  ' You are GoTry, a task-agnostic travel planning assistant.',
282
295
  ' Use only the current conversation and tools available in this benchmark session.',
283
296
  ]
@@ -367,9 +380,10 @@ if (benchmarkEnvironmentConfig) {
367
380
  }
368
381
  // dsh-map-tools 宿主插件(地图/路线/POI,零 key 走 OSM/OSRM):tarball 随包 vendor,
369
382
  // repo 工作副本走 runtime workspace 链接/依赖解析;都找不到就整块剔除 patch 条目
370
- // (缺地图不挡旅行规划)。不能改成 npm 依赖:其 peerDependencies 要求
371
- // dsh-settings/dsh-tools >=0.1.2-rc.1,与本包锁定的 0.1.2-alpha.3 家族在 npm
372
- // 严格 peer 解析下 ERESOLVE,会直接弄坏 npx 安装。
383
+ // (缺地图不挡旅行规划)。保持 vendored 形态而非 npm 依赖:历史上游 peerDependencies
384
+ // (dsh-settings/dsh-tools >=0.1.2-rc.1)与锁定的 0.1.2-alpha.3 家族在 npm 严格 peer
385
+ // 解析下 ERESOLVE;当前锁定 0.1.5-rc.1 家族,vendored 副本 peerDependencies 已
386
+ // 对齐 0.1.5-rc.1,继续以 vendored 形态复用同一份适配补丁与七工具接线。
373
387
  let mapEntry = ''
374
388
  if (!benchmarkEnvironmentConfig) {
375
389
  const vendoredMap = join(repoRoot, 'ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js')
@@ -423,7 +437,7 @@ if (mapEntry) {
423
437
  // 未配置的日历工具是纯负资产(会话中段才撞「未配置 username」报错),gotry 对它的
424
438
  // 唯一诉求(工作窗口)由访谈首轮覆盖。挂载与否由 **setup 状态面**决定(founder
425
439
  // 2026-09-03 纠偏:禁止环境变量控制产品行为;可选依赖进 setup 状态管理,与扩展
426
- // manifest 同居 ~/.gotry)——`npx gotry setup calendar` 写 ~/.gotry/calendar.json,
440
+ // manifest 同居 ~/.gotry)——`npx @danceiny/gotry setup calendar` 写 ~/.gotry/calendar.json,
427
441
  // `--off` 删除恢复默认;doctor 报告三态。
428
442
  let calEnabled = false
429
443
  try {
@@ -479,17 +493,86 @@ const cleanupPatch = () => {
479
493
  rmSync(patchDir, { recursive: true, force: true })
480
494
  }
481
495
  process.once('exit', cleanupPatch)
482
- let child = null
483
- const terminateOnSignal = (signal, listener) => {
484
- cleanupPatch()
485
- if (child && !child.killed) child.kill(signal)
486
- process.off(signal, listener)
487
- process.kill(process.pid, signal)
496
+ // 信号转发(issue #267 P1):spawnSync 阻塞 JS,信号处理器无法服务、finally 也跑不到。
497
+ // web-onboarding 子调用改 awaited spawn。SIGINT/SIGTERM 转发在 onboarding **之前**注册一次,
498
+ // 并贯穿后续 dsh web 子进程的整个生命周期——服务管理器只向父进程发 SIGTERM 时,无论当前活跃的是
499
+ // onboarding 子进程组还是 dsh 子,都收到同一信号。收到信号时:终止当前活跃子进程/进程组,
500
+ // 等待 bounded close/reap,清 patch + 私有 result 目录(均幂等),移除本监听器,再向自己重发信号恢复原生终止语义。
501
+ // signalHandling 护栏防递归/重复处理(重发信号不再二次进入本回调)。setup/doctor 的 spawnSync 不在此窗口,保持不变。
502
+ let child = null // dsh web 子进程(launchWeb 后)
503
+ let childGroupPid = null // POSIX dsh child process-group leader
504
+ let onboardingChild = null // onboarding 子进程(运行中时);POSIX 下独立进程组
505
+ let onboardingChildGroupPid = null
506
+ let onboardingResultDir = null // 私有结果目录;信号/正常/错误路径都清
507
+ // outer grace 必须大于 bootstrap installer cleanup budget(2s TERM + 1s SIGKILL wait)。
508
+ // 否则 stubborn installer 忽略 TERM 时,inner 可能先杀 bootstrap,导致 bootstrap 来不及 SIGKILL
509
+ // 它自己 detached 出去的 installer 进程组。
510
+ const ONBOARDING_TERM_GRACE_MS = 5_000
511
+ const ONBOARDING_KILL_WAIT_MS = 1_000
512
+ const cleanupOnboardingResult = () => {
513
+ if (!onboardingResultDir) return
514
+ const d = onboardingResultDir
515
+ onboardingResultDir = null
516
+ try { rmSync(d, { recursive: true, force: true }) } catch { /* ignore */ }
517
+ }
518
+ const onboardingChildTimeoutMs = (() => {
519
+ const raw = process.env.GOTRY_ONBOARDING_CHILD_TIMEOUT_MS
520
+ if (raw === undefined || raw === '') return 300_000
521
+ if (!/^[1-9]\d*$/.test(raw)) return 300_000
522
+ const n = Number(raw)
523
+ return Number.isSafeInteger(n) && n > 0 ? n : 300_000
524
+ })()
525
+ let signalHandling = false
526
+ const signalListeners = new Map()
527
+ const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
528
+ const signalChild = (proc, sig, groupPid = null) => {
529
+ if (!proc) return
530
+ if (process.platform !== 'win32' && groupPid) {
531
+ try { process.kill(-groupPid, sig); return } catch { /* fallback */ }
532
+ }
533
+ try { proc.kill(sig) } catch { /* ignore */ }
534
+ }
535
+ const terminateChildBounded = async (proc, sig, groupPid = null, graceMs = ONBOARDING_TERM_GRACE_MS, killMs = ONBOARDING_KILL_WAIT_MS) => {
536
+ await terminateOwnedChild({ child: proc, groupPid, signal: sig, termGraceMs: graceMs, killWaitMs: killMs })
537
+ }
538
+ const registerSignalForwarding = () => {
539
+ if (signalListeners.size) return
540
+ for (const sig of ['SIGINT', 'SIGTERM']) {
541
+ const listener = () => {
542
+ if (signalHandling) return // 防递归:重发信号不再二次进入
543
+ signalHandling = true
544
+ void (async () => {
545
+ const active = onboardingChild || child
546
+ const groupPid = onboardingChild ? onboardingChildGroupPid : childGroupPid
547
+ cleanupPatch()
548
+ cleanupOnboardingResult()
549
+ await terminateChildBounded(active, sig, groupPid)
550
+ for (const [s, l] of signalListeners) process.off(s, l)
551
+ signalListeners.clear()
552
+ process.kill(process.pid, sig) // 移除监听器后重发 → 原生终止语义
553
+ await sleep(2_000)
554
+ process.exit(128 + (sig === 'SIGINT' ? 2 : 15))
555
+ })()
556
+ }
557
+ process.on(sig, listener)
558
+ signalListeners.set(sig, listener)
559
+ }
560
+ }
561
+
562
+ const unregisterSignalForwarding = () => {
563
+ for (const [s, l] of signalListeners) process.off(s, l)
564
+ signalListeners.clear()
488
565
  }
489
566
  // LLM key 由 dsh 宿主管理(凭证是用户资产,UI 在 dsh 里;gotry 不拦截启动期,
490
567
  // 不在用户面前展示任何 key 配置引导),此处直接放手 spawn dsh。
568
+ const webPortArgs = (() => {
569
+ if (mode !== 'web') return []
570
+ const portIndex = rest.indexOf('--port')
571
+ const port = portIndex >= 0 ? rest[portIndex + 1] : undefined
572
+ return port !== undefined && /^(?:0|[1-9]\d*)$/.test(port) ? ['--port', port] : []
573
+ })()
491
574
  const binJs = mode === 'web'
492
- ? ['web', '--patch', patchPath, ...(process.argv.includes('--no-open') ? ['--no-open'] : [])]
575
+ ? ['web', '--patch', patchPath, ...(process.argv.includes('--no-open') ? ['--no-open'] : []), ...webPortArgs]
493
576
  : ['--profile', 'headless', '--patch', patchPath, ...rest]
494
577
 
495
578
  const childEnv = { ...process.env }
@@ -517,21 +600,106 @@ const childStdio = mode === 'web'
517
600
  let benchmarkCapturedBytes = 0
518
601
  let benchmarkDiagnosticBuffer = Buffer.alloc(0)
519
602
  let benchmarkOutputTruncated = false
520
- // 启动一次性 doctor 摘要(issue #114,design §3.1③):分离子进程后台跑只读体检,
521
- // 有待处理项打一行 stderr(stderr 继承,不污染 stdout;benchmark 面保持零杂音)。
522
- // detached+unref:不阻塞不拖慢启动;inner 生命周期天然一次,不重复刷;失败静默。
523
- if (!benchmarkEnvironmentConfig) {
524
- try {
525
- spawn(process.execPath, [join(here, 'gotry-bootstrap.js'), 'doctor', '--summary'], {
526
- detached: true,
527
- stdio: ['ignore', 'ignore', 'inherit'],
528
- }).unref()
529
- } catch { /* 体检不可用不挡启动 */ }
530
- }
531
- child = spawn(process.execPath, [dshBin, ...binJs], {
532
- stdio: childStdio,
533
- env: childEnv,
534
- cwd: dshCwd,
603
+ // issue #258:`gotry web` 启动前的交互式 onboarding——每次符合条件的启动评估一次、至多问一次,
604
+ // 无跨启动持久确认(每次 web 启动独立判定;不写「已问过」标记)。仅交互式 TTY + 有可自动安装
605
+ // 缺项时问一次;y 复用 doctor --fix 幂等安装器,不建第二套;n 立即继续 web)。
606
+ // CI/benchmark/非 TTY/全健康/GOTRY_SETUP_SKIP=1/GOTRY_ONBOARDING_SKIP=1/--no-onboarding
607
+ // 均零 prompt 零安装,仍启 web;永不 postinstall 或后台任务里安装。无 auto 但有需用户操作/
608
+ // 不可用缺项时( win32 平台不支持自动安装),onboarding 渲染分类计划与原因、不 prompt 不安装,
609
+ // 标记 reported;orchestrateWebLaunch 据此抑制重复的 detached doctor 摘要(design §3.1③,#114)。
610
+ // 编排走 orchestrateWebLaunch(显式注入依赖的 E2E 边界缝,bootstrap-tests 跨边界单测同款):
611
+ // bootstrap 以 `onboarding --result-file=…` 异步 inherit 跑(awaited spawn,POSIX 下独立进程组——
612
+ // 使 JS 能服务 SIGINT/SIGTERM 并在信号路径清 patch+result 目录、reap installer 子树);result JSON 告知是否 prompt /
613
+ // reported,用以抑制重复的启动摘要行。yes/no/失败/跳过/已 reported 均继续到 launchWeb(spawn dsh web)。
614
+ const onboardingSkip = onboardingSkipReason({
615
+ mode,
616
+ benchmark: Boolean(benchmarkEnvironmentConfig),
617
+ env: process.env,
618
+ argv: process.argv,
619
+ isTTY: process.stdin.isTTY,
620
+ })
621
+ // 一次性注册信号转发,覆盖 onboarding + dsh child 全生命周期(dsh close/error 时注销)。
622
+ registerSignalForwarding()
623
+ await orchestrateWebLaunch({
624
+ onboardingSkip,
625
+ runOnboarding: async () => {
626
+ // result 通道硬化:不在 os.tmpdir 下放可预测文件名——mkdtempSync 建 0700 私有目录,
627
+ // result.json 放其内;bootstrap 以 mode 0600 + flag wx 写(存在路径/符号链接永不覆盖,
628
+ // 防 symlink 互换攻击)。子调用改 awaited spawn(非 spawnSync)使 JS 能服务信号;
629
+ // 信号/正常/错误路径都清目录(幂等),详见上方 registerSignalForwarding/cleanupOnboardingResult。
630
+ let resultDir = null
631
+ try {
632
+ resultDir = mkdtempSync(join(tmpdir(), 'gotry-onboarding-'))
633
+ onboardingResultDir = resultDir
634
+ const resultFile = join(resultDir, 'result.json')
635
+ // 信号转发在 onboarding 之前注册一次,贯穿后续 dsh web 整个生命周期(见上方 registerSignalForwarding);
636
+ // onboarding 正常结束后不注销——服务管理器只向父进程发信号时,当前活跃子进程(onboarding 或 dsh)均收到。
637
+ registerSignalForwarding()
638
+ const detachedOnboarding = process.platform !== 'win32'
639
+ const oc = spawn(process.execPath, [join(here, 'gotry-bootstrap.js'), 'onboarding', `--result-file=${resultFile}`], {
640
+ stdio: 'inherit',
641
+ detached: detachedOnboarding,
642
+ })
643
+ onboardingChild = oc
644
+ onboardingChildGroupPid = detachedOnboarding ? oc.pid : null
645
+ // onboarding 永不挡 web:保留 spawnSync 原有的 300s 上限(可经 GOTRY_ONBOARDING_CHILD_TIMEOUT_MS 注入以供
646
+ // 确定性测试,生产默认 300_000)。超时即 SIGTERM(2s 后 SIGKILL 兜底),settle 恰好一次于 close/error/timeout;
647
+ // 之后继续 web(prompted:false),不留进程/目录(finally 清 result,信号路径清 patch+result)。
648
+ await new Promise((resolve) => {
649
+ let done = false
650
+ let killTimer = null
651
+ const timer = setTimeout(() => {
652
+ process.stderr.write(`[gotry] onboarding child timeout after ${onboardingChildTimeoutMs}ms; continuing to web\n`)
653
+ signalChild(oc, 'SIGTERM', onboardingChildGroupPid)
654
+ killTimer = setTimeout(() => { if (!done) signalChild(oc, 'SIGKILL', onboardingChildGroupPid) }, ONBOARDING_TERM_GRACE_MS)
655
+ }, onboardingChildTimeoutMs)
656
+ const finish = () => { if (done) return; done = true; clearTimeout(timer); if (killTimer) clearTimeout(killTimer); resolve() }
657
+ oc.once('close', finish)
658
+ oc.once('error', (error) => {
659
+ process.stderr.write(`[gotry] onboarding child spawn failed: ${error.code || error.name || 'error'}; continuing to web\n`)
660
+ finish()
661
+ })
662
+ })
663
+ if (signalHandling) await new Promise(() => {})
664
+ onboardingChild = null
665
+ onboardingChildGroupPid = null
666
+ if (existsSync(resultFile)) {
667
+ try { return JSON.parse(readFileSync(resultFile, 'utf-8')) }
668
+ catch { return { prompted: false } }
669
+ }
670
+ return { prompted: false }
671
+ } catch {
672
+ process.stderr.write('[gotry] onboarding setup failed; continuing to web\n')
673
+ return { prompted: false }
674
+ } finally {
675
+ onboardingChild = null
676
+ onboardingChildGroupPid = null
677
+ if (resultDir) { try { rmSync(resultDir, { recursive: true, force: true }) } catch { /* ignore */ } }
678
+ onboardingResultDir = null
679
+ }
680
+ },
681
+ // 启动一次性 doctor 摘要(issue #114,design §3.1③):分离子进程后台跑只读体检,
682
+ // 有待处理项打一行 stderr(stderr 继承,不污染 stdout;benchmark 面保持零杂音)。
683
+ // onboarding 已 prompt 时 orchestrateWebLaunch 已抑制本回调(不重复)。
684
+ runSummary: () => {
685
+ if (benchmarkEnvironmentConfig) return
686
+ try {
687
+ spawn(process.execPath, [join(here, 'gotry-bootstrap.js'), 'doctor', '--summary'], {
688
+ detached: true,
689
+ stdio: ['ignore', 'ignore', 'inherit'],
690
+ }).unref()
691
+ } catch { /* 体检不可用不挡启动 */ }
692
+ },
693
+ // dsh web 启动边界:launchWeb 即 inner 跨过 onboarding 后到达的 web 启动标记。
694
+ launchWeb: async () => {
695
+ const owned = spawnOwnedChild(process.execPath, [dshBin, ...binJs], {
696
+ stdio: childStdio,
697
+ env: childEnv,
698
+ cwd: dshCwd,
699
+ })
700
+ child = owned.child
701
+ childGroupPid = owned.groupPid
702
+ },
535
703
  })
536
704
  if (benchmarkStdout) {
537
705
  const maxBytes = Math.max(1, Number(benchmarkTerminalConfig?.max_bytes ?? 1))
@@ -569,8 +737,37 @@ const reportBenchmarkFailure = reason => {
569
737
  process.stderr.write(`[gotry] benchmark terminal output unavailable (${reason})\n`)
570
738
  }
571
739
 
572
- child.on('close', (code, signal) => {
740
+ let childLifecycleHandled = false
741
+ let childCleanupPromise = null
742
+ // `close` may be delayed by a descendant which inherited dsh's stdio pipe.
743
+ // Always start bounded TERM→KILL cleanup from `exit` so the owned group is
744
+ // reaped before any close/parser wait. The benchmark zero-success close path
745
+ // still parses the already-captured stdout buffer unchanged; cleanup runs
746
+ // fire-and-forget in the background and stays bounded by ONBOARDING bounds.
747
+ const startChildCleanup = () => {
748
+ if (childCleanupPromise || !child) return childCleanupPromise
749
+ childCleanupPromise = terminateOwnedChild({
750
+ child,
751
+ groupPid: childGroupPid,
752
+ signal: 'SIGTERM',
753
+ termGraceMs: ONBOARDING_TERM_GRACE_MS,
754
+ killWaitMs: ONBOARDING_KILL_WAIT_MS,
755
+ })
756
+ return childCleanupPromise
757
+ }
758
+ child.on('exit', (code, signal) => {
759
+ void startChildCleanup()
760
+ })
761
+ child.on('close', async (code, signal) => {
762
+ if (childLifecycleHandled) return
763
+ childLifecycleHandled = true
573
764
  cleanupPatch()
765
+ if (signalHandling) return
766
+ unregisterSignalForwarding()
767
+ // Non-benchmark web/headless children are owned through both normal and
768
+ // anomalous exits. The promise was normally started by `exit`; retaining
769
+ // this close fallback covers runtimes that report close without exit.
770
+ if (!benchmarkStdout || code !== 0 || signal) await startChildCleanup()
574
771
  if (benchmarkStdout) {
575
772
  const captured = Buffer.concat(benchmarkStdout).toString('utf8')
576
773
  if (code !== 0 || signal || benchmarkOutputTruncated) {
@@ -613,21 +810,43 @@ child.on('close', (code, signal) => {
613
810
  // D-NEW 护栏:dsh 异常退出也写一条 incident,留现场而非沉默
614
811
  // dist 模式 import JS(node_modules 下的 .ts 会被 Node 拒 strip)
615
812
  const incidentModule = distModuleMode && existsSync(join(repoRoot, 'dist/capabilities/incident-log.js'))
616
- ? '../dist/capabilities/incident-log.js'
617
- : '../ts/capabilities/incident-log.ts'
618
- import(incidentModule).then(({ recordIncident }) => {
813
+ ? join(repoRoot, 'dist/capabilities/incident-log.js')
814
+ : join(repoRoot, 'ts/capabilities/incident-log.ts')
815
+ try {
816
+ const { recordIncident } = await import(pathToFileURL(incidentModule).href)
619
817
  recordIncident({
620
818
  ts: new Date().toISOString(),
621
819
  kind: 'plugin_error',
622
820
  message: `gotry spawn exit: ${mode} code=${code} signal=${signal}`,
623
821
  source: 'gotry-cli',
624
- }, repoRoot).catch(() => {})
625
- }).catch(() => {}) // module 找不到时静默
822
+ }, repoRoot)
823
+ } catch { /* diagnostics remain best-effort; fatal exit is preserved */ }
824
+ }
825
+ if (signal) {
826
+ try { process.kill(process.pid, signal) } catch { process.exit(1) }
827
+ return
626
828
  }
627
829
  process.exit(code ?? 1)
628
830
  })
629
- child.on('error', (e) => {
831
+ child.on('error', async (e) => {
832
+ if (childLifecycleHandled) return
833
+ childLifecycleHandled = true
630
834
  cleanupPatch()
835
+ if (signalHandling) return
836
+ unregisterSignalForwarding()
837
+ await startChildCleanup()
838
+ try {
839
+ const incidentModule = distModuleMode && existsSync(join(repoRoot, 'dist/capabilities/incident-log.js'))
840
+ ? join(repoRoot, 'dist/capabilities/incident-log.js')
841
+ : join(repoRoot, 'ts/capabilities/incident-log.ts')
842
+ const { recordIncident } = await import(pathToFileURL(incidentModule).href)
843
+ recordIncident({
844
+ ts: new Date().toISOString(),
845
+ kind: 'plugin_error',
846
+ message: `gotry spawn error: ${e instanceof Error ? e.message : String(e)}`,
847
+ source: 'gotry-cli',
848
+ }, repoRoot)
849
+ } catch { /* diagnostics remain best-effort; fatal exit is preserved */ }
631
850
  if (benchmarkEnvironmentConfig) {
632
851
  reportBenchmarkFailure('child_spawn_failure')
633
852
  return
@@ -0,0 +1,27 @@
1
+ import type { ChildProcess, SpawnOptions } from 'node:child_process'
2
+
3
+ export interface OwnedChild {
4
+ child: ChildProcess
5
+ groupPid: number | null
6
+ }
7
+
8
+ export interface OwnedChildCleanupResult {
9
+ childExited: boolean
10
+ groupEmpty: boolean
11
+ forced: boolean
12
+ }
13
+
14
+ export interface OwnedChildCleanupOptions {
15
+ child?: ChildProcess | null
16
+ groupPid?: number | null
17
+ signal?: NodeJS.Signals
18
+ termGraceMs?: number
19
+ killWaitMs?: number
20
+ }
21
+
22
+ export declare function spawnOwnedChild(command: string, args: readonly string[], options?: SpawnOptions): OwnedChild
23
+ export declare function signalOwnedChild(child: ChildProcess | null | undefined, signal: NodeJS.Signals, groupPid?: number | null): void
24
+ export declare function isOwnedProcessGroupEmpty(groupPid: number | null): boolean
25
+ export declare function waitForOwnedChildQuiescence(child: ChildProcess, groupPid?: number | null, timeoutMs?: number): Promise<boolean>
26
+ export declare function terminateOwnedChild(options?: OwnedChildCleanupOptions): Promise<OwnedChildCleanupResult>
27
+ export declare const PROCESS_LIVENESS_BOUNDS: Readonly<{ termGraceMs: number; killWaitMs: number }>
@@ -0,0 +1,97 @@
1
+ /**
2
+ * GoTry-owned lifecycle for one externally spawned dsh child.
3
+ *
4
+ * The dsh CLI is an external process boundary. On POSIX we make it the
5
+ * leader of its own process group so a parent-only signal or a failed dsh
6
+ * child cannot strand descendants. This module owns only bounded signalling
7
+ * and quiescence observation; incident formatting remains in the existing
8
+ * GoTry incident writer.
9
+ */
10
+
11
+ import { spawn } from 'node:child_process'
12
+
13
+ const DEFAULT_TERM_GRACE_MS = 5_000
14
+ const DEFAULT_KILL_WAIT_MS = 1_000
15
+ const POLL_MS = 20
16
+
17
+ /** Spawn a child in a private process group where the host supports it. */
18
+ export function spawnOwnedChild(command, args, options = {}) {
19
+ const child = spawn(command, args, {
20
+ ...options,
21
+ detached: process.platform !== 'win32',
22
+ })
23
+ return {
24
+ child,
25
+ groupPid: process.platform !== 'win32' ? (child.pid ?? null) : null,
26
+ }
27
+ }
28
+
29
+ /** Signal the child group, falling back to the direct child when necessary. */
30
+ export function signalOwnedChild(child, signal, groupPid = null) {
31
+ if (!child) return
32
+ if (process.platform !== 'win32' && groupPid !== null) {
33
+ try {
34
+ process.kill(-groupPid, signal)
35
+ return
36
+ } catch {
37
+ // The group can disappear between observation and delivery. Try the
38
+ // direct handle as a best-effort race fallback.
39
+ }
40
+ }
41
+ try { child.kill(signal) } catch { /* exit races are contained */ }
42
+ }
43
+
44
+ function childExited(child) {
45
+ return child.exitCode !== null || child.signalCode !== null
46
+ }
47
+
48
+ function groupEmpty(groupPid) {
49
+ if (process.platform === 'win32' || groupPid === null) return true
50
+ try {
51
+ process.kill(-groupPid, 0)
52
+ return false
53
+ } catch (error) {
54
+ return error?.code === 'ESRCH'
55
+ }
56
+ }
57
+
58
+ /** Expose the same group-emptiness fact used by the bounded cleanup proof. */
59
+ export function isOwnedProcessGroupEmpty(groupPid) {
60
+ return groupEmpty(groupPid)
61
+ }
62
+
63
+ /** Wait for the direct child and, on POSIX, its whole private group. */
64
+ export async function waitForOwnedChildQuiescence(child, groupPid = null, timeoutMs = DEFAULT_TERM_GRACE_MS) {
65
+ const deadline = Date.now() + timeoutMs
66
+ while (Date.now() <= deadline) {
67
+ if (childExited(child) && groupEmpty(groupPid)) return true
68
+ await new Promise((resolve) => setTimeout(resolve, POLL_MS))
69
+ }
70
+ return childExited(child) && groupEmpty(groupPid)
71
+ }
72
+
73
+ /**
74
+ * Bounded TERM → KILL cleanup for one owned child group. It resolves only
75
+ * after the direct child and group are observed empty, or after the bound.
76
+ */
77
+ export async function terminateOwnedChild({
78
+ child,
79
+ groupPid = null,
80
+ signal = 'SIGTERM',
81
+ termGraceMs = DEFAULT_TERM_GRACE_MS,
82
+ killWaitMs = DEFAULT_KILL_WAIT_MS,
83
+ } = {}) {
84
+ if (!child) return { childExited: true, groupEmpty: true, forced: false }
85
+ signalOwnedChild(child, signal, groupPid)
86
+ if (await waitForOwnedChildQuiescence(child, groupPid, termGraceMs)) {
87
+ return { childExited: true, groupEmpty: true, forced: false }
88
+ }
89
+ signalOwnedChild(child, 'SIGKILL', groupPid)
90
+ const empty = await waitForOwnedChildQuiescence(child, groupPid, killWaitMs)
91
+ return { childExited: childExited(child), groupEmpty: empty, forced: true }
92
+ }
93
+
94
+ export const PROCESS_LIVENESS_BOUNDS = Object.freeze({
95
+ termGraceMs: DEFAULT_TERM_GRACE_MS,
96
+ killWaitMs: DEFAULT_KILL_WAIT_MS,
97
+ })
@@ -8,7 +8,7 @@ export type DshRuntime = {
8
8
  source: 'root'
9
9
  }
10
10
 
11
- export const REQUIRED_BENCHMARK_DSH_VERSION: '0.1.2-alpha.3'
11
+ export const REQUIRED_BENCHMARK_DSH_VERSION: '0.1.5-rc.1'
12
12
 
13
13
  export function readDshPackage(bin: string): Omit<DshRuntime, 'source'> | null
14
14
  export function resolveDshPackage(resolver: DshResolver): Omit<DshRuntime, 'source'> | null
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from 'node:fs'
2
2
  import { dirname, join } from 'node:path'
3
3
 
4
- export const REQUIRED_BENCHMARK_DSH_VERSION = '0.1.2-alpha.3'
4
+ export const REQUIRED_BENCHMARK_DSH_VERSION = '0.1.5-rc.1'
5
5
 
6
6
  export function readDshPackage(bin) {
7
7
  try {
@@ -34,7 +34,7 @@ export function selectDshRuntime({ rootResolver }) {
34
34
  if (root) return { ...root, source: 'root' }
35
35
  // D-27 清偿(issue #120):legacy vendored 回退路径已移除——该形态的 Node 兼容窗口
36
36
  // 断裂(Round 5 起「不承诺可运行」),解析成功只会把「dsh 缺失」变成下游玄学失败。
37
- // 唯一受支持形态 = root manifest / 依赖解析(216 精确依赖闭包);找不到即 fail-closed,
37
+ // 唯一受支持形态 = root manifest / 依赖解析(232 精确依赖闭包);找不到即 fail-closed,
38
38
  // 由调用方给明确重装指引。benchmark 原本就禁用回退,语义不变(入参保留以稳调用面)。
39
39
  return null
40
40
  }