@gajae-code/coding-agent 0.18.11 → 0.19.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 (462) hide show
  1. package/CHANGELOG.md +760 -0
  2. package/dist/types/async/job-manager.d.ts +7 -3
  3. package/dist/types/async/result-formatting.d.ts +2 -0
  4. package/dist/types/autoresearch/git.d.ts +1 -1
  5. package/dist/types/capability/fs.d.ts +2 -2
  6. package/dist/types/capability/rule.d.ts +7 -2
  7. package/dist/types/cli/accounts-cli.d.ts +2 -2
  8. package/dist/types/cli/append-system-prompt.d.ts +6 -0
  9. package/dist/types/cli/args.d.ts +7 -2
  10. package/dist/types/cli/customize-doctor-precedence.d.ts +16 -0
  11. package/dist/types/cli/customize-doctor.d.ts +4 -3
  12. package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
  13. package/dist/types/cli/install-activation.d.ts +10 -4
  14. package/dist/types/cli/public-command-errors.d.ts +2 -0
  15. package/dist/types/cli/root-flags.d.ts +0 -12
  16. package/dist/types/cli/update-cli.d.ts +21 -3
  17. package/dist/types/cli-main.d.ts +0 -6
  18. package/dist/types/cli-ordinary.d.ts +2 -2
  19. package/dist/types/commands/accounts.d.ts +2 -1
  20. package/dist/types/commands/launch.d.ts +0 -6
  21. package/dist/types/commands/migrate.d.ts +1 -1
  22. package/dist/types/commands/sdk.d.ts +13 -0
  23. package/dist/types/commit/changelog/index.d.ts +1 -0
  24. package/dist/types/commit/map-reduce/index.d.ts +4 -4
  25. package/dist/types/config/autorouting-tier-map.d.ts +53 -1
  26. package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
  27. package/dist/types/config/config-hot-reload.d.ts +44 -0
  28. package/dist/types/config/file-lock.d.ts +52 -3
  29. package/dist/types/config/model-discovery-manager.d.ts +2 -0
  30. package/dist/types/config/model-profile-activation.d.ts +79 -9
  31. package/dist/types/config/model-registry.d.ts +72 -8
  32. package/dist/types/config/model-resolver.d.ts +7 -4
  33. package/dist/types/config/models-config-schema.d.ts +10 -0
  34. package/dist/types/config/settings-schema.d.ts +57 -4
  35. package/dist/types/config/settings.d.ts +33 -1
  36. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  37. package/dist/types/customization/import.d.ts +2 -0
  38. package/dist/types/customization/types.d.ts +1 -1
  39. package/dist/types/dap/config.d.ts +8 -4
  40. package/dist/types/dap/session.d.ts +11 -2
  41. package/dist/types/debug/system-info.d.ts +6 -1
  42. package/dist/types/discovery/helpers.d.ts +7 -2
  43. package/dist/types/discovery/mcp-compat.d.ts +3 -1
  44. package/dist/types/edit/index.d.ts +5 -0
  45. package/dist/types/edit/modes/patch.d.ts +14 -0
  46. package/dist/types/edit/modes/replace.d.ts +3 -0
  47. package/dist/types/eval/py/runtime.d.ts +9 -0
  48. package/dist/types/exec/bash-executor.d.ts +12 -0
  49. package/dist/types/export/ttsr.d.ts +19 -0
  50. package/dist/types/extensibility/extensions/types.d.ts +5 -1
  51. package/dist/types/extensibility/slash-commands.d.ts +1 -0
  52. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
  53. package/dist/types/gjc-runtime/ralplan-current-final.d.ts +54 -0
  54. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -0
  55. package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
  56. package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
  57. package/dist/types/gjc-runtime/tmux-sessions.d.ts +24 -0
  58. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +6 -0
  59. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  60. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  61. package/dist/types/hindsight/content.d.ts +5 -6
  62. package/dist/types/internal-urls/types.d.ts +2 -0
  63. package/dist/types/lsp/clients/swiftlint-client.d.ts +0 -4
  64. package/dist/types/main.d.ts +23 -18
  65. package/dist/types/migrate/action-planner.d.ts +10 -1
  66. package/dist/types/migrate/adapters/codex.d.ts +2 -1
  67. package/dist/types/migrate/adapters/index.d.ts +42 -1
  68. package/dist/types/migrate/executor.d.ts +3 -1
  69. package/dist/types/migrate/hook-module.d.ts +23 -0
  70. package/dist/types/migrate/report.d.ts +2 -1
  71. package/dist/types/migrate/types.d.ts +82 -3
  72. package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
  73. package/dist/types/modes/components/model-selector.d.ts +43 -0
  74. package/dist/types/modes/components/settings-selector.d.ts +6 -0
  75. package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
  76. package/dist/types/modes/prompt-suggestion-controller.d.ts +1 -1
  77. package/dist/types/modes/theme/theme.d.ts +1 -0
  78. package/dist/types/modes/utils/host-status.d.ts +15 -11
  79. package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
  80. package/dist/types/runtime-mcp/manager.d.ts +7 -0
  81. package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
  82. package/dist/types/runtime-mcp/project-host-values.d.ts +11 -5
  83. package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
  84. package/dist/types/sdk/broker/broker-exit.d.ts +3 -3
  85. package/dist/types/sdk/broker/broker.d.ts +29 -2
  86. package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
  87. package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
  88. package/dist/types/sdk/broker/ensure.d.ts +141 -9
  89. package/dist/types/sdk/broker/hop.d.ts +45 -0
  90. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
  91. package/dist/types/sdk/broker/lifecycle.d.ts +22 -0
  92. package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
  93. package/dist/types/sdk/broker/session-index.d.ts +6 -1
  94. package/dist/types/sdk/broker/transport.d.ts +2 -0
  95. package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
  96. package/dist/types/sdk/bus/index.d.ts +44 -0
  97. package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
  98. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -1
  99. package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
  100. package/dist/types/sdk/bus/threaded-render.d.ts +2 -1
  101. package/dist/types/sdk/cli/session-cli.d.ts +0 -1
  102. package/dist/types/sdk/host/control/operations.d.ts +5 -1
  103. package/dist/types/sdk/host/events.d.ts +4 -0
  104. package/dist/types/sdk/host/host.d.ts +2 -1
  105. package/dist/types/sdk/host/prompt-image-upload.d.ts +43 -0
  106. package/dist/types/sdk/host/session-runtime.d.ts +40 -8
  107. package/dist/types/sdk/host/surface-policy.d.ts +2 -1
  108. package/dist/types/sdk/lifecycle/service.d.ts +12 -0
  109. package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
  110. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
  111. package/dist/types/sdk/prompt-status.d.ts +11 -4
  112. package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
  113. package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
  114. package/dist/types/sdk/session-list.d.ts +1 -1
  115. package/dist/types/session/agent-session.d.ts +107 -9
  116. package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
  117. package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
  118. package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
  119. package/dist/types/session/messages.d.ts +5 -1
  120. package/dist/types/session/session-manager.d.ts +29 -3
  121. package/dist/types/session/startup-auth-config.d.ts +24 -2
  122. package/dist/types/session/streaming-output.d.ts +11 -0
  123. package/dist/types/session/terminal-abort.d.ts +3 -1
  124. package/dist/types/session-import/service.d.ts +1 -1
  125. package/dist/types/skill-state/workflow-mutation-guard.d.ts +20 -4
  126. package/dist/types/slash-commands/helpers/mcp.d.ts +5 -0
  127. package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
  128. package/dist/types/ssh/ssh-executor.d.ts +8 -1
  129. package/dist/types/system-prompt.d.ts +3 -2
  130. package/dist/types/task/executor.d.ts +19 -2
  131. package/dist/types/task/role-body.d.ts +11 -0
  132. package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
  133. package/dist/types/tools/atomic-file-write.d.ts +9 -7
  134. package/dist/types/tools/awaiting-input.d.ts +28 -0
  135. package/dist/types/tools/bash-allowed-prefixes.d.ts +17 -0
  136. package/dist/types/tools/bash-skill-urls.d.ts +4 -0
  137. package/dist/types/tools/bash.d.ts +7 -0
  138. package/dist/types/tools/descriptors.d.ts +15 -2
  139. package/dist/types/tools/fetch.d.ts +7 -1
  140. package/dist/types/tools/index.d.ts +2 -0
  141. package/dist/types/tools/irc.d.ts +5 -3
  142. package/dist/types/tools/python.d.ts +1 -1
  143. package/dist/types/tools/report-issue.d.ts +41 -0
  144. package/dist/types/tools/subagent.d.ts +23 -0
  145. package/dist/types/tools/tmux-self-injection-guard.d.ts +2 -0
  146. package/dist/types/tools/ultragoal-ask-guard.d.ts +1 -1
  147. package/dist/types/tools/write.d.ts +2 -0
  148. package/dist/types/utils/file-mentions.d.ts +5 -0
  149. package/dist/types/utils/image-limits.d.ts +7 -0
  150. package/dist/types/utils/image-loading.d.ts +0 -1
  151. package/dist/types/utils/open.d.ts +0 -1
  152. package/dist/types/utils/pasted-image-loading.d.ts +1 -5
  153. package/dist/types/utils/prompt-suggestion.d.ts +2 -2
  154. package/dist/types/utils/shell-snapshot.d.ts +2 -0
  155. package/dist/types/utils/title-generator.d.ts +13 -3
  156. package/dist/types/web/scrapers/types.d.ts +8 -1
  157. package/dist/types/web/search/providers/codex.d.ts +6 -7
  158. package/dist/types/web/search/providers/insane.d.ts +7 -1
  159. package/dist/types/web/search/providers/searxng.d.ts +5 -0
  160. package/package.json +10 -9
  161. package/scripts/acp-conformance-agent.ts +12 -2
  162. package/scripts/build-binary.ts +2 -1
  163. package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
  164. package/scripts/generate-docs-index.ts +8 -1
  165. package/scripts/generate-sdk-operation-inventory.ts +38 -4
  166. package/src/async/job-manager.ts +356 -56
  167. package/src/async/result-formatting.ts +40 -0
  168. package/src/autoresearch/git.ts +9 -2
  169. package/src/autoresearch/prompts/prompt-setup.md +1 -1
  170. package/src/capability/fs.ts +58 -22
  171. package/src/capability/index.ts +2 -2
  172. package/src/capability/rule.ts +11 -2
  173. package/src/cli/accounts-cli.ts +176 -8
  174. package/src/cli/append-system-prompt.ts +12 -0
  175. package/src/cli/args.ts +11 -7
  176. package/src/cli/auth-broker-cli.ts +21 -11
  177. package/src/cli/customize-doctor-precedence.ts +31 -0
  178. package/src/cli/customize-doctor.ts +49 -39
  179. package/src/cli/doctor/install-repairs.ts +35 -5
  180. package/src/cli/fast-help.ts +1 -3
  181. package/src/cli/install-activation.ts +47 -4
  182. package/src/cli/migrate-cli.ts +19 -6
  183. package/src/cli/public-command-errors.ts +6 -0
  184. package/src/cli/root-flags.ts +2 -4
  185. package/src/cli/stats-cli.ts +9 -5
  186. package/src/cli/update-cli.ts +471 -13
  187. package/src/cli-ordinary.ts +7 -2
  188. package/src/commands/accounts.ts +10 -5
  189. package/src/commands/harness.ts +25 -15
  190. package/src/commands/migrate.ts +3 -3
  191. package/src/commands/sdk.ts +277 -50
  192. package/src/commands/ultragoal.ts +16 -8
  193. package/src/commit/agentic/agent.ts +6 -6
  194. package/src/commit/analysis/conventional.ts +14 -5
  195. package/src/commit/changelog/index.ts +39 -1
  196. package/src/commit/map-reduce/index.ts +7 -7
  197. package/src/commit/message.ts +10 -3
  198. package/src/commit/pipeline.ts +12 -12
  199. package/src/config/autorouting-tier-map.ts +50 -1
  200. package/src/config/config-file.ts +106 -3
  201. package/src/config/config-hot-reload-trust.ts +148 -0
  202. package/src/config/config-hot-reload.ts +742 -0
  203. package/src/config/file-lock.ts +1615 -328
  204. package/src/config/keybindings.ts +154 -7
  205. package/src/config/model-bindings-applier.ts +85 -47
  206. package/src/config/model-discovery-manager.ts +10 -0
  207. package/src/config/model-profile-activation.ts +889 -148
  208. package/src/config/model-registry.ts +1706 -205
  209. package/src/config/model-resolver.ts +52 -22
  210. package/src/config/models-config-schema.ts +6 -0
  211. package/src/config/settings-schema.ts +73 -4
  212. package/src/config/settings.ts +460 -24
  213. package/src/coordinator-mcp/policy.ts +26 -1
  214. package/src/coordinator-mcp/server.ts +37 -2
  215. package/src/crash/sanitize.ts +7 -1
  216. package/src/customization/import.ts +279 -8
  217. package/src/customization/inventory.ts +24 -8
  218. package/src/customization/types.ts +1 -0
  219. package/src/dap/client.ts +3 -2
  220. package/src/dap/config.ts +31 -11
  221. package/src/dap/defaults.json +1 -1
  222. package/src/dap/session.ts +161 -36
  223. package/src/debug/crash-diagnostics.ts +89 -6
  224. package/src/debug/report-bundle.ts +90 -2
  225. package/src/debug/system-info.ts +8 -3
  226. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
  227. package/src/discovery/agents-md.ts +1 -1
  228. package/src/discovery/builtin.ts +4 -3
  229. package/src/discovery/helpers.ts +25 -16
  230. package/src/discovery/mcp-compat.ts +120 -17
  231. package/src/edit/apply-patch/index.ts +12 -1
  232. package/src/edit/index.ts +204 -22
  233. package/src/edit/modes/patch.ts +214 -8
  234. package/src/edit/modes/replace.ts +136 -0
  235. package/src/eval/js/context-manager.ts +29 -3
  236. package/src/eval/js/worker-entry.ts +51 -18
  237. package/src/eval/py/executor.ts +14 -4
  238. package/src/eval/py/runner.py +144 -5
  239. package/src/eval/py/runtime.ts +44 -4
  240. package/src/exec/bash-executor.ts +81 -4
  241. package/src/exec/bash-shell-guardian.ts +4 -1
  242. package/src/exec/bash-shell-supervisor.ts +8 -4
  243. package/src/exec/isolated-shell.ts +5 -1
  244. package/src/exec/non-interactive-env.ts +0 -1
  245. package/src/export/html/index.ts +86 -6
  246. package/src/export/ttsr.ts +176 -1
  247. package/src/extensibility/custom-tools/wrapper.ts +3 -1
  248. package/src/extensibility/extensions/loader.ts +10 -1
  249. package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +63 -1
  250. package/src/extensibility/extensions/types.ts +5 -1
  251. package/src/extensibility/hooks/loader.ts +10 -2
  252. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
  253. package/src/extensibility/plugins/marketplace/manager.ts +36 -10
  254. package/src/extensibility/slash-commands.ts +5 -0
  255. package/src/gjc-runtime/gc-runtime.ts +37 -7
  256. package/src/gjc-runtime/launch-tmux.ts +30 -15
  257. package/src/gjc-runtime/launch-worktree.test.ts +148 -0
  258. package/src/gjc-runtime/launch-worktree.ts +101 -14
  259. package/src/gjc-runtime/managed-owner-admission.ts +46 -2
  260. package/src/gjc-runtime/ralplan-current-final.ts +294 -0
  261. package/src/gjc-runtime/ralplan-runtime.ts +95 -1
  262. package/src/gjc-runtime/session-resolution.ts +38 -6
  263. package/src/gjc-runtime/session-state-lock.ts +59 -14
  264. package/src/gjc-runtime/state-runtime.ts +23 -19
  265. package/src/gjc-runtime/state-writer.ts +109 -5
  266. package/src/gjc-runtime/tmux-sessions.ts +93 -13
  267. package/src/gjc-runtime/ultragoal-change-set.ts +90 -11
  268. package/src/gjc-runtime/ultragoal-guard.ts +9 -1
  269. package/src/gjc-runtime/ultragoal-runtime.ts +15 -5
  270. package/src/harness-control-plane/finalize.ts +11 -0
  271. package/src/harness-control-plane/receipt-spool.ts +105 -28
  272. package/src/harness-control-plane/storage.ts +124 -6
  273. package/src/hindsight/content.ts +116 -16
  274. package/src/hindsight/mental-models.ts +22 -10
  275. package/src/hindsight/state.ts +7 -4
  276. package/src/hooks/native-skill-hook.ts +17 -12
  277. package/src/internal-urls/artifact-protocol.ts +3 -1
  278. package/src/internal-urls/docs-index.generated.ts +23 -22
  279. package/src/internal-urls/local-protocol.ts +167 -8
  280. package/src/internal-urls/types.ts +2 -0
  281. package/src/lsp/client.ts +2 -1
  282. package/src/lsp/clients/biome-client.ts +2 -0
  283. package/src/lsp/clients/swiftlint-client.ts +2 -0
  284. package/src/lsp/index.ts +29 -0
  285. package/src/lsp/workspace-path.ts +23 -0
  286. package/src/main.ts +330 -92
  287. package/src/memories/index.ts +25 -2
  288. package/src/migrate/action-planner.ts +574 -2
  289. package/src/migrate/adapters/claude-code.ts +162 -5
  290. package/src/migrate/adapters/codex.ts +77 -7
  291. package/src/migrate/adapters/index.ts +258 -5
  292. package/src/migrate/adapters/opencode.ts +10 -1
  293. package/src/migrate/executor.ts +222 -7
  294. package/src/migrate/hook-module.ts +193 -0
  295. package/src/migrate/report.ts +63 -2
  296. package/src/migrate/types.ts +84 -4
  297. package/src/modes/DESIGN.md +51 -0
  298. package/src/modes/acp/acp-agent.ts +10 -1
  299. package/src/modes/acp/permission-mode.ts +18 -1
  300. package/src/modes/components/agent-dashboard.ts +20 -4
  301. package/src/modes/components/assistant-message.ts +55 -1
  302. package/src/modes/components/customization/customization-dashboard.ts +7 -0
  303. package/src/modes/components/customization/import-wizard.ts +29 -3
  304. package/src/modes/components/model-selector.ts +157 -103
  305. package/src/modes/components/settings-selector.ts +103 -37
  306. package/src/modes/components/welcome.ts +3 -1
  307. package/src/modes/controllers/command-controller.ts +300 -9
  308. package/src/modes/controllers/event-controller.ts +1 -1
  309. package/src/modes/controllers/extension-ui-controller.ts +31 -1
  310. package/src/modes/controllers/goal-mode-controller.ts +79 -10
  311. package/src/modes/controllers/input-controller.ts +2 -0
  312. package/src/modes/controllers/plan-mode-controller.ts +66 -11
  313. package/src/modes/controllers/runtime-mcp-command-controller.ts +7 -6
  314. package/src/modes/controllers/selector-controller.ts +76 -7
  315. package/src/modes/interactive-mode.ts +123 -4
  316. package/src/modes/prompt-suggestion-controller.ts +1 -1
  317. package/src/modes/runtime-init.ts +5 -1
  318. package/src/modes/theme/theme.ts +15 -0
  319. package/src/modes/utils/host-status.ts +21 -10
  320. package/src/prompts/agent-fragments/browser-aside.md +2 -2
  321. package/src/prompts/system/irc-incoming.md +3 -1
  322. package/src/prompts/tools/debug.md +1 -1
  323. package/src/prompts/tools/irc.md +1 -1
  324. package/src/prompts/tools/read.md +1 -1
  325. package/src/prompts/tools/report-issue.md +13 -0
  326. package/src/prompts/tools/task.md +1 -1
  327. package/src/runtime-credential-selector.ts +2 -2
  328. package/src/runtime-mcp/content-limits.ts +7 -0
  329. package/src/runtime-mcp/manager-pool.test.ts +5 -4
  330. package/src/runtime-mcp/manager.ts +40 -3
  331. package/src/runtime-mcp/oauth-flow.ts +32 -6
  332. package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
  333. package/src/runtime-mcp/project-host-values.ts +12 -8
  334. package/src/runtime-mcp/render.ts +16 -1
  335. package/src/runtime-mcp/tool-bridge.ts +194 -21
  336. package/src/runtime-mcp/transports/stdio.ts +2 -1
  337. package/src/sdk/broker/broker-exit.ts +10 -5
  338. package/src/sdk/broker/broker.ts +484 -85
  339. package/src/sdk/broker/daemon-entry.ts +338 -116
  340. package/src/sdk/broker/doctor-restart.ts +2 -1
  341. package/src/sdk/broker/ensure.ts +1214 -268
  342. package/src/sdk/broker/hop.ts +286 -0
  343. package/src/sdk/broker/lifecycle-ledger.ts +38 -11
  344. package/src/sdk/broker/lifecycle.ts +102 -6
  345. package/src/sdk/broker/lock-host-identity.ts +29 -0
  346. package/src/sdk/broker/session-index.ts +91 -16
  347. package/src/sdk/broker/transport.ts +17 -1
  348. package/src/sdk/bus/chat-command-policy.ts +16 -3
  349. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  350. package/src/sdk/bus/index.ts +460 -35
  351. package/src/sdk/bus/reconciliation-store.ts +201 -8
  352. package/src/sdk/bus/session-control.ts +2 -0
  353. package/src/sdk/bus/slack-daemon.ts +105 -9
  354. package/src/sdk/bus/telegram-daemon-contract.ts +9 -1
  355. package/src/sdk/bus/telegram-daemon.ts +10 -2
  356. package/src/sdk/bus/threaded-render.ts +20 -1
  357. package/src/sdk/cli/session-cli.ts +23 -6
  358. package/src/sdk/host/control/dispatch.ts +13 -1
  359. package/src/sdk/host/control/operations.ts +10 -1
  360. package/src/sdk/host/events.ts +67 -4
  361. package/src/sdk/host/host.ts +8 -2
  362. package/src/sdk/host/prompt-image-upload.ts +438 -0
  363. package/src/sdk/host/sdk-surface-parity.test.ts +15 -0
  364. package/src/sdk/host/session-runtime.test.ts +1737 -129
  365. package/src/sdk/host/session-runtime.ts +684 -83
  366. package/src/sdk/host/surface-policy.ts +4 -0
  367. package/src/sdk/host/websocket-transport.lifecycle.test.ts +90 -2
  368. package/src/sdk/host/websocket-transport.ts +19 -7
  369. package/src/sdk/lifecycle/service.ts +15 -1
  370. package/src/sdk/prompt-deadline-flush.ts +78 -5
  371. package/src/sdk/prompt-deadline-manager.ts +68 -14
  372. package/src/sdk/prompt-status.ts +6 -4
  373. package/src/sdk/protocol/adapter-validation.ts +81 -12
  374. package/src/sdk/protocol/operation-inventory.generated.json +261 -2
  375. package/src/sdk/protocol/operation-registry.ts +33 -3
  376. package/src/sdk/reconciliation-extensions.ts +1 -0
  377. package/src/sdk/session-list.ts +1 -0
  378. package/src/sdk/session.ts +237 -97
  379. package/src/session/agent-session.ts +2726 -479
  380. package/src/session/history-storage.ts +182 -12
  381. package/src/session/internal/managed-session-scope.ts +173 -57
  382. package/src/session/internal/managed-session-storage.ts +85 -12
  383. package/src/session/internal/native-publish-outcome.ts +5 -1
  384. package/src/session/messages.ts +47 -6
  385. package/src/session/session-manager.ts +326 -19
  386. package/src/session/startup-auth-config.ts +116 -8
  387. package/src/session/streaming-output.ts +262 -4
  388. package/src/session/terminal-abort.ts +14 -2
  389. package/src/session-import/redact.ts +53 -2
  390. package/src/session-import/service.ts +1 -0
  391. package/src/skill-state/workflow-mutation-guard.ts +2145 -206
  392. package/src/slash-commands/builtin-registry.ts +21 -6
  393. package/src/slash-commands/helpers/credential-switch.ts +10 -5
  394. package/src/slash-commands/helpers/mcp.ts +11 -3
  395. package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
  396. package/src/ssh/ssh-executor.ts +9 -2
  397. package/src/stt/recorder.ts +26 -9
  398. package/src/system-prompt.ts +61 -16
  399. package/src/task/executor.ts +115 -20
  400. package/src/task/index.ts +389 -175
  401. package/src/task/role-body.ts +84 -0
  402. package/src/task/token-log-mode.test.ts +162 -0
  403. package/src/task/token-log.ts +91 -2
  404. package/src/tools/archive-reader.ts +650 -37
  405. package/src/tools/ask.ts +1 -0
  406. package/src/tools/ast-edit-write-path.ts +64 -0
  407. package/src/tools/ast-edit.ts +18 -1
  408. package/src/tools/ast-grep.ts +7 -2
  409. package/src/tools/atomic-file-write.ts +46 -15
  410. package/src/tools/auto-generated-guard.ts +15 -3
  411. package/src/tools/awaiting-input.ts +114 -0
  412. package/src/tools/bash-allowed-prefixes.ts +214 -6
  413. package/src/tools/bash-interactive.ts +14 -2
  414. package/src/tools/bash-skill-urls.ts +58 -5
  415. package/src/tools/bash.ts +113 -26
  416. package/src/tools/browser/tab-worker.ts +39 -13
  417. package/src/tools/browser.ts +17 -7
  418. package/src/tools/cron.ts +10 -1
  419. package/src/tools/debug.ts +16 -8
  420. package/src/tools/descriptors.test.ts +44 -0
  421. package/src/tools/descriptors.ts +50 -6
  422. package/src/tools/eval.ts +15 -0
  423. package/src/tools/fetch.ts +93 -4
  424. package/src/tools/gh.ts +27 -12
  425. package/src/tools/image-gen.ts +20 -6
  426. package/src/tools/index.ts +2 -0
  427. package/src/tools/irc.ts +5 -3
  428. package/src/tools/monitor.ts +9 -1
  429. package/src/tools/output-meta.ts +74 -5
  430. package/src/tools/python.ts +1 -1
  431. package/src/tools/read.ts +26 -37
  432. package/src/tools/recipe/runner.ts +23 -12
  433. package/src/tools/render-utils.ts +2 -2
  434. package/src/tools/report-issue.ts +184 -0
  435. package/src/tools/search.ts +3 -2
  436. package/src/tools/ssh.ts +274 -8
  437. package/src/tools/subagent-render.ts +114 -12
  438. package/src/tools/subagent.ts +118 -5
  439. package/src/tools/tmux-self-injection-guard.ts +3301 -122
  440. package/src/tools/todo-write.ts +41 -3
  441. package/src/tools/tool-catalog.generated.ts +32 -4
  442. package/src/tools/ultragoal-ask-guard.ts +13 -1
  443. package/src/tools/write.ts +42 -3
  444. package/src/tui/tree-list.ts +15 -1
  445. package/src/utils/commit-message-generator.ts +12 -4
  446. package/src/utils/external-editor.ts +21 -7
  447. package/src/utils/file-mentions.ts +75 -7
  448. package/src/utils/git.ts +38 -7
  449. package/src/utils/image-limits.ts +8 -0
  450. package/src/utils/image-loading.ts +1 -1
  451. package/src/utils/open.ts +73 -4
  452. package/src/utils/pasted-image-loading.ts +9 -8
  453. package/src/utils/prompt-suggestion.ts +5 -4
  454. package/src/utils/shell-snapshot.ts +9 -1
  455. package/src/utils/title-generator.ts +72 -22
  456. package/src/web/scrapers/index.ts +3 -1
  457. package/src/web/scrapers/types.ts +8 -0
  458. package/src/web/scrapers/youtube.ts +3 -2
  459. package/src/web/search/index.ts +2 -1
  460. package/src/web/search/providers/codex.ts +29 -32
  461. package/src/web/search/providers/insane.ts +91 -28
  462. package/src/web/search/providers/searxng.ts +62 -10
