@gajae-code/coding-agent 0.13.2 → 0.14.0

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 (610) hide show
  1. package/CHANGELOG.md +215 -8
  2. package/README.md +2 -0
  3. package/bin/gajaessi.js +4 -0
  4. package/dist/types/async/job-manager.d.ts +55 -2
  5. package/dist/types/async/support.d.ts +14 -0
  6. package/dist/types/capability/mcp.d.ts +6 -0
  7. package/dist/types/capability/types.d.ts +6 -0
  8. package/dist/types/cli/accounts-cli.d.ts +19 -0
  9. package/dist/types/cli/args.d.ts +3 -0
  10. package/dist/types/cli/config-cli.d.ts +6 -0
  11. package/dist/types/cli/crash-cli.d.ts +10 -0
  12. package/dist/types/cli/customize-doctor.d.ts +145 -0
  13. package/dist/types/cli/daemon-cli.d.ts +3 -1
  14. package/dist/types/cli/mcp-cli.d.ts +11 -0
  15. package/dist/types/cli/notify-cli.d.ts +1 -1
  16. package/dist/types/cli/root-flags.d.ts +12 -0
  17. package/dist/types/cli/setup-cli.d.ts +3 -1
  18. package/dist/types/cli/skills-cli.d.ts +2 -1
  19. package/dist/types/cli/update-cli.d.ts +19 -1
  20. package/dist/types/cli.d.ts +6 -0
  21. package/dist/types/commands/accounts.d.ts +40 -0
  22. package/dist/types/commands/crash.d.ts +24 -0
  23. package/dist/types/commands/customize.d.ts +22 -0
  24. package/dist/types/commands/daemon.d.ts +1 -1
  25. package/dist/types/commands/launch.d.ts +6 -0
  26. package/dist/types/commands/setup.d.ts +6 -0
  27. package/dist/types/commands/skills.d.ts +6 -1
  28. package/dist/types/commands/update.d.ts +7 -0
  29. package/dist/types/config/atomic-yaml-patch-worker.d.ts +25 -0
  30. package/dist/types/config/atomic-yaml-patch.d.ts +62 -2
  31. package/dist/types/config/config-schema-version.d.ts +10 -0
  32. package/dist/types/config/file-lock.d.ts +18 -0
  33. package/dist/types/config/model-profile-activation.d.ts +3 -1
  34. package/dist/types/config/model-registry.d.ts +10 -1
  35. package/dist/types/config/models-config-schema.d.ts +2 -0
  36. package/dist/types/config/settings-schema.d.ts +73 -9
  37. package/dist/types/config/settings.d.ts +63 -0
  38. package/dist/types/config/skill-settings-defaults.d.ts +26 -0
  39. package/dist/types/config/task-delegation.d.ts +39 -0
  40. package/dist/types/config/update-channel.d.ts +10 -0
  41. package/dist/types/coordinator-mcp/server.d.ts +5 -1
  42. package/dist/types/crash/index-store.d.ts +87 -0
  43. package/dist/types/crash/nudge.d.ts +50 -0
  44. package/dist/types/crash/record-loader.d.ts +17 -0
  45. package/dist/types/crash/report.d.ts +134 -0
  46. package/dist/types/crash/sanitize.d.ts +25 -0
  47. package/dist/types/cursor.d.ts +13 -2
  48. package/dist/types/customization/import.d.ts +37 -0
  49. package/dist/types/customization/index.d.ts +4 -0
  50. package/dist/types/customization/inventory.d.ts +25 -0
  51. package/dist/types/customization/mutations.d.ts +32 -0
  52. package/dist/types/customization/types.d.ts +115 -0
  53. package/dist/types/daemon/control-types.d.ts +8 -1
  54. package/dist/types/daemon/operator-contract.d.ts +4 -4
  55. package/dist/types/discovery/claude.d.ts +39 -1
  56. package/dist/types/discovery/codex.d.ts +39 -1
  57. package/dist/types/discovery/index.d.ts +2 -0
  58. package/dist/types/discovery/mcp-compat.d.ts +30 -0
  59. package/dist/types/edit/renderer.d.ts +23 -0
  60. package/dist/types/extensibility/extensions/runner.d.ts +3 -0
  61. package/dist/types/extensibility/extensions/types.d.ts +21 -4
  62. package/dist/types/extensibility/gjc-plugins/observability.d.ts +6 -1
  63. package/dist/types/extensibility/gjc-plugins/registry.d.ts +8 -1
  64. package/dist/types/extensibility/hooks/loader.d.ts +19 -5
  65. package/dist/types/extensibility/runtime-skill-discovery.d.ts +22 -1
  66. package/dist/types/extensibility/skill-management.d.ts +107 -0
  67. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +3 -1
  68. package/dist/types/gjc-runtime/launch-worktree.d.ts +8 -0
  69. package/dist/types/gjc-runtime/linux-proc.d.ts +3 -0
  70. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +10 -6
  71. package/dist/types/gjc-runtime/session-state-lock.d.ts +104 -0
  72. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +120 -2
  73. package/dist/types/gjc-runtime/state-writer.d.ts +10 -1
  74. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +4 -2
  75. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -6
  76. package/dist/types/gjc-runtime/workflow-settings.d.ts +109 -0
  77. package/dist/types/hooks/events.d.ts +60 -0
  78. package/dist/types/hooks/normalize.d.ts +83 -0
  79. package/dist/types/hooks/skill-state.d.ts +9 -2
  80. package/dist/types/main.d.ts +2 -1
  81. package/dist/types/modes/acp/abort-scope.d.ts +13 -0
  82. package/dist/types/modes/acp/acp-agent.d.ts +7 -4
  83. package/dist/types/modes/components/customization/customization-dashboard.d.ts +44 -0
  84. package/dist/types/modes/components/customization/import-wizard.d.ts +21 -0
  85. package/dist/types/modes/components/customization/index.d.ts +1 -0
  86. package/dist/types/modes/components/dynamic-theme-text.d.ts +9 -0
  87. package/dist/types/modes/components/gajae-pet-widget.d.ts +6 -28
  88. package/dist/types/modes/components/irc-sidebar.d.ts +1 -0
  89. package/dist/types/modes/components/iterm-pet-transport.d.ts +122 -0
  90. package/dist/types/modes/components/oauth-selector.d.ts +10 -4
  91. package/dist/types/modes/components/pet-capability.d.ts +7 -21
  92. package/dist/types/modes/components/settings-selector.d.ts +5 -0
  93. package/dist/types/modes/components/status-line/gh.d.ts +6 -2
  94. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  95. package/dist/types/modes/controllers/selector-controller.d.ts +7 -8
  96. package/dist/types/modes/controllers/stt-controller.d.ts +1 -1
  97. package/dist/types/modes/interactive-mode.d.ts +1 -0
  98. package/dist/types/modes/theme/defaults/index.d.ts +118 -0
  99. package/dist/types/modes/types.d.ts +1 -0
  100. package/dist/types/modes/utils/injected-user-submission.d.ts +10 -0
  101. package/dist/types/runtime-mcp/config.d.ts +9 -0
  102. package/dist/types/runtime-mcp/manager.d.ts +26 -1
  103. package/dist/types/runtime-mcp/oauth-discovery.d.ts +4 -0
  104. package/dist/types/runtime-mcp/oauth-flow.d.ts +24 -1
  105. package/dist/types/runtime-mcp/pool-key.d.ts +2 -0
  106. package/dist/types/runtime-mcp/protocol.d.ts +278 -0
  107. package/dist/types/runtime-mcp/scope-config.d.ts +29 -0
  108. package/dist/types/runtime-mcp/tool-bridge.d.ts +15 -13
  109. package/dist/types/runtime-mcp/transports/http.d.ts +9 -0
  110. package/dist/types/runtime-mcp/types.d.ts +102 -10
  111. package/dist/types/sdk/acp/adapter.d.ts +26 -5
  112. package/dist/types/sdk/broker/broker.d.ts +3 -55
  113. package/dist/types/sdk/broker/lifecycle.d.ts +18 -12
  114. package/dist/types/sdk/broker/session-index.d.ts +14 -5
  115. package/dist/types/sdk/broker/startup-failure.d.ts +16 -1
  116. package/dist/types/sdk/bus/chat-daemon-control.d.ts +51 -1
  117. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -41
  118. package/dist/types/sdk/bus/config.d.ts +11 -0
  119. package/dist/types/sdk/bus/conversation-store.d.ts +0 -1
  120. package/dist/types/sdk/bus/daemon-paths.d.ts +6 -0
  121. package/dist/types/sdk/bus/discord-conversation.d.ts +2 -0
  122. package/dist/types/sdk/bus/discord-daemon.d.ts +17 -12
  123. package/dist/types/sdk/bus/existing-thread-readiness.d.ts +4 -34
  124. package/dist/types/sdk/bus/inbound-reaction-ordering.d.ts +61 -0
  125. package/dist/types/sdk/bus/index.d.ts +85 -99
  126. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +29 -7
  127. package/dist/types/sdk/bus/managed-daemon.d.ts +2 -32
  128. package/dist/types/sdk/bus/notification-service.d.ts +52 -0
  129. package/dist/types/sdk/bus/operator-runtime.d.ts +1 -1
  130. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +16 -5
  131. package/dist/types/sdk/bus/reconciliation-store.d.ts +113 -5
  132. package/dist/types/sdk/bus/session-control.d.ts +25 -1
  133. package/dist/types/sdk/bus/slack-conversation.d.ts +4 -0
  134. package/dist/types/sdk/bus/slack-daemon.d.ts +9 -17
  135. package/dist/types/sdk/bus/slack-live-provider.d.ts +3 -1
  136. package/dist/types/sdk/bus/slack-provider.d.ts +4 -0
  137. package/dist/types/sdk/bus/slack-thread-binding.d.ts +6 -40
  138. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +30 -104
  139. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +10 -0
  140. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +262 -19
  141. package/dist/types/sdk/bus/telegram-daemon-orphan-reap.d.ts +125 -0
  142. package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +22 -0
  143. package/dist/types/sdk/bus/telegram-daemon.d.ts +119 -310
  144. package/dist/types/sdk/bus/telegram-reference.d.ts +1 -47
  145. package/dist/types/sdk/bus/threaded-render.d.ts +1 -0
  146. package/dist/types/sdk/bus/topic-registry.d.ts +33 -17
  147. package/dist/types/sdk/cli/rows.d.ts +2 -11
  148. package/dist/types/sdk/cli/session-cli.d.ts +15 -25
  149. package/dist/types/sdk/client/client.d.ts +97 -1
  150. package/dist/types/sdk/host/control/dispatch.d.ts +0 -7
  151. package/dist/types/sdk/host/control/index.d.ts +1 -1
  152. package/dist/types/sdk/host/control/operations.d.ts +18 -5
  153. package/dist/types/sdk/host/control/runtime-gate.d.ts +19 -0
  154. package/dist/types/sdk/host/host.d.ts +15 -1
  155. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  156. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  157. package/dist/types/sdk/host/session-runtime.d.ts +127 -7
  158. package/dist/types/sdk/host/types.d.ts +1 -1
  159. package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
  160. package/dist/types/sdk/index.d.ts +2 -1
  161. package/dist/types/sdk/lifecycle/broker-client.d.ts +8 -0
  162. package/dist/types/sdk/lifecycle/client.d.ts +59 -0
  163. package/dist/types/sdk/lifecycle/index.d.ts +3 -0
  164. package/dist/types/sdk/lifecycle/service.d.ts +232 -0
  165. package/dist/types/sdk/mcp/server.d.ts +9 -6
  166. package/dist/types/sdk/prompt-deadline-lease.d.ts +43 -0
  167. package/dist/types/sdk/prompt-deadline-manager.d.ts +28 -0
  168. package/dist/types/sdk/prompt-status.d.ts +10 -3
  169. package/dist/types/sdk/protocol/operation-registry.d.ts +5 -0
  170. package/dist/types/sdk/receipt-state.d.ts +17 -0
  171. package/dist/types/sdk/reconciliation-extensions.d.ts +2 -0
  172. package/dist/types/sdk/router/index.d.ts +1 -0
  173. package/dist/types/sdk/router/session-router.d.ts +152 -0
  174. package/dist/types/sdk/session-list.d.ts +22 -0
  175. package/dist/types/sdk/session-reconnect.d.ts +43 -1
  176. package/dist/types/sdk/session.d.ts +34 -1
  177. package/dist/types/sdk/transport/serve-cli.d.ts +1 -4
  178. package/dist/types/session/account-inventory.d.ts +86 -0
  179. package/dist/types/session/agent-session.d.ts +145 -6
  180. package/dist/types/session/agent-storage.d.ts +10 -0
  181. package/dist/types/session/coordinator-tool-label.d.ts +28 -0
  182. package/dist/types/session/internal/managed-session-storage.d.ts +23 -4
  183. package/dist/types/session/messages.d.ts +1 -1
  184. package/dist/types/session/secure-token-file.d.ts +3 -0
  185. package/dist/types/session/session-manager.d.ts +47 -1
  186. package/dist/types/session/startup-auth-config.d.ts +44 -0
  187. package/dist/types/session/terminal-abort.d.ts +378 -0
  188. package/dist/types/session/yield-queue.d.ts +7 -0
  189. package/dist/types/session-import/claude.d.ts +26 -0
  190. package/dist/types/session-import/codex.d.ts +88 -0
  191. package/dist/types/session-import/command.d.ts +35 -0
  192. package/dist/types/session-import/detect.d.ts +22 -0
  193. package/dist/types/session-import/index.d.ts +7 -0
  194. package/dist/types/session-import/provider-codex.d.ts +22 -0
  195. package/dist/types/session-import/provider-service.d.ts +38 -0
  196. package/dist/types/session-import/redact.d.ts +23 -0
  197. package/dist/types/session-import/service.d.ts +47 -0
  198. package/dist/types/session-import/types.d.ts +120 -0
  199. package/dist/types/setup/paseo/check.d.ts +16 -0
  200. package/dist/types/setup/paseo/install-saga.d.ts +115 -0
  201. package/dist/types/setup/paseo/json-publisher.d.ts +76 -0
  202. package/dist/types/setup/paseo/orchestration-preferences.d.ts +24 -0
  203. package/dist/types/setup/paseo/paseo-ownership.d.ts +87 -0
  204. package/dist/types/setup/paseo/paseo-setup.d.ts +29 -0
  205. package/dist/types/setup/paseo/provider-config.d.ts +35 -0
  206. package/dist/types/setup/paseo/remove.d.ts +14 -0
  207. package/dist/types/setup/paseo/result-types.d.ts +59 -0
  208. package/dist/types/setup/paseo/setup-deps.d.ts +73 -0
  209. package/dist/types/setup/paseo/skills-bridge.d.ts +44 -0
  210. package/dist/types/skill-state/active-state.d.ts +5 -1
  211. package/dist/types/slash-commands/acp-builtins.d.ts +5 -1
  212. package/dist/types/slash-commands/builtin-registry.d.ts +2 -0
  213. package/dist/types/slash-commands/helpers/credential-switch.d.ts +12 -0
  214. package/dist/types/slash-commands/helpers/usage-report.d.ts +5 -6
  215. package/dist/types/slash-commands/types.d.ts +7 -1
  216. package/dist/types/task/executor.d.ts +10 -0
  217. package/dist/types/task/index.d.ts +3 -1
  218. package/dist/types/tools/ask-answer-registry.d.ts +6 -3
  219. package/dist/types/tools/bash-sleep-advisory.d.ts +14 -0
  220. package/dist/types/tools/bash.d.ts +2 -6
  221. package/dist/types/tools/browser/attach.d.ts +11 -0
  222. package/dist/types/tools/browser/launch.d.ts +20 -0
  223. package/dist/types/tools/browser/profile-discovery.d.ts +6 -0
  224. package/dist/types/tools/browser/render.d.ts +1 -0
  225. package/dist/types/tools/browser.d.ts +3 -1
  226. package/dist/types/tools/image-gen.d.ts +10 -2
  227. package/dist/types/tools/index.d.ts +8 -0
  228. package/dist/types/tools/monitor.d.ts +1 -1
  229. package/dist/types/tools/skill-discovery.d.ts +8 -1
  230. package/dist/types/tools/subagent.d.ts +2 -0
  231. package/dist/types/tools/ultragoal-ask-guard.d.ts +2 -2
  232. package/dist/types/utils/herdr-pane.d.ts +76 -0
  233. package/package.json +19 -9
  234. package/scripts/build-binary.ts +2 -1
  235. package/scripts/build-sdk-package-smoke.ts +2 -13
  236. package/scripts/capture-sticky-viewport-showcase.ts +0 -1
  237. package/scripts/compile-args.ts +2 -0
  238. package/scripts/generate-sdk-adapter-parity-manifest.ts +31 -16
  239. package/scripts/generate-sdk-operation-inventory.ts +28 -0
  240. package/scripts/generate-telegram-baseline-manifest.ts +1 -0
  241. package/scripts/generate-tool-catalog.ts +6 -4
  242. package/scripts/verify-gjc-sdk-canonicalization.ts +5 -39
  243. package/src/async/job-manager.ts +352 -17
  244. package/src/async/support.ts +25 -0
  245. package/src/autoresearch/dashboard.ts +28 -13
  246. package/src/capability/index.ts +3 -1
  247. package/src/capability/mcp.ts +6 -0
  248. package/src/capability/types.ts +6 -0
  249. package/src/cli/accounts-cli.ts +395 -0
  250. package/src/cli/args.ts +23 -0
  251. package/src/cli/auth-broker-cli.ts +136 -66
  252. package/src/cli/auth-gateway-cli.ts +94 -70
  253. package/src/cli/config-cli.ts +27 -2
  254. package/src/cli/crash-cli.ts +78 -0
  255. package/src/cli/customize-doctor.ts +1605 -0
  256. package/src/cli/daemon-cli.ts +12 -3
  257. package/src/cli/fast-help.ts +1 -0
  258. package/src/cli/mcp-cli.ts +128 -60
  259. package/src/cli/notify-cli.ts +8 -14
  260. package/src/cli/root-flags.ts +8 -0
  261. package/src/cli/setup-cli.ts +145 -0
  262. package/src/cli/skills-cli.ts +38 -2
  263. package/src/cli/update-cli.ts +167 -12
  264. package/src/cli.ts +4 -0
  265. package/src/commands/accounts.ts +71 -0
  266. package/src/commands/crash.ts +35 -0
  267. package/src/commands/customize.ts +84 -0
  268. package/src/commands/daemon.ts +2 -2
  269. package/src/commands/deep-interview.ts +2 -0
  270. package/src/commands/launch.ts +1 -0
  271. package/src/commands/mcp.ts +12 -7
  272. package/src/commands/ralplan.ts +2 -0
  273. package/src/commands/sdk.ts +65 -32
  274. package/src/commands/setup.ts +5 -0
  275. package/src/commands/skills.ts +14 -4
  276. package/src/commands/ultragoal.ts +10 -1
  277. package/src/commands/update.ts +26 -15
  278. package/src/config/atomic-yaml-patch-worker.ts +57 -0
  279. package/src/config/atomic-yaml-patch.ts +769 -29
  280. package/src/config/config-schema-version.ts +10 -0
  281. package/src/config/file-lock.ts +70 -1
  282. package/src/config/mcp-schema.json +52 -14
  283. package/src/config/model-profile-activation.ts +23 -2
  284. package/src/config/model-profiles.ts +88 -21
  285. package/src/config/model-registry.ts +147 -21
  286. package/src/config/models-config-schema.ts +1 -1
  287. package/src/config/settings-schema.ts +77 -7
  288. package/src/config/settings.ts +4038 -199
  289. package/src/config/skill-settings-defaults.ts +41 -3
  290. package/src/config/task-delegation.ts +87 -0
  291. package/src/config/update-channel.ts +23 -0
  292. package/src/coordinator-mcp/server.ts +377 -308
  293. package/src/crash/index-store.ts +723 -0
  294. package/src/crash/nudge.ts +89 -0
  295. package/src/crash/record-loader.ts +149 -0
  296. package/src/crash/report.ts +551 -0
  297. package/src/crash/sanitize.ts +129 -0
  298. package/src/cursor.ts +36 -10
  299. package/src/customization/import.ts +946 -0
  300. package/src/customization/index.ts +4 -0
  301. package/src/customization/inventory.ts +335 -0
  302. package/src/customization/mutations.ts +141 -0
  303. package/src/customization/types.ts +223 -0
  304. package/src/daemon/control-types.ts +8 -1
  305. package/src/daemon/operator-contract.ts +6 -6
  306. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +31 -6
  307. package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -22
  308. package/src/defaults/gjc/skills/ralplan/SKILL.md +36 -23
  309. package/src/defaults/gjc/skills/ultragoal/SKILL.md +16 -0
  310. package/src/discovery/claude-hooks.ts +38 -0
  311. package/src/discovery/claude.ts +142 -209
  312. package/src/discovery/codex-hooks.ts +38 -0
  313. package/src/discovery/codex.ts +132 -198
  314. package/src/discovery/helpers.ts +8 -1
  315. package/src/discovery/index.ts +2 -0
  316. package/src/discovery/mcp-compat.ts +234 -0
  317. package/src/discovery/mcp-json.ts +6 -0
  318. package/src/edit/renderer.ts +40 -0
  319. package/src/edit/streaming.ts +4 -2
  320. package/src/exec/bash-executor.ts +24 -20
  321. package/src/export/html/index.ts +51 -2
  322. package/src/extensibility/extensions/loader.ts +9 -1
  323. package/src/extensibility/extensions/runner.ts +17 -4
  324. package/src/extensibility/extensions/types.ts +31 -6
  325. package/src/extensibility/gjc-plugins/constrained-hooks.ts +33 -0
  326. package/src/extensibility/gjc-plugins/installer.ts +9 -5
  327. package/src/extensibility/gjc-plugins/observability.ts +8 -2
  328. package/src/extensibility/gjc-plugins/registry.ts +13 -2
  329. package/src/extensibility/gjc-plugins/runtime-adapters.ts +3 -1
  330. package/src/extensibility/gjc-plugins/schema.ts +222 -39
  331. package/src/extensibility/hooks/loader.ts +154 -10
  332. package/src/extensibility/runtime-skill-discovery.ts +257 -60
  333. package/src/extensibility/skill-management.ts +368 -0
  334. package/src/extensibility/skills.ts +52 -16
  335. package/src/gjc-runtime/deep-interview-runtime.ts +45 -61
  336. package/src/gjc-runtime/launch-worktree.ts +56 -2
  337. package/src/gjc-runtime/linux-proc.ts +7 -1
  338. package/src/gjc-runtime/ralplan-runtime.ts +100 -164
  339. package/src/gjc-runtime/session-state-lock.ts +992 -0
  340. package/src/gjc-runtime/session-state-sidecar.ts +644 -142
  341. package/src/gjc-runtime/state-writer.ts +39 -14
  342. package/src/gjc-runtime/tmux-owner-isolation.ts +5 -4
  343. package/src/gjc-runtime/ultragoal-guard.ts +11 -4
  344. package/src/gjc-runtime/ultragoal-runtime.ts +44 -41
  345. package/src/gjc-runtime/workflow-settings.ts +596 -0
  346. package/src/goals/tools/goal-tool.ts +6 -1
  347. package/src/hooks/events.ts +291 -0
  348. package/src/hooks/native-skill-hook.ts +4 -0
  349. package/src/hooks/normalize.ts +369 -0
  350. package/src/hooks/skill-state.ts +165 -46
  351. package/src/internal-urls/docs-index.generated.ts +41 -32
  352. package/src/main.ts +36 -5
  353. package/src/migrate/mcp-mapper.ts +25 -2
  354. package/src/modes/acp/abort-scope.ts +29 -0
  355. package/src/modes/acp/acp-agent.ts +423 -98
  356. package/src/modes/components/assistant-message.ts +6 -1
  357. package/src/modes/components/chrome.ts +4 -3
  358. package/src/modes/components/customization/customization-dashboard.ts +443 -0
  359. package/src/modes/components/customization/import-wizard.ts +372 -0
  360. package/src/modes/components/customization/index.ts +1 -0
  361. package/src/modes/components/dynamic-theme-text.ts +22 -0
  362. package/src/modes/components/gajae-pet-widget.ts +607 -193
  363. package/src/modes/components/irc-sidebar.ts +11 -0
  364. package/src/modes/components/iterm-pet-transport.ts +585 -0
  365. package/src/modes/components/model-selector.ts +1 -6
  366. package/src/modes/components/oauth-selector.ts +277 -86
  367. package/src/modes/components/pet-capability.ts +35 -31
  368. package/src/modes/components/settings-selector.ts +29 -5
  369. package/src/modes/components/status-line/gh.ts +45 -1
  370. package/src/modes/components/status-line/segments.ts +19 -4
  371. package/src/modes/components/tool-status-header.ts +13 -3
  372. package/src/modes/controllers/command-controller.ts +20 -8
  373. package/src/modes/controllers/extension-ui-controller.ts +56 -257
  374. package/src/modes/controllers/input-controller.ts +10 -0
  375. package/src/modes/controllers/runtime-mcp-command-controller.ts +16 -3
  376. package/src/modes/controllers/selector-controller.ts +238 -77
  377. package/src/modes/controllers/stt-controller.ts +3 -3
  378. package/src/modes/interactive-mode.ts +88 -16
  379. package/src/modes/print-mode.ts +35 -6
  380. package/src/modes/runtime-init.ts +21 -68
  381. package/src/modes/theme/defaults/index.ts +2 -0
  382. package/src/modes/theme/defaults/ouroboros.json +118 -0
  383. package/src/modes/types.ts +1 -0
  384. package/src/modes/utils/injected-user-submission.ts +25 -0
  385. package/src/modes/utils/terminal-bell.ts +15 -1
  386. package/src/prompts/agent-fragments/ralplan-persistence.md +1 -1
  387. package/src/prompts/agent-fragments/restricted-bash.md +1 -1
  388. package/src/prompts/agents/critic.md +1 -1
  389. package/src/prompts/system/rlm-research.md +1 -1
  390. package/src/prompts/system/system-prompt.md +0 -2
  391. package/src/prompts/tools/bash.md +7 -1
  392. package/src/prompts/tools/browser.md +1 -1
  393. package/src/prompts/tools/cron.md +6 -6
  394. package/src/prompts/tools/read.md +0 -2
  395. package/src/prompts/tools/skill-discovery.md +4 -2
  396. package/src/runtime-mcp/client.ts +427 -23
  397. package/src/runtime-mcp/config.ts +37 -5
  398. package/src/runtime-mcp/manager-pool.test.ts +56 -0
  399. package/src/runtime-mcp/manager.ts +52 -2
  400. package/src/runtime-mcp/oauth-discovery.ts +13 -0
  401. package/src/runtime-mcp/oauth-flow.ts +46 -1
  402. package/src/runtime-mcp/pool-key.ts +5 -1
  403. package/src/runtime-mcp/pool.test.ts +9 -0
  404. package/src/runtime-mcp/protocol.test.ts +353 -0
  405. package/src/runtime-mcp/protocol.ts +624 -0
  406. package/src/runtime-mcp/scope-config.ts +78 -0
  407. package/src/runtime-mcp/tool-bridge.ts +44 -10
  408. package/src/runtime-mcp/transports/http.ts +84 -13
  409. package/src/runtime-mcp/types.ts +123 -12
  410. package/src/sdk/acp/adapter.ts +328 -97
  411. package/src/sdk/broker/broker.ts +160 -741
  412. package/src/sdk/broker/ensure.ts +9 -1
  413. package/src/sdk/broker/lifecycle.ts +510 -116
  414. package/src/sdk/broker/operation-receipt-ledger.ts +11 -2
  415. package/src/sdk/broker/process-incarnation.ts +19 -0
  416. package/src/sdk/broker/session-index.ts +516 -117
  417. package/src/sdk/broker/startup-failure.ts +28 -5
  418. package/src/sdk/broker/transport.ts +1 -13
  419. package/src/sdk/bus/chat-daemon-cli.ts +4 -2
  420. package/src/sdk/bus/chat-daemon-control.ts +67 -6
  421. package/src/sdk/bus/chat-daemon-runtime.ts +183 -859
  422. package/src/sdk/bus/config.ts +13 -0
  423. package/src/sdk/bus/conversation-store.ts +59 -171
  424. package/src/sdk/bus/daemon-paths.ts +20 -0
  425. package/src/sdk/bus/discord-conversation.ts +2 -0
  426. package/src/sdk/bus/discord-daemon.ts +524 -126
  427. package/src/sdk/bus/discord-live-provider.ts +206 -79
  428. package/src/sdk/bus/existing-thread-readiness.ts +4 -97
  429. package/src/sdk/bus/inbound-reaction-ordering.ts +122 -0
  430. package/src/sdk/bus/index.ts +2480 -456
  431. package/src/sdk/bus/kind-aware-reconciliation.ts +204 -34
  432. package/src/sdk/bus/lifecycle-commands.ts +2 -2
  433. package/src/sdk/bus/managed-daemon.ts +2 -145
  434. package/src/sdk/bus/notification-service.ts +196 -0
  435. package/src/sdk/bus/operator-runtime.ts +1 -1
  436. package/src/sdk/bus/prompt-reconciliation.ts +24 -15
  437. package/src/sdk/bus/reconciliation-store.ts +553 -24
  438. package/src/sdk/bus/session-control.ts +49 -3
  439. package/src/sdk/bus/slack-conversation.ts +4 -0
  440. package/src/sdk/bus/slack-daemon.ts +733 -134
  441. package/src/sdk/bus/slack-live-provider.ts +65 -15
  442. package/src/sdk/bus/slack-provider.ts +4 -0
  443. package/src/sdk/bus/slack-thread-binding.ts +9 -104
  444. package/src/sdk/bus/telegram-adoption-intent.ts +299 -209
  445. package/src/sdk/bus/telegram-daemon-cli.ts +194 -7
  446. package/src/sdk/bus/telegram-daemon-contract.ts +265 -19
  447. package/src/sdk/bus/telegram-daemon-control.ts +3 -13
  448. package/src/sdk/bus/telegram-daemon-orphan-reap.ts +498 -0
  449. package/src/sdk/bus/telegram-daemon-owner-registry.ts +176 -0
  450. package/src/sdk/bus/telegram-daemon.ts +2552 -2280
  451. package/src/sdk/bus/telegram-reference.ts +1 -226
  452. package/src/sdk/bus/telegram-setup.ts +2 -2
  453. package/src/sdk/bus/threaded-render.ts +1 -0
  454. package/src/sdk/bus/topic-registry.ts +279 -146
  455. package/src/sdk/cli/rows.ts +2 -35
  456. package/src/sdk/cli/session-cli.ts +776 -700
  457. package/src/sdk/client/client.ts +892 -1
  458. package/src/sdk/host/control/dispatch.ts +93 -58
  459. package/src/sdk/host/control/index.ts +1 -1
  460. package/src/sdk/host/control/operations.ts +18 -10
  461. package/src/sdk/host/control/runtime-gate.ts +40 -0
  462. package/src/sdk/host/host.ts +63 -12
  463. package/src/sdk/host/query/handlers.ts +66 -7
  464. package/src/sdk/host/reverse-leases.ts +2 -2
  465. package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
  466. package/src/sdk/host/session-runtime.test.ts +2787 -34
  467. package/src/sdk/host/session-runtime.ts +1489 -101
  468. package/src/sdk/host/surface-policy.ts +12 -8
  469. package/src/sdk/host/types.ts +1 -1
  470. package/src/sdk/host/websocket-transport.lifecycle.test.ts +31 -59
  471. package/src/sdk/host/websocket-transport.ts +85 -50
  472. package/src/sdk/index.ts +2 -1
  473. package/src/sdk/lifecycle/broker-client.ts +53 -0
  474. package/src/sdk/lifecycle/client.ts +234 -0
  475. package/src/sdk/lifecycle/index.ts +3 -0
  476. package/src/sdk/{bus/recent-activity.ts → lifecycle/recent-sessions.ts} +5 -2
  477. package/src/sdk/lifecycle/service.ts +720 -0
  478. package/src/sdk/mcp/server.ts +226 -188
  479. package/src/sdk/prompt-deadline-lease.ts +55 -0
  480. package/src/sdk/prompt-deadline-manager.ts +148 -0
  481. package/src/sdk/prompt-status.ts +11 -3
  482. package/src/sdk/protocol/operation-inventory.generated.json +272 -129
  483. package/src/sdk/protocol/operation-registry.ts +71 -8
  484. package/src/sdk/receipt-state.ts +43 -0
  485. package/src/sdk/reconciliation-extensions.ts +14 -0
  486. package/src/sdk/router/index.ts +1 -0
  487. package/src/sdk/router/session-router.ts +1658 -0
  488. package/src/sdk/session-list.ts +93 -0
  489. package/src/sdk/session-reconnect.ts +45 -1
  490. package/src/sdk/session.ts +716 -120
  491. package/src/sdk/transport/serve-cli.ts +18 -34
  492. package/src/session/account-inventory.ts +603 -0
  493. package/src/session/agent-session.ts +2873 -559
  494. package/src/session/agent-storage.ts +35 -0
  495. package/src/session/blob-store.ts +32 -1
  496. package/src/session/coordinator-tool-label.ts +40 -0
  497. package/src/session/internal/managed-session-scope.ts +4 -0
  498. package/src/session/internal/managed-session-storage.ts +199 -31
  499. package/src/session/messages.ts +1 -1
  500. package/src/session/secure-token-file.ts +80 -0
  501. package/src/session/session-manager.ts +757 -57
  502. package/src/session/session-storage.ts +25 -16
  503. package/src/session/startup-auth-config.ts +297 -0
  504. package/src/session/terminal-abort.ts +1253 -0
  505. package/src/session/yield-queue.ts +57 -15
  506. package/src/session-import/claude.ts +334 -0
  507. package/src/session-import/codex.ts +1033 -0
  508. package/src/session-import/command.ts +77 -0
  509. package/src/session-import/detect.ts +133 -0
  510. package/src/session-import/index.ts +7 -0
  511. package/src/session-import/provider-codex.ts +327 -0
  512. package/src/session-import/provider-service.ts +457 -0
  513. package/src/session-import/redact.ts +128 -0
  514. package/src/session-import/service.ts +1264 -0
  515. package/src/session-import/types.ts +148 -0
  516. package/src/setup/paseo/check.ts +213 -0
  517. package/src/setup/paseo/install-saga.ts +293 -0
  518. package/src/setup/paseo/json-publisher.ts +263 -0
  519. package/src/setup/paseo/orchestration-preferences.ts +86 -0
  520. package/src/setup/paseo/paseo-ownership.ts +224 -0
  521. package/src/setup/paseo/paseo-setup.ts +253 -0
  522. package/src/setup/paseo/provider-config.ts +139 -0
  523. package/src/setup/paseo/remove.ts +183 -0
  524. package/src/setup/paseo/result-types.ts +79 -0
  525. package/src/setup/paseo/setup-deps.ts +175 -0
  526. package/src/setup/paseo/skills-bridge.ts +260 -0
  527. package/src/skill-state/active-state.ts +21 -11
  528. package/src/slash-commands/acp-builtins.ts +9 -1
  529. package/src/slash-commands/builtin-registry.ts +117 -19
  530. package/src/slash-commands/helpers/credential-switch.ts +143 -0
  531. package/src/slash-commands/helpers/mcp.ts +9 -3
  532. package/src/slash-commands/helpers/usage-report.ts +61 -56
  533. package/src/slash-commands/types.ts +7 -2
  534. package/src/task/executor.ts +30 -11
  535. package/src/task/index.ts +66 -7
  536. package/src/task/render.ts +23 -9
  537. package/src/tools/ask-answer-registry.ts +23 -7
  538. package/src/tools/ask.ts +8 -4
  539. package/src/tools/bash-sleep-advisory.ts +115 -0
  540. package/src/tools/bash.ts +98 -18
  541. package/src/tools/browser/attach.ts +71 -3
  542. package/src/tools/browser/launch.ts +86 -13
  543. package/src/tools/browser/profile-discovery.ts +57 -7
  544. package/src/tools/browser/render.ts +8 -1
  545. package/src/tools/browser.ts +130 -21
  546. package/src/tools/computer.enforcement.test.ts +1 -1
  547. package/src/tools/computer.ts +3 -2
  548. package/src/tools/cron.ts +12 -1
  549. package/src/tools/image-gen.ts +349 -142
  550. package/src/tools/index.ts +8 -0
  551. package/src/tools/job.ts +33 -6
  552. package/src/tools/monitor.ts +41 -2
  553. package/src/tools/skill-discovery.ts +21 -29
  554. package/src/tools/subagent.ts +142 -24
  555. package/src/tools/todo-contract.ts +55 -3
  556. package/src/tools/todo-write.ts +4 -3
  557. package/src/tools/tool-catalog.generated.ts +6 -6
  558. package/src/tools/ultragoal-ask-guard.ts +10 -3
  559. package/src/utils/herdr-pane.ts +345 -0
  560. package/src/utils/title-generator.ts +2 -0
  561. package/src/web/scrapers/index.ts +6 -4
  562. package/dist/types/cli/malloc-env-guard.test.d.ts +0 -1
  563. package/dist/types/defaults/gjc-defaults.test.d.ts +0 -1
  564. package/dist/types/memory-backend/lazy-loading.test.d.ts +0 -1
  565. package/dist/types/runtime/boot-ordering.test.d.ts +0 -1
  566. package/dist/types/runtime/lazy-service.test.d.ts +0 -1
  567. package/dist/types/runtime/network-prewarm-service.test.d.ts +0 -1
  568. package/dist/types/runtime/workspace-tree-service.test.d.ts +0 -1
  569. package/dist/types/runtime-mcp/config.test.d.ts +0 -1
  570. package/dist/types/runtime-mcp/http.test.d.ts +0 -1
  571. package/dist/types/runtime-mcp/manager-pool.test.d.ts +0 -1
  572. package/dist/types/runtime-mcp/pool-key.test.d.ts +0 -1
  573. package/dist/types/runtime-mcp/pool.test.d.ts +0 -1
  574. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +0 -117
  575. package/dist/types/sdk/bus/lifecycle-orchestrator.d.ts +0 -141
  576. package/dist/types/sdk/bus/telegram-cli.d.ts +0 -31
  577. package/dist/types/sdk/elevation/allowlist.d.ts +0 -20
  578. package/dist/types/sdk/elevation/audit.d.ts +0 -93
  579. package/dist/types/sdk/elevation/capability.d.ts +0 -3
  580. package/dist/types/sdk/elevation/digest.d.ts +0 -72
  581. package/dist/types/sdk/elevation/dispatch-receipt.d.ts +0 -85
  582. package/dist/types/sdk/elevation/grant-ledger.d.ts +0 -163
  583. package/dist/types/sdk/elevation/owner.d.ts +0 -48
  584. package/dist/types/sdk/host/import-graph.test.d.ts +0 -1
  585. package/dist/types/sdk/host/sdk-surface-parity.test.d.ts +0 -1
  586. package/dist/types/sdk/host/session-runtime.test.d.ts +0 -1
  587. package/dist/types/sdk/host/websocket-transport.lifecycle.test.d.ts +0 -1
  588. package/dist/types/session/auth-broker-config.d.ts +0 -13
  589. package/dist/types/session/heap-eviction-artifacts.test.d.ts +0 -1
  590. package/dist/types/skills/skills.test.d.ts +0 -1
  591. package/dist/types/slash-commands/notify-command.test.d.ts +0 -1
  592. package/dist/types/task/token-log.test.d.ts +0 -1
  593. package/dist/types/tools/computer.enforcement.test.d.ts +0 -1
  594. package/dist/types/tools/descriptors.test.d.ts +0 -1
  595. package/scripts/g005-daemon-smoke.ts +0 -180
  596. package/scripts/g011-daemon-path-smoke.ts +0 -153
  597. package/src/sdk/bus/lifecycle-control-runtime.ts +0 -1733
  598. package/src/sdk/bus/lifecycle-orchestrator.ts +0 -489
  599. package/src/sdk/bus/telegram-cli.ts +0 -212
  600. package/src/sdk/elevation/allowlist.ts +0 -54
  601. package/src/sdk/elevation/audit.ts +0 -208
  602. package/src/sdk/elevation/capability.ts +0 -41
  603. package/src/sdk/elevation/digest.ts +0 -129
  604. package/src/sdk/elevation/dispatch-receipt.ts +0 -184
  605. package/src/sdk/elevation/grant-ledger.ts +0 -1142
  606. package/src/sdk/elevation/owner.ts +0 -109
  607. package/src/session/auth-broker-config.ts +0 -109
  608. /package/dist/types/{ai-core-import-gate.test.d.ts → discovery/claude-hooks.d.ts} +0 -0
  609. /package/dist/types/{cli/fixture-report.test.d.ts → discovery/codex-hooks.d.ts} +0 -0
  610. /package/dist/types/sdk/{bus/recent-activity.d.ts → lifecycle/recent-sessions.d.ts} +0 -0