@@ -2157,19 +2157,54 @@ const eventWebhookTails = new Map<string, Promise<void>>();
2157
2157
  const eventWebhookStartupTails = new Map<string, Promise<void>>();
2158
2158
  const EVENT_WEBHOOK_ERROR_CAP = 240;
2159
2159
 
2160
+ const EVENT_WEBHOOK_DIAGNOSTIC_CAP = 512;
2161
+
2162
+ /** Printable ASCII only, so a journal id cannot inject another diagnostic line. */
2163
+ function webhookEventDiagnosticToken(eventId: string, maxLength: number): string {
2164
+ if (maxLength <= 0) return "";
2165
+ let token = "";
2166
+ for (let index = 0; index < eventId.length && token.length < maxLength; index += 1) {
2167
+ const code = eventId.charCodeAt(index);
2168
+ if (code >= 0x20 && code <= 0x7e) token += eventId[index];
2169
+ }
2170
+ return token;
2171
+ }
2172
+
2173
+ /** Test-only access for generated normalization and truncation properties. */
2174
+ export function webhookEventDiagnosticTokenForTest(eventId: string, maxLength: number): string {
2175
+ return webhookEventDiagnosticToken(eventId, maxLength);
2176
+ }
2177
+
2160
2178
  async function appendEventWebhookDiagnostic(namespaceDir: string, eventId: string, error: unknown): Promise<void> {
2161
2179
  const code =
2162
2180
  error instanceof Error && /^[a-z0-9_]+$/.test(error.message)
2163
2181
  ? error.message.slice(0, EVENT_WEBHOOK_ERROR_CAP)
2164
2182
  : "event_webhook_delivery_failed";
2165
- const line = `${new Date().toISOString()} event=${eventId} error=${code}\n`;
2183
+ const timestamp = new Date().toISOString();
2184
+ const prefix = `${timestamp} event=`;
2185
+ const suffix = ` error=${code}\n`;
2186
+ const token = webhookEventDiagnosticToken(eventId, EVENT_WEBHOOK_DIAGNOSTIC_CAP - prefix.length - suffix.length);
2187
+ const line = `${prefix}${token}${suffix}`;
2166
2188
  try {
2167
- await fs.appendFile(path.join(namespaceDir, "event-webhook-errors.log"), line.slice(0, 512), { mode: 0o600 });
2189
+ await fs.appendFile(
2190
+ path.join(namespaceDir, "event-webhook-errors.log"),
2191
+ line.slice(0, EVENT_WEBHOOK_DIAGNOSTIC_CAP),
2192
+ { mode: 0o600 },
2193
+ );
2168
2194
  } catch {
2169
2195
  // Diagnostics are best-effort; sink problems must never propagate upward.
2170
2196
  }
2171
2197
  }
2172
2198
 
2199
+ /** Test-only access for diagnostic record boundary coverage. */
2200
+ export async function appendEventWebhookDiagnosticForTest(
2201
+ namespaceDir: string,
2202
+ eventId: string,
2203
+ error: unknown,
2204
+ ): Promise<void> {
2205
+ await appendEventWebhookDiagnostic(namespaceDir, eventId, error);
2206
+ }
2207
+
2173
2208
  function enqueueEventWebhook(namespaceDir: string, event: CoordinatorEvent): void {
2174
2209
  const config = eventWebhookConfigs.get(namespaceDir);
2175
2210
  if (config === null || config === undefined) return;
@@ -49,6 +49,10 @@ const RESIDUAL_PATTERNS: readonly RegExp[] = [
49
49
  /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\./,
50
50
  /\bxox[baprs]-[A-Za-z0-9-]{8,}/,
51
51
  /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/,
52
+ // Same shapes `redactCrashSecrets` rewrites. A survivor means this scanner
53
+ // is not certain, so the submission is refused rather than sent raw.
54
+ /(?<![A-Za-z0-9/+])(?=[A-Za-z0-9/+]{0,39}[/+])[A-Za-z0-9/+]{40}(?![A-Za-z0-9/+])/,
55
+ /(?<![A-Za-z0-9_%])session(?:id|[_-]id)%3[Dd][A-Za-z0-9._~%-]{8,}/i,
52
56
  // Opaque URIs the URL rule cannot parse into an origin, and inline payloads.
53
57
  /\bdata:[^\s]{0,64};base64,/i,
54
58
  /:\/\//,
@@ -95,8 +99,10 @@ export function sanitizeExternalCrashV1(text: string, maxBytes: number = CRASH_F
95
99
  let value = text.replace(/\r\n?/g, "\n");
96
100
  value = value.replace(OSC_PATTERN, " ").replace(CSI_PATTERN, "");
97
101
  value = value.replace(CONTROL_PATTERN, "").replace(INVISIBLE_PATTERN, "");
102
+ // Redact each URL separately so an unquoted query value cannot absorb prose
103
+ // after the URL; then sanitize the remaining crash text normally.
104
+ value = value.replace(URL_PATTERN, match => sanitizeUrl(redactCrashSecrets(match)));
98
105
  value = redactCrashSecrets(value);
99
- value = value.replace(URL_PATTERN, match => sanitizeUrl(match));
100
106
  value = replaceAbsolutePaths(value);
101
107
  value = value.replace(UUID_PATTERN, "<uuid>");
102
108
  value = value.replace(HEX_RUN_PATTERN, match => (/[a-fA-F]/.test(match) ? "<hex>" : "<num>"));
@@ -8,7 +8,8 @@
8
8
  * - Hooks: Claude `.claude/hooks/<pre|post>/<file>` and Codex flat
9
9
  * `.codex/hooks/<phase>-<tool>.ts|js` conventions, normalized through the
10
10
  * canonical hook IR (`normalizeDirectoryHook`, #4286/#4289) and written to
11
- * the runtime-discovered `<root>/hooks/<pre|post>/` layout.
11
+ * the canonical `<root>/hooks/<pre|post>/` layout. Project-scope hooks are
12
+ * stored-only; ordinary sessions do not load repository hook modules.
12
13
  * - MCPs: `normalizeClaudeMcpJson` / `normalizeCodexMcpToml` plus
13
14
  * `validateMCPCompatServer` (#4284 bounded compatibility adapters), merged
14
15
  * through the canonical atomic config writer.
@@ -17,12 +18,16 @@
17
18
  * secret values live only in the separate `ImportPlan`. `applyImport` runs as
18
19
  * a validated transaction: every destination path is containment- and
19
20
  * symlink-checked before any write, destinations are revalidated against the
20
- * preview decisions, files are published with same-directory temp+rename, and
21
- * any failure restores exactly the files this transaction wrote (pre-existing
22
- * symlinks are never touched). Verification failures roll back too.
21
+ * preview decisions, and skill/hook files are published through a no-follow
22
+ * directory descriptor with same-directory temp+rename. Any failure restores
23
+ * exactly the files this transaction wrote (pre-existing symlinks are never
24
+ * touched). Verification failures roll back too.
23
25
  */
26
+ import { dlopen, FFIType, read } from "bun:ffi";
24
27
  import type { Dirent } from "node:fs";
28
+ import * as fsSync from "node:fs";
25
29
  import * as fs from "node:fs/promises";
30
+ import * as os from "node:os";
26
31
  import * as path from "node:path";
27
32
  import { parseFrontmatter, tryParseJson } from "@gajae-code/utils";
28
33
  import { YAML } from "bun";
@@ -351,6 +356,11 @@ export async function buildImportPreview(options: BuildImportPreviewOptions): Pr
351
356
  const entries: ImportPreviewEntry[] = [];
352
357
  const payloads: (NormalizedPayload | undefined)[] = [];
353
358
  const warnings: string[] = [];
359
+ if (surfaces.includes("hooks") && options.destinationScope === "project") {
360
+ warnings.push(
361
+ "Project-scope hooks are stored-only: ordinary sessions do not load repository hook modules. Import hooks into Global .gjc or configure an explicit hook path to activate them.",
362
+ );
363
+ }
354
364
  const productName = productLabel(options.product);
355
365
  const sourceLabel = `${productName} ${sourceScopeLabel(options.sourceScope)}`;
356
366
 
@@ -538,7 +548,9 @@ export async function buildImportPreview(options: BuildImportPreviewOptions): Pr
538
548
  read.kind === "value"
539
549
  ? options.product === "claude-code"
540
550
  ? normalizeClaudeMcpJson(read.content, sourcePath, options.sourceScope)
541
- : normalizeCodexMcpToml(read.content, sourcePath, options.sourceScope)
551
+ : normalizeCodexMcpToml(read.content, sourcePath, options.sourceScope, {
552
+ omitEnvReferences: options.destinationScope === "project",
553
+ })
542
554
  : { items: [], warnings: [] as string[] };
543
555
  for (const warning of normalized.warnings) warnings.push(`${sourceLabel} MCPs: ${warning}`);
544
556
  const unsupportedNestedAuth =
@@ -634,6 +646,9 @@ interface FileSnapshot {
634
646
  path: string;
635
647
  /** Previous content, or null when the file did not exist. */
636
648
  previous: string | null;
649
+ /** Set when publication was bound to this directory descriptor. */
650
+ parentFd?: number;
651
+ name?: string;
637
652
  }
638
653
 
639
654
  interface PlannedFileWrite {
@@ -673,6 +688,13 @@ async function findSymlinkedAncestor(root: string, target: string): Promise<stri
673
688
 
674
689
  /** Remove stale staging temp files from an interrupted earlier apply. */
675
690
  async function sweepStagingTemps(dir: string): Promise<void> {
691
+ try {
692
+ const stat = await fs.lstat(dir);
693
+ // readdir follows a swapped directory symlink; never clean through one.
694
+ if (stat.isSymbolicLink() || !stat.isDirectory()) return;
695
+ } catch {
696
+ return;
697
+ }
676
698
  let names: string[];
677
699
  try {
678
700
  names = await fs.readdir(dir);
@@ -685,6 +707,209 @@ async function sweepStagingTemps(dir: string): Promise<void> {
685
707
  }
686
708
  }
687
709
 
710
+ const { ENOENT, EEXIST, ELOOP, ENOTDIR } = os.constants.errno;
711
+ const CLOEXEC = (fsSync.constants as { O_CLOEXEC?: number }).O_CLOEXEC ?? 0;
712
+ const NOFOLLOW = fsSync.constants.O_NOFOLLOW ?? 0;
713
+ const DIR_FLAGS = fsSync.constants.O_RDONLY | (fsSync.constants.O_DIRECTORY ?? 0) | NOFOLLOW | CLOEXEC;
714
+ const READ_FLAGS = fsSync.constants.O_RDONLY | (fsSync.constants.O_NONBLOCK ?? 0) | NOFOLLOW | CLOEXEC;
715
+ const WRITE_FLAGS = fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL | NOFOLLOW | CLOEXEC;
716
+ const MAX_PINNED_IMPORT_PARENTS = 128;
717
+
718
+ interface PinnedDestination {
719
+ fds: number[];
720
+ parentFd: number;
721
+ name: string;
722
+ }
723
+
724
+ type DirectoryOps = {
725
+ mkdirat: (dirfd: number, name: string, mode: number) => number;
726
+ openat: (dirfd: number, name: string, flags: number, mode: number) => number;
727
+ renameat: (fromfd: number, from: string, tofd: number, to: string) => number;
728
+ unlinkat: (dirfd: number, name: string, flags: number) => number;
729
+ errno: () => number;
730
+ };
731
+
732
+ let directoryOpsCache: DirectoryOps | undefined;
733
+
734
+ /** POSIX only. Windows has no O_NOFOLLOW, so publication keeps the lexical recheck. */
735
+ function pinnedPublicationAvailable(): boolean {
736
+ return (
737
+ process.platform !== "win32" &&
738
+ fsSync.constants.O_NOFOLLOW !== undefined &&
739
+ fsSync.constants.O_DIRECTORY !== undefined
740
+ );
741
+ }
742
+
743
+ function directoryOps(): DirectoryOps {
744
+ if (directoryOpsCache) return directoryOpsCache;
745
+ const darwin = process.platform === "darwin";
746
+ const lib = dlopen(darwin ? "/usr/lib/libSystem.B.dylib" : "libc.so.6", {
747
+ mkdirat: { args: [FFIType.i32, FFIType.cstring, FFIType.u32], returns: FFIType.i32 },
748
+ openat: { args: [FFIType.i32, FFIType.cstring, FFIType.i32, FFIType.u32], returns: FFIType.i32 },
749
+ renameat: { args: [FFIType.i32, FFIType.cstring, FFIType.i32, FFIType.cstring], returns: FFIType.i32 },
750
+ unlinkat: { args: [FFIType.i32, FFIType.cstring, FFIType.i32], returns: FFIType.i32 },
751
+ ...(darwin
752
+ ? { __error: { args: [], returns: FFIType.ptr } }
753
+ : { __errno_location: { args: [], returns: FFIType.ptr } }),
754
+ });
755
+ const symbols = lib.symbols as unknown as DirectoryOps & {
756
+ __error?: () => Parameters<typeof read.i32>[0];
757
+ __errno_location?: () => Parameters<typeof read.i32>[0];
758
+ };
759
+ directoryOpsCache = {
760
+ mkdirat: symbols.mkdirat,
761
+ openat: symbols.openat,
762
+ renameat: symbols.renameat,
763
+ unlinkat: symbols.unlinkat,
764
+ errno: () => read.i32(darwin ? symbols.__error!() : symbols.__errno_location!()),
765
+ };
766
+ return directoryOpsCache;
767
+ }
768
+
769
+ function closeRawFds(fds: number[]): void {
770
+ for (const fd of fds.splice(0).reverse()) fsSync.closeSync(fd);
771
+ }
772
+
773
+ function symlinkRefusal(location: string): Error {
774
+ return new Error(`refusing to write through symlinked ancestor: ${location}`);
775
+ }
776
+
777
+ /** Lexical shape of an import destination. Symlink checks are separate. */
778
+ export function importDestinationShapeAllowed(root: string, target: string): boolean {
779
+ const relative = path.relative(root, path.dirname(target));
780
+ if (relative.startsWith("..") || path.isAbsolute(relative)) return false;
781
+ const segments = relative === "" ? [] : relative.split(path.sep);
782
+ return [...segments, path.basename(target)].every(isSafeName);
783
+ }
784
+
785
+ /** Walk from `root` with openat/mkdirat and retain only the final parent descriptor. */
786
+ function pinImportParent(root: string, target: string): PinnedDestination {
787
+ if (!importDestinationShapeAllowed(root, target)) throw new Error(`refusing unsafe import destination: ${target}`);
788
+ const relative = path.relative(root, path.dirname(target));
789
+ const segments = relative === "" ? [] : relative.split(path.sep);
790
+ const name = path.basename(target);
791
+ const ops = directoryOps();
792
+ const fds: number[] = [];
793
+ try {
794
+ let parentFd: number;
795
+ try {
796
+ parentFd = fsSync.openSync(root, DIR_FLAGS);
797
+ } catch (error) {
798
+ const code = (error as NodeJS.ErrnoException).code;
799
+ if (code === "ELOOP" || code === "ENOTDIR") throw symlinkRefusal(root);
800
+ throw error;
801
+ }
802
+ fds.push(parentFd);
803
+ let cursor = root;
804
+ for (const segment of segments) {
805
+ cursor = path.join(cursor, segment);
806
+ let fd = ops.openat(parentFd, segment, DIR_FLAGS, 0);
807
+ if (fd < 0 && ops.errno() === ENOENT) {
808
+ if (ops.mkdirat(parentFd, segment, 0o700) < 0 && ops.errno() !== EEXIST) {
809
+ throw new Error(`cannot create import directory ${cursor}: errno ${ops.errno()}`);
810
+ }
811
+ fd = ops.openat(parentFd, segment, DIR_FLAGS, 0);
812
+ }
813
+ if (fd < 0) {
814
+ const err = ops.errno();
815
+ if (err === ELOOP || err === ENOTDIR) throw symlinkRefusal(cursor);
816
+ throw new Error(`cannot open import directory ${cursor}: errno ${err}`);
817
+ }
818
+ let isDirectory: boolean;
819
+ try {
820
+ isDirectory = fsSync.fstatSync(fd).isDirectory();
821
+ } catch (error) {
822
+ fsSync.closeSync(fd);
823
+ throw error;
824
+ }
825
+ if (!isDirectory) {
826
+ fsSync.closeSync(fd);
827
+ throw symlinkRefusal(cursor);
828
+ }
829
+ closeRawFds(fds);
830
+ fds.push(fd);
831
+ parentFd = fd;
832
+ }
833
+ const named = fsSync.lstatSync(path.dirname(target));
834
+ const opened = fsSync.fstatSync(parentFd);
835
+ if (named.isSymbolicLink() || !named.isDirectory() || named.dev !== opened.dev || named.ino !== opened.ino) {
836
+ throw symlinkRefusal(path.dirname(target));
837
+ }
838
+ return { fds, parentFd, name };
839
+ } catch (error) {
840
+ closeRawFds(fds);
841
+ throw error;
842
+ }
843
+ }
844
+
845
+ function readRegularAt(parentFd: number, name: string, lexicalPath: string): string | null {
846
+ const ops = directoryOps();
847
+ const fd = ops.openat(parentFd, name, READ_FLAGS, 0);
848
+ if (fd < 0) {
849
+ const err = ops.errno();
850
+ if (err === ENOENT) return null;
851
+ if (err === ELOOP || err === ENOTDIR) throw new Error(`refusing to write through symlink: ${lexicalPath}`);
852
+ throw new Error(`cannot read ${lexicalPath}: errno ${err}`);
853
+ }
854
+ try {
855
+ if (!fsSync.fstatSync(fd).isFile()) throw new Error(`not a regular file: ${lexicalPath}`);
856
+ return fsSync.readFileSync(fd, "utf-8");
857
+ } finally {
858
+ fsSync.closeSync(fd);
859
+ }
860
+ }
861
+
862
+ function writeRegularAt(parentFd: number, name: string, content: string, index: number, lexicalPath: string): void {
863
+ const ops = directoryOps();
864
+ const tmp = `.gjc-import-${process.pid}-${index}-${name}.tmp`;
865
+ const fd = ops.openat(parentFd, tmp, WRITE_FLAGS, 0o600);
866
+ if (fd < 0) throw new Error(`cannot stage import file ${lexicalPath}: errno ${ops.errno()}`);
867
+ try {
868
+ fsSync.writeFileSync(fd, content, "utf-8");
869
+ fsSync.fchmodSync(fd, 0o600);
870
+ } finally {
871
+ fsSync.closeSync(fd);
872
+ }
873
+ if (ops.renameat(parentFd, tmp, parentFd, name) < 0) {
874
+ const err = ops.errno();
875
+ ops.unlinkat(parentFd, tmp, 0);
876
+ throw new Error(`cannot publish import file ${lexicalPath}: errno ${err}`);
877
+ }
878
+ }
879
+
880
+ /** The lexical parent must still be the directory the descriptor published into. */
881
+ function assertPinnedParentStillAt(parentFd: number, lexicalParent: string, lexicalPath: string): void {
882
+ let named: fsSync.Stats;
883
+ try {
884
+ named = fsSync.lstatSync(lexicalParent);
885
+ } catch {
886
+ throw new Error(`post-import verification failed: ${lexicalPath} was not persisted`);
887
+ }
888
+ const opened = fsSync.fstatSync(parentFd);
889
+ if (named.isSymbolicLink() || !named.isDirectory() || named.dev !== opened.dev || named.ino !== opened.ino) {
890
+ throw new Error(`post-import verification failed: ${lexicalPath} was not persisted`);
891
+ }
892
+ try {
893
+ const leaf = fsSync.lstatSync(lexicalPath);
894
+ if (leaf.isSymbolicLink() || !leaf.isFile()) {
895
+ throw new Error(`post-import verification failed: ${lexicalPath} was not persisted`);
896
+ }
897
+ } catch (error) {
898
+ if (error instanceof Error && error.message.startsWith("post-import verification failed:")) throw error;
899
+ throw new Error(`post-import verification failed: ${lexicalPath} was not persisted`);
900
+ }
901
+ }
902
+
903
+ function restoreRegularAt(parentFd: number, name: string, previous: string | null, lexicalPath: string): void {
904
+ if (previous === null) {
905
+ if (directoryOps().unlinkat(parentFd, name, 0) < 0 && directoryOps().errno() !== ENOENT) {
906
+ throw new Error(`cannot remove ${lexicalPath}: errno ${directoryOps().errno()}`);
907
+ }
908
+ return;
909
+ }
910
+ writeRegularAt(parentFd, name, previous, 1_000_000 + process.pid, lexicalPath);
911
+ }
912
+
688
913
  /**
689
914
  * Apply a confirmed plan. The transaction contract:
690
915
  *
@@ -848,12 +1073,44 @@ export async function applyImport(plan: ImportPlan, options: { cwd: string }): P
848
1073
 
849
1074
  // --- Phase 2: publication ------------------------------------------------
850
1075
  const snapshots: FileSnapshot[] = [];
1076
+ const pins: PinnedDestination[] = [];
1077
+ const pinsByParent = new Map<string, PinnedDestination>();
851
1078
  for (const dir of [destination.skillsDir, destination.hooksDir]) {
852
1079
  await sweepStagingTemps(dir).catch(() => {});
853
1080
  }
854
1081
 
855
1082
  try {
1083
+ if (fileWrites.length > 0 && pinnedPublicationAvailable()) {
1084
+ // Scope root only. Children are created with mkdirat on the pinned fd.
1085
+ await fs.mkdir(destination.root, { recursive: true, mode: 0o700 });
1086
+ for (const write of fileWrites) {
1087
+ const parentPath = path.dirname(write.path);
1088
+ if (pinsByParent.has(parentPath)) continue;
1089
+ if (pinsByParent.size >= MAX_PINNED_IMPORT_PARENTS) {
1090
+ throw new Error(
1091
+ `import batch exceeds the limit of ${MAX_PINNED_IMPORT_PARENTS} distinct destination directories`,
1092
+ );
1093
+ }
1094
+ const pin = pinImportParent(destination.root, write.path);
1095
+ pins.push(pin);
1096
+ pinsByParent.set(parentPath, pin);
1097
+ }
1098
+ // Fail before publishing anything if pinned directories leave no
1099
+ // descriptor available for staging or rollback.
1100
+ const descriptorProbe = fsSync.openSync(os.devNull, fsSync.constants.O_RDONLY | CLOEXEC);
1101
+ fsSync.closeSync(descriptorProbe);
1102
+ }
856
1103
  for (const [index, write] of fileWrites.entries()) {
1104
+ if (pinnedPublicationAvailable()) {
1105
+ const pin = pinsByParent.get(path.dirname(write.path));
1106
+ if (!pin) throw new Error(`cannot pin import destination directory: ${path.dirname(write.path)}`);
1107
+ const previous = readRegularAt(pin.parentFd, pin.name, write.path);
1108
+ writeRegularAt(pin.parentFd, pin.name, write.content, index, write.path);
1109
+ snapshots.push({ path: write.path, previous, parentFd: pin.parentFd, name: pin.name });
1110
+ continue;
1111
+ }
1112
+ const symlinked = await findSymlinkedAncestor(destination.root, write.path);
1113
+ if (symlinked) throw new Error(`refusing to write through symlinked ancestor: ${symlinked}`);
857
1114
  await fs.mkdir(path.dirname(write.path), { recursive: true, mode: 0o700 });
858
1115
  const stat = await fs.lstat(write.path).catch(() => null);
859
1116
  if (stat?.isSymbolicLink()) throw new Error(`refusing to write through symlink: ${write.path}`);
@@ -889,8 +1146,18 @@ export async function applyImport(plan: ImportPlan, options: { cwd: string }): P
889
1146
 
890
1147
  // --- Phase 3: verification (inside the transaction) ------------------
891
1148
  for (const write of fileWrites) {
892
- const persisted = await readStructured(write.path);
893
- if (persisted.kind !== "value" || persisted.content !== write.content) {
1149
+ const pinned = snapshots.find(snapshot => snapshot.path === write.path && snapshot.parentFd !== undefined);
1150
+ let persisted: string | null;
1151
+ if (pinned?.parentFd !== undefined && pinned.name) {
1152
+ assertPinnedParentStillAt(pinned.parentFd, path.dirname(write.path), write.path);
1153
+ persisted = readRegularAt(pinned.parentFd, pinned.name, write.path);
1154
+ assertPinnedParentStillAt(pinned.parentFd, path.dirname(write.path), write.path);
1155
+ } else {
1156
+ persisted = await readStructured(write.path).then(result =>
1157
+ result.kind === "value" ? result.content : null,
1158
+ );
1159
+ }
1160
+ if (persisted !== write.content) {
894
1161
  throw new Error(`post-import verification failed: ${write.path} was not persisted`);
895
1162
  }
896
1163
  }
@@ -908,7 +1175,9 @@ export async function applyImport(plan: ImportPlan, options: { cwd: string }): P
908
1175
  const rollbackErrors: string[] = [];
909
1176
  for (const snapshot of snapshots.reverse()) {
910
1177
  try {
911
- if (snapshot.previous === null) {
1178
+ if (snapshot.parentFd !== undefined && snapshot.name) {
1179
+ restoreRegularAt(snapshot.parentFd, snapshot.name, snapshot.previous, snapshot.path);
1180
+ } else if (snapshot.previous === null) {
912
1181
  await fs.rm(snapshot.path, { force: true });
913
1182
  } else {
914
1183
  const staged = `${snapshot.path}.gjc-rollback-${process.pid}.tmp`;
@@ -926,6 +1195,8 @@ export async function applyImport(plan: ImportPlan, options: { cwd: string }): P
926
1195
  `import failed and was rolled back: ${reason}` +
927
1196
  (rollbackErrors.length > 0 ? `; ROLLBACK ERRORS (manual repair needed): ${rollbackErrors.join("; ")}` : ""),
928
1197
  );
1198
+ } finally {
1199
+ for (const pin of pins) closeRawFds(pin.fds);
929
1200
  }
930
1201
 
931
1202
  for (const { entry } of writable) {
@@ -6,9 +6,10 @@
6
6
  * discovery/policy model (trust, include/ignore, disabledExtensions,
7
7
  * bundled-name protection). A narrow supplemental scan flags SKILL.md files
8
8
  * the management loader rejects as `invalid` rows.
9
- * - Hooks: canonical `<root>/hooks/<pre|post>/` layout with the runtime's
10
- * project-first, first-wins dedupe key (`<type>:<tool>:<name>`); losers are
11
- * shown as `shadowed`.
9
+ * - Hooks: canonical `<root>/hooks/<pre|post>/` layout. Project hooks are
10
+ * stored-only and do not shadow user hooks because sessions never load
11
+ * repository hook modules. User-scope collisions follow the runtime's
12
+ * first-wins dedupe key (`<type>:<tool>:<name>`); losers are `shadowed`.
12
13
  * - MCPs: canonical config files for both scopes with the runtime's
13
14
  * project-first precedence and the union of disabled markers; malformed
14
15
  * files surface as `invalid` rows without leaking raw JSON.
@@ -173,7 +174,8 @@ const HOOK_PHASES = ["pre", "post"] as const;
173
174
 
174
175
  async function loadHookRows(options: LoadCustomizationInventoryOptions, _warnings: string[]): Promise<InventoryRow[]> {
175
176
  const rows: InventoryRow[] = [];
176
- // Runtime semantics: project-first, first-wins on `<type>:<tool>:<name>`.
177
+ // Repository hooks are deliberately excluded by session startup. They stay
178
+ // visible for management, but are stored-only and cannot shadow user hooks.
177
179
  const seen = new Set<string>();
178
180
  for (const scope of ["project", "global"] as const) {
179
181
  const hooksDir = resolveScopePaths(scope, options.cwd).hooksDir;
@@ -190,19 +192,33 @@ async function loadHookRows(options: LoadCustomizationInventoryOptions, _warning
190
192
  const hookPath = path.join(phaseDir, entry.name);
191
193
  const baseName = entry.name.includes(".") ? entry.name.slice(0, entry.name.lastIndexOf(".")) : entry.name;
192
194
  const key = `${phase}:${baseName}:${entry.name}`;
193
- const shadowed = seen.has(key);
194
- seen.add(key);
195
+ const storedOnly = scope === "project";
196
+ const shadowed = !storedOnly && seen.has(key);
197
+ if (!storedOnly) seen.add(key);
198
+ let status: InventoryStatus;
199
+ let diagnostics: string[] | undefined;
200
+ if (storedOnly) {
201
+ status = "stored-only";
202
+ diagnostics = [
203
+ "project hooks are stored only; sessions do not load repository hook modules automatically",
204
+ ];
205
+ } else if (shadowed) {
206
+ status = "shadowed";
207
+ diagnostics = ["shadowed by the same hook at higher-precedence scope"];
208
+ } else {
209
+ status = "enabled";
210
+ }
195
211
  rows.push({
196
212
  id: inventoryRowId("hooks", scope, hookPath),
197
213
  surface: "hooks",
198
214
  name: entry.name,
199
215
  displayName: `${phase}/${entry.name}`,
200
- status: shadowed ? "shadowed" : "enabled",
216
+ status,
201
217
  provenance: `${scopeLabel(scope)} hooks/${phase}`,
202
218
  path: hookPath,
203
219
  scope,
204
220
  description: `${phase}-tool hook for ${baseName}`,
205
- diagnostics: shadowed ? ["shadowed by the same hook at higher-precedence scope"] : undefined,
221
+ diagnostics,
206
222
  raw: { name: entry.name, type: phase, tool: baseName },
207
223
  });
208
224
  }
@@ -119,6 +119,7 @@ export function sourceConfigDir(
119
119
 
120
120
  export type InventoryStatus =
121
121
  | "enabled"
122
+ | "stored-only"
122
123
  | "disabled"
123
124
  | "invalid"
124
125
  | "shadowed"
package/src/dap/client.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import * as fs from "node:fs/promises";
3
3
  import { logger } from "@gajae-code/utils";
4
+ import { projectSafeSpawnEnv } from "@gajae-code/utils/shell-config";
4
5
  import { formatCrashDiagnosticNotice, writeCrashReport } from "../debug/crash-diagnostics";
5
6
  import { NON_INTERACTIVE_ENV } from "../exec/non-interactive-env";
6
7
  import { type OwnedProcess, spawnOwnedProcess } from "../runtime/process-lifecycle";
@@ -134,7 +135,7 @@ export class DapClient {
134
135
  // debuggee children can reach /dev/tty and trigger SIGTTIN, suspending
135
136
  // the parent harness under shell job control.
136
137
  const env = {
137
- ...Bun.env,
138
+ ...projectSafeSpawnEnv(),
138
139
  ...NON_INTERACTIVE_ENV,
139
140
  };
140
141
  const owner = spawnOwnedProcess([adapter.resolvedCommand, ...adapter.args], {
@@ -159,7 +160,7 @@ export class DapClient {
159
160
  */
160
161
  static async #spawnSocket({ adapter, cwd }: DapSpawnOptions): Promise<DapClient> {
161
162
  const env = {
162
- ...Bun.env,
163
+ ...projectSafeSpawnEnv(),
163
164
  ...NON_INTERACTIVE_ENV,
164
165
  };
165
166
  const isLinux = process.platform === "linux";
package/src/dap/config.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as path from "node:path";
2
2
  import { isRecord } from "@gajae-code/utils";
3
3
  import { hasRootMarkers, resolveTrustedCommand } from "../lsp/config";
4
+ import { isProjectControlledPath } from "../lsp/path-trust";
4
5
  import DEFAULTS from "./defaults.json" with { type: "json" };
5
6
  import type { DapAdapterConfig, DapResolvedAdapter } from "./types";
6
7
 
@@ -48,11 +49,20 @@ export function getAdapterConfigs(): Record<string, DapAdapterConfig> {
48
49
  return { ...DEFAULT_ADAPTERS };
49
50
  }
50
51
 
51
- export function resolveAdapter(adapterName: string, cwd: string): DapResolvedAdapter | null {
52
+ /**
53
+ * `trustCwds` are extra trust roots, such as the session workspace when the command
54
+ * cwd is a different directory. A command controlled by any of them is refused.
55
+ */
56
+ export function resolveAdapter(
57
+ adapterName: string,
58
+ cwd: string,
59
+ trustCwds?: readonly string[],
60
+ ): DapResolvedAdapter | null {
52
61
  const config = DEFAULT_ADAPTERS[adapterName];
53
62
  if (!config) return null;
54
63
  const resolvedCommand = resolveTrustedCommand(config.command, cwd);
55
64
  if (!resolvedCommand) return null;
65
+ if (trustCwds?.some(root => isProjectControlledPath(resolvedCommand, root))) return null;
56
66
  return {
57
67
  name: adapterName,
58
68
  command: config.command,
@@ -67,15 +77,15 @@ export function resolveAdapter(adapterName: string, cwd: string): DapResolvedAda
67
77
  };
68
78
  }
69
79
 
70
- export function getAvailableAdapters(cwd: string): DapResolvedAdapter[] {
80
+ export function getAvailableAdapters(cwd: string, trustCwds?: readonly string[]): DapResolvedAdapter[] {
71
81
  return Object.keys(DEFAULT_ADAPTERS)
72
- .map(name => resolveAdapter(name, cwd))
82
+ .map(name => resolveAdapter(name, cwd, trustCwds))
73
83
  .filter((adapter): adapter is DapResolvedAdapter => adapter !== null);
74
84
  }
75
85
 
76
- function getMatchingAdapters(program: string, cwd: string): DapResolvedAdapter[] {
86
+ function getMatchingAdapters(program: string, cwd: string, trustCwds?: readonly string[]): DapResolvedAdapter[] {
77
87
  const extension = path.extname(program).toLowerCase();
78
- const available = getAvailableAdapters(cwd);
88
+ const available = getAvailableAdapters(cwd, trustCwds);
79
89
  if (!extension) {
80
90
  // For extensionless binaries, only consider native debuggers (gdb, lldb-dap)
81
91
  // or adapters that match by root markers. Don't silently fall back to
@@ -124,20 +134,30 @@ function sortAdaptersForLaunch(program: string, cwd: string, adapters: DapResolv
124
134
  return rootAware.map(entry => entry.adapter);
125
135
  }
126
136
 
127
- export function selectLaunchAdapter(program: string, cwd: string, adapterName?: string): DapResolvedAdapter | null {
137
+ export function selectLaunchAdapter(
138
+ program: string,
139
+ cwd: string,
140
+ adapterName?: string,
141
+ trustCwds?: readonly string[],
142
+ ): DapResolvedAdapter | null {
128
143
  if (adapterName) {
129
- return resolveAdapter(adapterName, cwd);
144
+ return resolveAdapter(adapterName, cwd, trustCwds);
130
145
  }
131
- const matches = getMatchingAdapters(program, cwd);
146
+ const matches = getMatchingAdapters(program, cwd, trustCwds);
132
147
  const sorted = sortAdaptersForLaunch(program, cwd, matches);
133
148
  return sorted[0] ?? null;
134
149
  }
135
150
 
136
- export function selectAttachAdapter(cwd: string, adapterName?: string, port?: number): DapResolvedAdapter | null {
151
+ export function selectAttachAdapter(
152
+ cwd: string,
153
+ adapterName?: string,
154
+ port?: number,
155
+ trustCwds?: readonly string[],
156
+ ): DapResolvedAdapter | null {
137
157
  if (adapterName) {
138
- return resolveAdapter(adapterName, cwd);
158
+ return resolveAdapter(adapterName, cwd, trustCwds);
139
159
  }
140
- const available = getAvailableAdapters(cwd);
160
+ const available = getAvailableAdapters(cwd, trustCwds);
141
161
  if (port !== undefined) {
142
162
  const debugpy = available.find(adapter => adapter.name === "debugpy");
143
163
  if (debugpy) return debugpy;
@@ -44,7 +44,7 @@
44
44
  },
45
45
  "debugpy": {
46
46
  "command": "python",
47
- "args": ["-m", "debugpy.adapter"],
47
+ "args": ["-I", "-m", "debugpy.adapter"],
48
48
  "languages": ["python"],
49
49
  "fileTypes": [".py"],
50
50
  "rootMarkers": ["pyproject.toml", "setup.py", "requirements.txt", "Pipfile"],