@@ -0,0 +1,551 @@
1
+ /**
2
+ * `gjc crash report` — user-driven, preview-first, fail-closed crash reporting.
3
+ *
4
+ * The ordering in `runCrashReportFlow` **is** the consent boundary: nothing
5
+ * touches the network, authentication, the repository or even the `gh` binary
6
+ * until the user has seen the exact bytes and confirmed them against their
7
+ * digest. Automatic issue creation is an explicit non-goal — one field host
8
+ * would have filed hundreds of duplicates for a single bug.
9
+ */
10
+ import { createHash, randomUUID } from "node:crypto";
11
+ import * as fs from "node:fs/promises";
12
+ import * as path from "node:path";
13
+ import { CRASH_ISSUE_MARKER_PREFIX, normalizeCrashFrames, VERSION } from "@gajae-code/utils";
14
+ import type { RunGh } from "../utils/gh";
15
+ import {
16
+ type CrashSignatureView,
17
+ type CrashStatePaths,
18
+ compactCrashIndex,
19
+ listCrashSignatures,
20
+ recordCrashStateEvent,
21
+ } from "./index-store";
22
+ import { findLatestRecord, type LoadedCrashRecord } from "./record-loader";
23
+ import { CRASH_BODY_MAX_BYTES, fenceCrashText, type SanitizeVerdict, sanitizeExternalCrashV1 } from "./sanitize";
24
+
25
+ /** The one repository this command may ever target. */
26
+ export const CRASH_REPORT_REPO = "Yeachan-Heo/gajae-code";
27
+ export const CRASH_REPORT_ISSUE_PREFIX = `https://github.com/${CRASH_REPORT_REPO}/issues/`;
28
+ const GH_TIMEOUT_MS = 15_000;
29
+ const NOT_CAPTURED = "_not captured — please fill in_";
30
+
31
+ export interface CrashReportEnvironment {
32
+ platform: string;
33
+ gjcVersion: string;
34
+ bunVersion: string;
35
+ }
36
+
37
+ export interface CrashReportAnswers {
38
+ steps: string;
39
+ expected: string;
40
+ provider: string;
41
+ area: string;
42
+ additional: string;
43
+ }
44
+
45
+ export function defaultCrashReportEnvironment(): CrashReportEnvironment {
46
+ const platform =
47
+ process.platform === "darwin"
48
+ ? "macOS"
49
+ : process.platform === "win32"
50
+ ? "Windows (native)"
51
+ : process.env.WSL_DISTRO_NAME
52
+ ? "Windows (WSL)"
53
+ : "Linux";
54
+ return { platform, gjcVersion: VERSION, bunVersion: Bun.version };
55
+ }
56
+
57
+ function coarseDate(epochMs: number): string {
58
+ // Exact timestamps are omitted by default; a date is enough to triage.
59
+ return new Date(epochMs).toISOString().slice(0, 10);
60
+ }
61
+
62
+ /** Generic title built from normalized inputs only — never from raw crash text. */
63
+ export function buildCrashReportTitle(signature: CrashSignatureView, record: LoadedCrashRecord): string {
64
+ const frame = normalizeCrashFrames(record.body)[0] ?? "<no-app-frame>";
65
+ const framePath = frame.split("#")[0] ?? frame;
66
+ const verdict = sanitizeExternalCrashV1(`crash: ${signature.errorName} in ${framePath}`, 160);
67
+ return verdict.ok ? verdict.value : `crash: ${CRASH_ISSUE_MARKER_PREFIX}${signature.fingerprint}`;
68
+ }
69
+
70
+ function field(label: string, value: string): string {
71
+ return `## ${label}\n\n${value}\n`;
72
+ }
73
+
74
+ function answerOrPrompt(value: string): SanitizeVerdict {
75
+ const trimmed = value.trim();
76
+ if (!trimmed) return { ok: true, value: NOT_CAPTURED };
77
+ return sanitizeExternalCrashV1(trimmed, 4 * 1024);
78
+ }
79
+
80
+ export interface BuildCrashReportBodyInput {
81
+ signature: CrashSignatureView;
82
+ record: LoadedCrashRecord;
83
+ answers: CrashReportAnswers;
84
+ environment: CrashReportEnvironment;
85
+ }
86
+
87
+ /**
88
+ * Render the full issue body. Every `bug_report.yml` required field is present:
89
+ * crash-derived fields prefilled, non-derivable fields carried through from the
90
+ * interactive answers (or an explicit "not captured" prompt).
91
+ *
92
+ * All crash-derived text lives inside fenced blocks with backticks neutralized
93
+ * and `@` de-fanged; the machine marker is emitted outside those blocks so a
94
+ * forged marker inside crash text cannot impersonate one.
95
+ */
96
+ export function buildCrashReportBody(input: BuildCrashReportBodyInput): SanitizeVerdict {
97
+ const { signature, record, answers, environment } = input;
98
+ const headline = sanitizeExternalCrashV1(record.headline, 1024);
99
+ if (!headline.ok) return headline;
100
+ const stack = sanitizeExternalCrashV1(record.body, 16 * 1024);
101
+ if (!stack.ok) return stack;
102
+ const messageClass = sanitizeExternalCrashV1(signature.messageClass, 1024);
103
+ if (!messageClass.ok) return messageClass;
104
+ const errorName = sanitizeExternalCrashV1(signature.errorName, 128);
105
+ if (!errorName.ok) return errorName;
106
+
107
+ const steps = answerOrPrompt(answers.steps);
108
+ if (!steps.ok) return steps;
109
+ const expected = answerOrPrompt(answers.expected);
110
+ if (!expected.ok) return expected;
111
+ const provider = answerOrPrompt(answers.provider);
112
+ if (!provider.ok) return provider;
113
+ const area = answerOrPrompt(answers.area);
114
+ if (!area.ok) return area;
115
+ const additional = answerOrPrompt(answers.additional);
116
+ if (!additional.ok) return additional;
117
+
118
+ const description =
119
+ `Automatic crash report assembled by \`gjc crash report\` from a local crash record. ` +
120
+ `Crash-derived text below is sanitized (paths, URLs, credentials and control characters removed) ` +
121
+ `and carries no pid or exact timestamps.\n\n` +
122
+ `- Signature: \`${CRASH_ISSUE_MARKER_PREFIX}${signature.fingerprint}\` (algorithm v${signature.fpv})\n` +
123
+ `- Occurrences: ${signature.lifetimeCount} lifetime, ${signature.retainedCount} still in the local crash log\n` +
124
+ `- First seen: ${coarseDate(signature.firstSeen)} — last seen: ${coarseDate(signature.lastSeen)}\n` +
125
+ `- Error class: \`${fenceCrashText(errorName.value)}\`\n` +
126
+ `- Normalized message class:\n\n\`\`\`\n${fenceCrashText(messageClass.value)}\n\`\`\``;
127
+
128
+ const body =
129
+ field("Description", description) +
130
+ `\n${field("Steps to Reproduce", steps.value)}` +
131
+ `\n${field("Expected Behavior", expected.value)}` +
132
+ `\n${field("Error Output", `\`\`\`\n${fenceCrashText(headline.value)}\n${fenceCrashText(stack.value)}\n\`\`\``)}` +
133
+ `\n${field("Platform", environment.platform)}` +
134
+ `\n${field("gjc version", environment.gjcVersion)}` +
135
+ `\n${field("Bun version", environment.bunVersion)}` +
136
+ `\n${field("Provider", provider.value)}` +
137
+ `\n${field("Area", area.value)}` +
138
+ `\n${field("Additional context", additional.value)}` +
139
+ `\n<!-- ${CRASH_ISSUE_MARKER_PREFIX}${signature.fingerprint} -->\n`;
140
+
141
+ if (Buffer.byteLength(body, "utf8") > CRASH_BODY_MAX_BYTES)
142
+ return { ok: false, reason: `report body exceeds ${CRASH_BODY_MAX_BYTES} bytes` };
143
+ return { ok: true, value: body };
144
+ }
145
+
146
+ export interface CrashReportSnapshot {
147
+ path: string;
148
+ digest: string;
149
+ byteLength: number;
150
+ title: string;
151
+ body: string;
152
+ }
153
+
154
+ /**
155
+ * Freeze the exact bytes that may be submitted into a 0600 file.
156
+ *
157
+ * `wx` refuses to follow or clobber an existing path, so a planted symlink
158
+ * cannot redirect the snapshot, and the digest shown to the user is verified
159
+ * again immediately before submission.
160
+ */
161
+ export async function writeCrashReportSnapshot(dir: string, title: string, body: string): Promise<CrashReportSnapshot> {
162
+ await fs.mkdir(dir, { recursive: true, mode: 0o700 });
163
+ const target = path.join(dir, `crash-report-${Date.now()}-${randomUUID()}.md`);
164
+ await fs.writeFile(target, body, { mode: 0o600, flag: "wx" });
165
+ return {
166
+ path: target,
167
+ digest: createHash("sha256").update(body, "utf8").digest("hex"),
168
+ byteLength: Buffer.byteLength(body, "utf8"),
169
+ title,
170
+ body,
171
+ };
172
+ }
173
+
174
+ async function verifySnapshot(snapshot: CrashReportSnapshot): Promise<boolean> {
175
+ try {
176
+ const contents = await fs.readFile(snapshot.path, "utf8");
177
+ return createHash("sha256").update(contents, "utf8").digest("hex") === snapshot.digest;
178
+ } catch {
179
+ return false;
180
+ }
181
+ }
182
+
183
+ const BOUNDED_TITLE = /^[A-Za-z0-9 :._<>#/-]{1,160}$/;
184
+ const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
185
+
186
+ /**
187
+ * Prefilled issue URL for installs without `gh`.
188
+ *
189
+ * Built with `URL`/`URLSearchParams` against a fixed allowlisted origin and
190
+ * carrying only bounded-grammar fields — never message or stack content. Never
191
+ * auto-opened.
192
+ */
193
+ export function buildPrefillUrl(title: string, fingerprint: string, version: string): string | undefined {
194
+ if (!BOUNDED_TITLE.test(title)) return undefined;
195
+ if (!/^[0-9a-f]{32}$/.test(fingerprint)) return undefined;
196
+ if (!SEMVER.test(version)) return undefined;
197
+ const url = new URL(`https://github.com/${CRASH_REPORT_REPO}/issues/new`);
198
+ const params = new URLSearchParams({
199
+ template: "bug_report.yml",
200
+ title,
201
+ description: `Crash signature ${CRASH_ISSUE_MARKER_PREFIX}${fingerprint} (gjc ${version}). Full sanitized report is in the local snapshot file printed by \`gjc crash report\`.`,
202
+ version,
203
+ });
204
+ url.search = params.toString();
205
+ return url.toString();
206
+ }
207
+
208
+ export interface DuplicateCandidate {
209
+ url: string;
210
+ title: string;
211
+ author: string;
212
+ }
213
+
214
+ export type DuplicateCheck =
215
+ | { status: "none" }
216
+ | { status: "candidate"; candidate: DuplicateCandidate }
217
+ | { status: "uncertain"; reason: string };
218
+
219
+ /** Validate that a `gh`-returned URL really points at the canonical repository. */
220
+ export function isCanonicalIssueUrl(value: string): boolean {
221
+ try {
222
+ const url = new URL(value);
223
+ return (
224
+ url.origin === "https://github.com" &&
225
+ url.pathname.startsWith(`/${CRASH_REPORT_REPO}/issues/`) &&
226
+ /^\d+$/.test(url.pathname.slice(`/${CRASH_REPORT_REPO}/issues/`.length))
227
+ );
228
+ } catch {
229
+ return false;
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Read-only duplicate search, always `--repo` pinned. Any timeout, auth failure
235
+ * or unparseable result is `uncertain`, which refuses creation unless the user
236
+ * explicitly overrides.
237
+ */
238
+ export async function checkForDuplicateIssue(runGh: RunGh, fingerprint: string): Promise<DuplicateCheck> {
239
+ const marker = `${CRASH_ISSUE_MARKER_PREFIX}${fingerprint}`;
240
+ const result = await runGh(
241
+ [
242
+ "issue",
243
+ "list",
244
+ "--repo",
245
+ CRASH_REPORT_REPO,
246
+ "--state",
247
+ "all",
248
+ "--search",
249
+ `"${marker}" in:body`,
250
+ "--limit",
251
+ "5",
252
+ "--json",
253
+ "url,title,author",
254
+ ],
255
+ { timeoutMs: GH_TIMEOUT_MS },
256
+ );
257
+ if (result.timedOut) return { status: "uncertain", reason: "duplicate search timed out" };
258
+ if (result.exitCode !== 0)
259
+ return { status: "uncertain", reason: `duplicate search failed: ${result.stderr.trim() || "unknown error"}` };
260
+ let parsed: unknown;
261
+ try {
262
+ parsed = JSON.parse(result.stdout) as unknown;
263
+ } catch {
264
+ return { status: "uncertain", reason: "duplicate search returned unparseable output" };
265
+ }
266
+ if (!Array.isArray(parsed)) return { status: "uncertain", reason: "duplicate search returned unexpected shape" };
267
+ if (parsed.length === 0) return { status: "none" };
268
+ const first = parsed[0] as { url?: unknown; title?: unknown; author?: unknown };
269
+ if (typeof first.url !== "string" || !isCanonicalIssueUrl(first.url))
270
+ return { status: "uncertain", reason: "duplicate search returned a non-canonical URL" };
271
+ const title = typeof first.title === "string" ? first.title : "(untitled)";
272
+ const author =
273
+ first.author &&
274
+ typeof first.author === "object" &&
275
+ typeof (first.author as { login?: unknown }).login === "string"
276
+ ? ((first.author as { login: string }).login satisfies string)
277
+ : "unknown";
278
+ // A marker in a body is forgeable, so this is a candidate only: the default
279
+ // action prints the URL and stops.
280
+ return { status: "candidate", candidate: { url: first.url, title, author } };
281
+ }
282
+
283
+ export interface CrashReportIo {
284
+ print(text: string): void;
285
+ /** Returns the chosen index, or undefined when cancelled. */
286
+ select(prompt: string, options: string[]): Promise<number | undefined>;
287
+ input(prompt: string): Promise<string>;
288
+ confirm(prompt: string): Promise<boolean>;
289
+ }
290
+
291
+ export interface CrashReportDeps {
292
+ io: CrashReportIo;
293
+ paths: CrashStatePaths;
294
+ snapshotDir: string;
295
+ runGh: RunGh;
296
+ interactive: boolean;
297
+ environment?: CrashReportEnvironment;
298
+ now?: () => Date;
299
+ }
300
+
301
+ export type CrashReportOutcome =
302
+ | { status: "no-signatures" }
303
+ | { status: "non-interactive"; snapshotPath?: string }
304
+ | { status: "cancelled" }
305
+ | { status: "acknowledged"; fingerprint: string }
306
+ | { status: "refused"; reason: string }
307
+ | { status: "unmatchable"; fingerprint: string }
308
+ | { status: "manual"; snapshotPath: string; prefillUrl?: string }
309
+ | { status: "duplicate"; url: string }
310
+ | { status: "commented"; url: string }
311
+ | { status: "created"; url: string };
312
+
313
+ function describeSignature(signature: CrashSignatureView): string {
314
+ const reported = signature.reportedAt !== undefined ? "reported" : "unreported";
315
+ const acknowledged = signature.acknowledgedAt !== undefined ? ", dismissed" : "";
316
+ return (
317
+ `${signature.fingerprint} ${signature.errorName}: ${signature.messageClass.slice(0, 80)}\n` +
318
+ ` ${signature.lifetimeCount}x (${signature.retainedCount} retained), ` +
319
+ `${coarseDate(signature.firstSeen)} → ${coarseDate(signature.lastSeen)}, fpv:${signature.fpv}, ${reported}${acknowledged}`
320
+ );
321
+ }
322
+
323
+ async function readCrashLog(crashLogPath: string): Promise<string> {
324
+ try {
325
+ return await fs.readFile(crashLogPath, "utf8");
326
+ } catch {
327
+ return "";
328
+ }
329
+ }
330
+
331
+ /**
332
+ * The whole flow. Steps 1–4 are strictly local; `deps.runGh` is first reachable
333
+ * at step 5, after the digest-confirmed consent gate.
334
+ */
335
+ export async function runCrashReportFlow(deps: CrashReportDeps): Promise<CrashReportOutcome> {
336
+ const now = deps.now ?? (() => new Date());
337
+ const environment = deps.environment ?? defaultCrashReportEnvironment();
338
+ const index = await compactCrashIndex({ paths: deps.paths, now: now().getTime() });
339
+ const signatures = listCrashSignatures(index);
340
+
341
+ if (index.overflow)
342
+ deps.io.print(
343
+ "warning: the crash index is full and stopped recording new signatures; report or dismiss existing ones to make room.\n",
344
+ );
345
+ if (signatures.length === 0) {
346
+ deps.io.print("No crash signatures recorded.\n");
347
+ return { status: "no-signatures" };
348
+ }
349
+
350
+ const contents = await readCrashLog(deps.paths.crashLog);
351
+
352
+ if (!deps.interactive) {
353
+ deps.io.print(`${signatures.length} crash signature(s):\n`);
354
+ for (const signature of signatures) deps.io.print(` ${describeSignature(signature)}\n`);
355
+ // Headless callers still get the artifact, never the transmission: the
356
+ // newest reportable signature is rendered to a snapshot file they can read,
357
+ // edit and submit by hand.
358
+ const newest = signatures.find(candidate => findLatestRecord(contents, candidate.fingerprint) !== undefined);
359
+ const record = newest ? findLatestRecord(contents, newest.fingerprint) : undefined;
360
+ let snapshotPath: string | undefined;
361
+ if (newest && record) {
362
+ const built = buildCrashReportBody({
363
+ signature: newest,
364
+ record,
365
+ answers: { steps: "", expected: "", provider: "", area: "", additional: "" },
366
+ environment,
367
+ });
368
+ if (built.ok) {
369
+ const snapshot = await writeCrashReportSnapshot(
370
+ deps.snapshotDir,
371
+ buildCrashReportTitle(newest, record),
372
+ built.value,
373
+ );
374
+ snapshotPath = snapshot.path;
375
+ deps.io.print(`\nReport draft for ${newest.fingerprint}: ${snapshot.path}\n`);
376
+ }
377
+ }
378
+ deps.io.print(
379
+ "\nSubmission requires an interactive terminal; nothing was transmitted. Re-run in a TTY to review and submit a report.\n",
380
+ );
381
+ return { status: "non-interactive", snapshotPath };
382
+ }
383
+
384
+ const options = signatures.map(describeSignature);
385
+ options.push("Cancel");
386
+ const chosen = await deps.io.select("Select a crash signature", options);
387
+ if (chosen === undefined || chosen >= signatures.length) return { status: "cancelled" };
388
+ const signature = signatures[chosen] as CrashSignatureView;
389
+
390
+ const action = await deps.io.select(`Signature ${signature.fingerprint}`, [
391
+ "Prepare a report for review",
392
+ "Dismiss this signature (stops the startup nudge)",
393
+ "Cancel",
394
+ ]);
395
+ if (action === undefined || action === 2) return { status: "cancelled" };
396
+ if (action === 1) {
397
+ const eventAt = now().getTime();
398
+ await recordCrashStateEvent(
399
+ { kind: "acknowledged", fingerprint: signature.fingerprint, at: eventAt },
400
+ { paths: deps.paths, now: eventAt },
401
+ );
402
+ deps.io.print(`Dismissed ${signature.fingerprint}.\n`);
403
+ return { status: "acknowledged", fingerprint: signature.fingerprint };
404
+ }
405
+
406
+ const record = findLatestRecord(contents, signature.fingerprint);
407
+ if (!record) {
408
+ deps.io.print(
409
+ "No identity-bearing crash record is available for this signature. Records written before crash fingerprinting are unmatchable and cannot be reported through this flow.\n",
410
+ );
411
+ return { status: "unmatchable", fingerprint: signature.fingerprint };
412
+ }
413
+
414
+ deps.io.print(
415
+ "\nThe following fields are not derivable from a crash record. Leave blank to submit an explicit " +
416
+ '"not captured" prompt instead.\n',
417
+ );
418
+ const answers: CrashReportAnswers = {
419
+ steps: await deps.io.input("Steps to reproduce"),
420
+ expected: await deps.io.input("Expected behavior"),
421
+ provider: await deps.io.input("Provider (blank if not provider-specific)"),
422
+ area: await deps.io.input("Area (TUI, Tool execution, CLI commands, …)"),
423
+ additional: await deps.io.input("Additional context"),
424
+ };
425
+
426
+ const title = buildCrashReportTitle(signature, record);
427
+ const built = buildCrashReportBody({ signature, record, answers, environment });
428
+ if (!built.ok) {
429
+ deps.io.print(`Refusing to submit: ${built.reason}\n`);
430
+ return { status: "refused", reason: built.reason };
431
+ }
432
+
433
+ const snapshot = await writeCrashReportSnapshot(deps.snapshotDir, title, built.value);
434
+ deps.io.print(
435
+ `\n--- exact bytes to be submitted (sha256 ${snapshot.digest}, ${snapshot.byteLength} bytes) ---\n` +
436
+ `Title: ${snapshot.title}\n\n${snapshot.body}\n--- end of snapshot ---\n` +
437
+ `Snapshot file: ${snapshot.path}\n` +
438
+ `Target repository: ${CRASH_REPORT_REPO} (fixed)\n` +
439
+ "The signature is a public, pseudonymous correlation token: it is deterministic over normalized crash text, " +
440
+ "dictionary-testable, and links the same crash class across installs. It is not a confidentiality control.\n",
441
+ );
442
+
443
+ if (!(await deps.io.confirm(`Submit exactly these ${snapshot.byteLength} bytes to ${CRASH_REPORT_REPO}?`))) {
444
+ deps.io.print(`Nothing was transmitted. The report stays at ${snapshot.path}.\n`);
445
+ return { status: "cancelled" };
446
+ }
447
+
448
+ // ---- consent boundary: `gh` may run from here on, and not one step earlier.
449
+ if (!(await verifySnapshot(snapshot))) {
450
+ deps.io.print("Refusing to submit: the snapshot file changed after it was confirmed.\n");
451
+ return { status: "refused", reason: "snapshot digest mismatch" };
452
+ }
453
+
454
+ const identity = await deps.runGh(["api", "user", "--jq", ".login"], { timeoutMs: GH_TIMEOUT_MS });
455
+ if (identity.exitCode !== 0) {
456
+ const prefillUrl = buildPrefillUrl(snapshot.title, signature.fingerprint, environment.gjcVersion);
457
+ deps.io.print(
458
+ `GitHub CLI is unavailable or unauthenticated (${identity.stderr.trim() || "no gh"}).\n` +
459
+ `Nothing was transmitted. Submit manually with the snapshot at ${snapshot.path}.\n` +
460
+ (prefillUrl ? `Prefilled issue form (not opened automatically): ${prefillUrl}\n` : ""),
461
+ );
462
+ return { status: "manual", snapshotPath: snapshot.path, prefillUrl };
463
+ }
464
+ deps.io.print(`Active gh identity: ${identity.stdout.trim() || "unknown"}\n`);
465
+
466
+ const duplicate = await checkForDuplicateIssue(deps.runGh, signature.fingerprint);
467
+ if (duplicate.status === "uncertain") {
468
+ deps.io.print(`Duplicate check inconclusive: ${duplicate.reason}\n`);
469
+ if (!(await deps.io.confirm("Skip the duplicate check and create a new issue anyway?"))) {
470
+ deps.io.print(`Nothing was transmitted. The report stays at ${snapshot.path}.\n`);
471
+ return { status: "refused", reason: duplicate.reason };
472
+ }
473
+ } else if (duplicate.status === "candidate") {
474
+ const { candidate } = duplicate;
475
+ deps.io.print(
476
+ `\nA possible existing report carries this signature (markers are forgeable, so this is a candidate only):\n` +
477
+ ` ${candidate.url}\n "${candidate.title}" by ${candidate.author}\n`,
478
+ );
479
+ const already = index.signatures[signature.fingerprint]?.commentedIssues?.includes(candidate.url) === true;
480
+ const choice = await deps.io.select("What would you like to do?", [
481
+ "Stop here (recommended) — the existing issue already tracks this",
482
+ already ? 'Add a "+1" comment (already commented from this install)' : 'Add a "+1" comment to that issue',
483
+ ]);
484
+ if (choice !== 1) {
485
+ deps.io.print(`Nothing was transmitted. Existing issue: ${candidate.url}\n`);
486
+ return { status: "duplicate", url: candidate.url };
487
+ }
488
+ if (already) {
489
+ deps.io.print(`This install already commented on ${candidate.url}; not commenting again.\n`);
490
+ return { status: "duplicate", url: candidate.url };
491
+ }
492
+ if (
493
+ !(await deps.io.confirm(
494
+ `Post a public "+1" comment as ${identity.stdout.trim() || "your gh identity"} on ${candidate.url}?`,
495
+ ))
496
+ ) {
497
+ deps.io.print(`Nothing was transmitted. Existing issue: ${candidate.url}\n`);
498
+ return { status: "duplicate", url: candidate.url };
499
+ }
500
+ const comment = await deps.runGh(
501
+ [
502
+ "issue",
503
+ "comment",
504
+ candidate.url,
505
+ "--repo",
506
+ CRASH_REPORT_REPO,
507
+ "--body",
508
+ `Also hit here: \`${CRASH_ISSUE_MARKER_PREFIX}${signature.fingerprint}\` (gjc ${environment.gjcVersion}, ${environment.platform}).`,
509
+ ],
510
+ { timeoutMs: GH_TIMEOUT_MS },
511
+ );
512
+ if (comment.exitCode !== 0) {
513
+ deps.io.print(`Comment failed: ${comment.stderr.trim() || "unknown error"}\n`);
514
+ return { status: "refused", reason: "comment failed" };
515
+ }
516
+ const eventAt = now().getTime();
517
+ await recordCrashStateEvent(
518
+ {
519
+ kind: "reported",
520
+ fingerprint: signature.fingerprint,
521
+ at: eventAt,
522
+ issueUrl: candidate.url,
523
+ commented: true,
524
+ },
525
+ { paths: deps.paths, now: eventAt },
526
+ );
527
+ deps.io.print(`Commented on ${candidate.url}\n`);
528
+ return { status: "commented", url: candidate.url };
529
+ }
530
+
531
+ const created = await deps.runGh(
532
+ ["issue", "create", "--repo", CRASH_REPORT_REPO, "--title", snapshot.title, "--body-file", snapshot.path],
533
+ { timeoutMs: GH_TIMEOUT_MS },
534
+ );
535
+ if (created.exitCode !== 0) {
536
+ deps.io.print(`Issue creation failed: ${created.stderr.trim() || "unknown error"}\n`);
537
+ return { status: "refused", reason: "issue creation failed" };
538
+ }
539
+ const url = created.stdout.trim().split("\n").at(-1) ?? "";
540
+ if (!isCanonicalIssueUrl(url)) {
541
+ deps.io.print(`Issue creation returned an unexpected URL; not recording it: ${url}\n`);
542
+ return { status: "refused", reason: "non-canonical issue URL" };
543
+ }
544
+ const eventAt = now().getTime();
545
+ await recordCrashStateEvent(
546
+ { kind: "reported", fingerprint: signature.fingerprint, at: eventAt, issueUrl: url },
547
+ { paths: deps.paths, now: eventAt },
548
+ );
549
+ deps.io.print(`Created ${url}\n`);
550
+ return { status: "created", url };
551
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * `sanitizeExternalCrashV1` — the outbound contract for crash text.
3
+ *
4
+ * This is deliberately a *different, stricter* contract than the persistence
5
+ * scrub in `redactCrashSecrets`. That one is best-effort credential hygiene for
6
+ * a local file; this one governs bytes that leave the machine, so it is an
7
+ * allowlist with a residual scanner and a fail-closed verdict: if the scanner
8
+ * is not certain the output is clean, the submission is refused rather than
9
+ * "warned and continued".
10
+ *
11
+ * Applied to every crash-derived string:
12
+ * - CRLF normalized, C0/C1/ANSI/OSC/bidi/zero-width controls removed
13
+ * - absolute POSIX/Windows/UNC/BunFS paths and home prefixes → placeholders
14
+ * - URLs parsed; userinfo, query and fragment dropped; unparseable URLs dropped
15
+ * - credential shapes rewritten to redaction markers
16
+ * - UUIDs and long hex runs (account/session/request ids) → placeholders
17
+ * - pid and exact timestamps are never included (coarse dates only)
18
+ * - per-field and whole-body byte caps
19
+ */
20
+ import { redactCrashSecrets, replaceAbsolutePaths } from "@gajae-code/utils";
21
+
22
+ /** High-entropy identifiers: account/session/request ids carry no triage value. */
23
+ const UUID_PATTERN = /\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b/g;
24
+ const HEX_RUN_PATTERN = /\b[0-9a-fA-F]{12,}\b/g;
25
+
26
+ /** Whole-body cap for a generated issue body. */
27
+ export const CRASH_BODY_MAX_BYTES = 48 * 1024;
28
+ /** Per-field cap for crash-derived text. */
29
+ export const CRASH_FIELD_MAX_BYTES = 8 * 1024;
30
+
31
+ export type SanitizeVerdict = { ok: true; value: string } | { ok: false; reason: string };
32
+
33
+ const OSC_PATTERN = /\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)?/g;
34
+ const CSI_PATTERN = /\u001b\[[0-9;?]*[ -/]*[@-~]/g;
35
+ const CONTROL_CLASS = "[\\u0000-\\u0008\\u000b-\\u001f\\u007f-\\u009f]";
36
+ const INVISIBLE_CLASS = "[\\u200b-\\u200f\\u2028\\u2029\\u202a-\\u202e\\u2060-\\u2064\\u2066-\\u2069\\ufeff]";
37
+ const CONTROL_PATTERN = new RegExp(CONTROL_CLASS, "g");
38
+ const INVISIBLE_PATTERN = new RegExp(INVISIBLE_CLASS, "g");
39
+ // Separate non-global instances: `RegExp.test` on a /g regex is stateful, and a
40
+ // residual scanner that silently alternates verdicts is worse than none.
41
+ const CONTROL_PROBE = new RegExp(CONTROL_CLASS);
42
+ const INVISIBLE_PROBE = new RegExp(INVISIBLE_CLASS);
43
+ const URL_PATTERN = /\b[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s<>"'`)\]]+/g;
44
+ /** Anything still matching these after sanitization means the scanner is not certain. */
45
+ const RESIDUAL_PATTERNS: readonly RegExp[] = [
46
+ /\bsk-[A-Za-z0-9_-]{8,}/,
47
+ /\bgh[opsur]_[A-Za-z0-9]{16,}/,
48
+ /\bgithub_pat_[A-Za-z0-9_]{20,}/,
49
+ /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\./,
50
+ /\bxox[baprs]-[A-Za-z0-9-]{8,}/,
51
+ /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/,
52
+ // Opaque URIs the URL rule cannot parse into an origin, and inline payloads.
53
+ /\bdata:[^\s]{0,64};base64,/i,
54
+ /:\/\//,
55
+ /(?<![\w.~])\/[A-Za-z0-9._-]+\//,
56
+ /\b[A-Za-z]:\\/,
57
+ /\\\\[A-Za-z0-9]/,
58
+ ];
59
+
60
+ function truncateUtf8(text: string, maxBytes: number): string {
61
+ if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
62
+ const bytes = Buffer.from(text, "utf8");
63
+ let end = maxBytes;
64
+ while (end > 0 && ((bytes[end - 1] ?? 0) & 0xc0) === 0x80) end--;
65
+ if (end > 0 && (bytes[end - 1] ?? 0) >= 0xc0) end--;
66
+ return `${bytes.subarray(0, end).toString("utf8")}…[truncated]`;
67
+ }
68
+
69
+ /**
70
+ * Reduce one URL to origin + path. Userinfo, query and fragment are the parts
71
+ * that routinely carry tokens and identifiers, so they are dropped outright;
72
+ * an unparseable or non-http(s) URL is dropped entirely.
73
+ */
74
+ function sanitizeUrl(raw: string): string {
75
+ let parsed: URL;
76
+ try {
77
+ parsed = new URL(raw);
78
+ } catch {
79
+ return "«url dropped»";
80
+ }
81
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return "«url dropped»";
82
+ const host = parsed.hostname;
83
+ if (!host) return "«url dropped»";
84
+ const pathname = parsed.pathname === "/" ? "" : parsed.pathname;
85
+ return `«url ${host}${pathname}»`;
86
+ }
87
+
88
+ /**
89
+ * Sanitize one crash-derived field. Returns a verdict: `ok:false` means the
90
+ * residual scanner still found something it cannot vouch for, and the caller
91
+ * must refuse to submit.
92
+ */
93
+ export function sanitizeExternalCrashV1(text: string, maxBytes: number = CRASH_FIELD_MAX_BYTES): SanitizeVerdict {
94
+ if (typeof text !== "string") return { ok: false, reason: "field is not text" };
95
+ let value = text.replace(/\r\n?/g, "\n");
96
+ value = value.replace(OSC_PATTERN, " ").replace(CSI_PATTERN, "");
97
+ value = value.replace(CONTROL_PATTERN, "").replace(INVISIBLE_PATTERN, "");
98
+ value = redactCrashSecrets(value);
99
+ value = value.replace(URL_PATTERN, match => sanitizeUrl(match));
100
+ value = replaceAbsolutePaths(value);
101
+ value = value.replace(UUID_PATTERN, "<uuid>");
102
+ value = value.replace(HEX_RUN_PATTERN, match => (/[a-fA-F]/.test(match) ? "<hex>" : "<num>"));
103
+ value = value
104
+ .split("\n")
105
+ .map(line => line.replace(/[ \t]+$/, ""))
106
+ .join("\n")
107
+ .trim();
108
+ value = truncateUtf8(value, maxBytes);
109
+ for (const pattern of RESIDUAL_PATTERNS) {
110
+ if (pattern.test(value)) return { ok: false, reason: `residual scanner matched ${pattern.source}` };
111
+ }
112
+ if (CONTROL_PROBE.test(value) || INVISIBLE_PROBE.test(value))
113
+ return { ok: false, reason: "residual control characters" };
114
+ return { ok: true, value };
115
+ }
116
+
117
+ /**
118
+ * Wrap already-sanitized crash-derived text for a fenced Markdown block:
119
+ * backticks are neutralized so the fence cannot be escaped, and `@` is de-fanged
120
+ * so a crash message can never notify a GitHub user or team.
121
+ */
122
+ export function fenceCrashText(value: string): string {
123
+ return value.replace(/`/g, "'").replace(/@/g, "(at)");
124
+ }
125
+
126
+ /** Sanitize a label (field name / heading) with the same rules as a value. */
127
+ export function sanitizeLabel(label: string): SanitizeVerdict {
128
+ return sanitizeExternalCrashV1(label, 128);
129
+ }