apex-code 0.0.1-alpha.9 → 0.0.4

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 (513) hide show
  1. package/CHANGELOG.md +150 -2
  2. package/README.md +9 -8
  3. package/dist/cli/args.d.ts +25 -1
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +43 -1
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/cli/mcp-command.d.ts +9 -0
  8. package/dist/cli/mcp-command.d.ts.map +1 -0
  9. package/dist/cli/mcp-command.js +53 -0
  10. package/dist/cli/mcp-command.js.map +1 -0
  11. package/dist/cli/project-trust.d.ts.map +1 -1
  12. package/dist/cli/project-trust.js +3 -1
  13. package/dist/cli/project-trust.js.map +1 -1
  14. package/dist/cli/startup-ui.d.ts.map +1 -1
  15. package/dist/cli/startup-ui.js +2 -1
  16. package/dist/cli/startup-ui.js.map +1 -1
  17. package/dist/cli.d.ts.map +1 -1
  18. package/dist/cli.js +94 -5
  19. package/dist/cli.js.map +1 -1
  20. package/dist/config.d.ts.map +1 -1
  21. package/dist/config.js +24 -5
  22. package/dist/config.js.map +1 -1
  23. package/dist/core/agent-session-services.d.ts +3 -0
  24. package/dist/core/agent-session-services.d.ts.map +1 -1
  25. package/dist/core/agent-session-services.js +1 -0
  26. package/dist/core/agent-session-services.js.map +1 -1
  27. package/dist/core/agent-session.d.ts +157 -8
  28. package/dist/core/agent-session.d.ts.map +1 -1
  29. package/dist/core/agent-session.js +565 -22
  30. package/dist/core/agent-session.js.map +1 -1
  31. package/dist/core/bash-executor.d.ts.map +1 -1
  32. package/dist/core/bash-executor.js +7 -0
  33. package/dist/core/bash-executor.js.map +1 -1
  34. package/dist/core/checkpoints/git-checkpoints.d.ts +39 -0
  35. package/dist/core/checkpoints/git-checkpoints.d.ts.map +1 -0
  36. package/dist/core/checkpoints/git-checkpoints.js +251 -0
  37. package/dist/core/checkpoints/git-checkpoints.js.map +1 -0
  38. package/dist/core/checkpoints/session-checkpoints.d.ts +23 -0
  39. package/dist/core/checkpoints/session-checkpoints.d.ts.map +1 -0
  40. package/dist/core/checkpoints/session-checkpoints.js +36 -0
  41. package/dist/core/checkpoints/session-checkpoints.js.map +1 -0
  42. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  43. package/dist/core/compaction/branch-summarization.js +4 -3
  44. package/dist/core/compaction/branch-summarization.js.map +1 -1
  45. package/dist/core/compaction/compaction.d.ts +5 -0
  46. package/dist/core/compaction/compaction.d.ts.map +1 -1
  47. package/dist/core/compaction/compaction.js +19 -5
  48. package/dist/core/compaction/compaction.js.map +1 -1
  49. package/dist/core/delegation/runtime.d.ts +6 -5
  50. package/dist/core/delegation/runtime.d.ts.map +1 -1
  51. package/dist/core/delegation/runtime.js +4 -3
  52. package/dist/core/delegation/runtime.js.map +1 -1
  53. package/dist/core/exec.d.ts.map +1 -1
  54. package/dist/core/exec.js +5 -2
  55. package/dist/core/exec.js.map +1 -1
  56. package/dist/core/extensions/index.d.ts +1 -1
  57. package/dist/core/extensions/index.d.ts.map +1 -1
  58. package/dist/core/extensions/index.js.map +1 -1
  59. package/dist/core/extensions/loader.d.ts.map +1 -1
  60. package/dist/core/extensions/loader.js +8 -2
  61. package/dist/core/extensions/loader.js.map +1 -1
  62. package/dist/core/extensions/runner.d.ts +5 -0
  63. package/dist/core/extensions/runner.d.ts.map +1 -1
  64. package/dist/core/extensions/runner.js +45 -1
  65. package/dist/core/extensions/runner.js.map +1 -1
  66. package/dist/core/extensions/source-runtime.d.ts +14 -0
  67. package/dist/core/extensions/source-runtime.d.ts.map +1 -0
  68. package/dist/core/extensions/source-runtime.js +56 -0
  69. package/dist/core/extensions/source-runtime.js.map +1 -0
  70. package/dist/core/extensions/types.d.ts +22 -2
  71. package/dist/core/extensions/types.d.ts.map +1 -1
  72. package/dist/core/extensions/types.js.map +1 -1
  73. package/dist/core/formatter-lifecycle.d.ts +69 -0
  74. package/dist/core/formatter-lifecycle.d.ts.map +1 -0
  75. package/dist/core/formatter-lifecycle.js +360 -0
  76. package/dist/core/formatter-lifecycle.js.map +1 -0
  77. package/dist/core/hooks/command-handler.d.ts +19 -0
  78. package/dist/core/hooks/command-handler.d.ts.map +1 -0
  79. package/dist/core/hooks/command-handler.js +112 -0
  80. package/dist/core/hooks/command-handler.js.map +1 -0
  81. package/dist/core/hooks/http-handler.d.ts +10 -0
  82. package/dist/core/hooks/http-handler.d.ts.map +1 -0
  83. package/dist/core/hooks/http-handler.js +36 -0
  84. package/dist/core/hooks/http-handler.js.map +1 -0
  85. package/dist/core/hooks/loader.d.ts +29 -0
  86. package/dist/core/hooks/loader.d.ts.map +1 -0
  87. package/dist/core/hooks/loader.js +139 -0
  88. package/dist/core/hooks/loader.js.map +1 -0
  89. package/dist/core/hooks/runtime.d.ts +13 -0
  90. package/dist/core/hooks/runtime.d.ts.map +1 -0
  91. package/dist/core/hooks/runtime.js +63 -0
  92. package/dist/core/hooks/runtime.js.map +1 -0
  93. package/dist/core/hooks/types.d.ts +83 -0
  94. package/dist/core/hooks/types.d.ts.map +1 -0
  95. package/dist/core/hooks/types.js +24 -0
  96. package/dist/core/hooks/types.js.map +1 -0
  97. package/dist/core/mcp/config.d.ts +16 -0
  98. package/dist/core/mcp/config.d.ts.map +1 -0
  99. package/dist/core/mcp/config.js +166 -0
  100. package/dist/core/mcp/config.js.map +1 -0
  101. package/dist/core/mcp/connector.d.ts +35 -0
  102. package/dist/core/mcp/connector.d.ts.map +1 -0
  103. package/dist/core/mcp/connector.js +105 -0
  104. package/dist/core/mcp/connector.js.map +1 -0
  105. package/dist/core/mcp/contract.d.ts +19 -0
  106. package/dist/core/mcp/contract.d.ts.map +1 -0
  107. package/dist/core/mcp/contract.js +89 -0
  108. package/dist/core/mcp/contract.js.map +1 -0
  109. package/dist/core/mcp/mcp-tool.d.ts +32 -0
  110. package/dist/core/mcp/mcp-tool.d.ts.map +1 -0
  111. package/dist/core/mcp/mcp-tool.js +171 -0
  112. package/dist/core/mcp/mcp-tool.js.map +1 -0
  113. package/dist/core/mcp/metadata-cache.d.ts +29 -0
  114. package/dist/core/mcp/metadata-cache.d.ts.map +1 -0
  115. package/dist/core/mcp/metadata-cache.js +99 -0
  116. package/dist/core/mcp/metadata-cache.js.map +1 -0
  117. package/dist/core/mcp/oauth/authorize.d.ts +21 -0
  118. package/dist/core/mcp/oauth/authorize.d.ts.map +1 -0
  119. package/dist/core/mcp/oauth/authorize.js +32 -0
  120. package/dist/core/mcp/oauth/authorize.js.map +1 -0
  121. package/dist/core/mcp/oauth/discover.d.ts +27 -0
  122. package/dist/core/mcp/oauth/discover.d.ts.map +1 -0
  123. package/dist/core/mcp/oauth/discover.js +116 -0
  124. package/dist/core/mcp/oauth/discover.js.map +1 -0
  125. package/dist/core/mcp/oauth/flow.d.ts +31 -0
  126. package/dist/core/mcp/oauth/flow.d.ts.map +1 -0
  127. package/dist/core/mcp/oauth/flow.js +175 -0
  128. package/dist/core/mcp/oauth/flow.js.map +1 -0
  129. package/dist/core/mcp/oauth/mcp-token.d.ts +51 -0
  130. package/dist/core/mcp/oauth/mcp-token.d.ts.map +1 -0
  131. package/dist/core/mcp/oauth/mcp-token.js +129 -0
  132. package/dist/core/mcp/oauth/mcp-token.js.map +1 -0
  133. package/dist/core/mcp/oauth/pkce.d.ts +12 -0
  134. package/dist/core/mcp/oauth/pkce.d.ts.map +1 -0
  135. package/dist/core/mcp/oauth/pkce.js +11 -0
  136. package/dist/core/mcp/oauth/pkce.js.map +1 -0
  137. package/dist/core/mcp/oauth/register.d.ts +16 -0
  138. package/dist/core/mcp/oauth/register.d.ts.map +1 -0
  139. package/dist/core/mcp/oauth/register.js +42 -0
  140. package/dist/core/mcp/oauth/register.js.map +1 -0
  141. package/dist/core/mcp/oauth/token-client.d.ts +33 -0
  142. package/dist/core/mcp/oauth/token-client.d.ts.map +1 -0
  143. package/dist/core/mcp/oauth/token-client.js +65 -0
  144. package/dist/core/mcp/oauth/token-client.js.map +1 -0
  145. package/dist/core/mcp/runtime.d.ts +20 -0
  146. package/dist/core/mcp/runtime.d.ts.map +1 -0
  147. package/dist/core/mcp/runtime.js +37 -0
  148. package/dist/core/mcp/runtime.js.map +1 -0
  149. package/dist/core/mcp/schema.d.ts +22 -0
  150. package/dist/core/mcp/schema.d.ts.map +1 -0
  151. package/dist/core/mcp/schema.js +20 -0
  152. package/dist/core/mcp/schema.js.map +1 -0
  153. package/dist/core/mcp/server-manager.d.ts +43 -0
  154. package/dist/core/mcp/server-manager.d.ts.map +1 -0
  155. package/dist/core/mcp/server-manager.js +142 -0
  156. package/dist/core/mcp/server-manager.js.map +1 -0
  157. package/dist/core/mcp/types.d.ts +100 -0
  158. package/dist/core/mcp/types.d.ts.map +1 -0
  159. package/dist/core/mcp/types.js +8 -0
  160. package/dist/core/mcp/types.js.map +1 -0
  161. package/dist/core/permissions/gate.d.ts.map +1 -1
  162. package/dist/core/permissions/gate.js +45 -4
  163. package/dist/core/permissions/gate.js.map +1 -1
  164. package/dist/core/permissions/operations.d.ts +20 -0
  165. package/dist/core/permissions/operations.d.ts.map +1 -0
  166. package/dist/core/permissions/operations.js +9 -0
  167. package/dist/core/permissions/operations.js.map +1 -0
  168. package/dist/core/permissions/policy-command.d.ts +42 -0
  169. package/dist/core/permissions/policy-command.d.ts.map +1 -0
  170. package/dist/core/permissions/policy-command.js +44 -0
  171. package/dist/core/permissions/policy-command.js.map +1 -0
  172. package/dist/core/permissions/responder.d.ts +49 -2
  173. package/dist/core/permissions/responder.d.ts.map +1 -1
  174. package/dist/core/permissions/responder.js +20 -8
  175. package/dist/core/permissions/responder.js.map +1 -1
  176. package/dist/core/permissions/rules.d.ts.map +1 -1
  177. package/dist/core/permissions/rules.js +11 -1
  178. package/dist/core/permissions/rules.js.map +1 -1
  179. package/dist/core/permissions/store.d.ts +18 -0
  180. package/dist/core/permissions/store.d.ts.map +1 -1
  181. package/dist/core/permissions/store.js +89 -8
  182. package/dist/core/permissions/store.js.map +1 -1
  183. package/dist/core/policy-executor.d.ts +47 -0
  184. package/dist/core/policy-executor.d.ts.map +1 -0
  185. package/dist/core/policy-executor.js +228 -0
  186. package/dist/core/policy-executor.js.map +1 -0
  187. package/dist/core/policy-loader.d.ts +72 -0
  188. package/dist/core/policy-loader.d.ts.map +1 -0
  189. package/dist/core/policy-loader.js +195 -0
  190. package/dist/core/policy-loader.js.map +1 -0
  191. package/dist/core/project-trust.d.ts +1 -1
  192. package/dist/core/project-trust.d.ts.map +1 -1
  193. package/dist/core/project-trust.js.map +1 -1
  194. package/dist/core/sandbox/bwrap-arguments.d.ts +41 -0
  195. package/dist/core/sandbox/bwrap-arguments.d.ts.map +1 -0
  196. package/dist/core/sandbox/bwrap-arguments.js +136 -0
  197. package/dist/core/sandbox/bwrap-arguments.js.map +1 -0
  198. package/dist/core/sandbox/child-entry.d.ts.map +1 -1
  199. package/dist/core/sandbox/child-entry.js +3 -1
  200. package/dist/core/sandbox/child-entry.js.map +1 -1
  201. package/dist/core/sandbox/cli-launch.d.ts +84 -4
  202. package/dist/core/sandbox/cli-launch.d.ts.map +1 -1
  203. package/dist/core/sandbox/cli-launch.js +115 -16
  204. package/dist/core/sandbox/cli-launch.js.map +1 -1
  205. package/dist/core/sandbox/cli-supervisor.d.ts +2 -0
  206. package/dist/core/sandbox/cli-supervisor.d.ts.map +1 -1
  207. package/dist/core/sandbox/cli-supervisor.js +50 -2
  208. package/dist/core/sandbox/cli-supervisor.js.map +1 -1
  209. package/dist/core/sandbox/full-access.d.ts +28 -0
  210. package/dist/core/sandbox/full-access.d.ts.map +1 -0
  211. package/dist/core/sandbox/full-access.js +63 -0
  212. package/dist/core/sandbox/full-access.js.map +1 -0
  213. package/dist/core/sandbox/git-identity.d.ts +57 -0
  214. package/dist/core/sandbox/git-identity.d.ts.map +1 -0
  215. package/dist/core/sandbox/git-identity.js +78 -0
  216. package/dist/core/sandbox/git-identity.js.map +1 -0
  217. package/dist/core/sandbox/host-approval.d.ts +42 -0
  218. package/dist/core/sandbox/host-approval.d.ts.map +1 -0
  219. package/dist/core/sandbox/host-approval.js +99 -0
  220. package/dist/core/sandbox/host-approval.js.map +1 -0
  221. package/dist/core/sandbox/linux-backend.d.ts +14 -0
  222. package/dist/core/sandbox/linux-backend.d.ts.map +1 -1
  223. package/dist/core/sandbox/linux-backend.js +158 -80
  224. package/dist/core/sandbox/linux-backend.js.map +1 -1
  225. package/dist/core/sandbox/macos-backend.d.ts +5 -0
  226. package/dist/core/sandbox/macos-backend.d.ts.map +1 -1
  227. package/dist/core/sandbox/macos-backend.js +164 -5
  228. package/dist/core/sandbox/macos-backend.js.map +1 -1
  229. package/dist/core/sandbox/network-proxy.d.ts +19 -0
  230. package/dist/core/sandbox/network-proxy.d.ts.map +1 -1
  231. package/dist/core/sandbox/network-proxy.js +93 -19
  232. package/dist/core/sandbox/network-proxy.js.map +1 -1
  233. package/dist/core/sandbox/policy.d.ts +8 -0
  234. package/dist/core/sandbox/policy.d.ts.map +1 -1
  235. package/dist/core/sandbox/policy.js +29 -12
  236. package/dist/core/sandbox/policy.js.map +1 -1
  237. package/dist/core/sandbox/profiles.d.ts +25 -0
  238. package/dist/core/sandbox/profiles.d.ts.map +1 -0
  239. package/dist/core/sandbox/profiles.js +23 -0
  240. package/dist/core/sandbox/profiles.js.map +1 -0
  241. package/dist/core/sandbox/rpc/command-client.d.ts +28 -0
  242. package/dist/core/sandbox/rpc/command-client.d.ts.map +1 -0
  243. package/dist/core/sandbox/rpc/command-client.js +86 -0
  244. package/dist/core/sandbox/rpc/command-client.js.map +1 -0
  245. package/dist/core/sandbox/rpc/command-proxy.d.ts +48 -0
  246. package/dist/core/sandbox/rpc/command-proxy.d.ts.map +1 -0
  247. package/dist/core/sandbox/rpc/command-proxy.js +120 -0
  248. package/dist/core/sandbox/rpc/command-proxy.js.map +1 -0
  249. package/dist/core/sandbox/rpc/credential-proxy.d.ts.map +1 -1
  250. package/dist/core/sandbox/rpc/credential-proxy.js +2 -96
  251. package/dist/core/sandbox/rpc/credential-proxy.js.map +1 -1
  252. package/dist/core/sandbox/rpc/framing.d.ts +35 -0
  253. package/dist/core/sandbox/rpc/framing.d.ts.map +1 -0
  254. package/dist/core/sandbox/rpc/framing.js +114 -0
  255. package/dist/core/sandbox/rpc/framing.js.map +1 -0
  256. package/dist/core/sandbox/rpc/git-credential-helper.d.ts +47 -0
  257. package/dist/core/sandbox/rpc/git-credential-helper.d.ts.map +1 -0
  258. package/dist/core/sandbox/rpc/git-credential-helper.js +222 -0
  259. package/dist/core/sandbox/rpc/git-credential-helper.js.map +1 -0
  260. package/dist/core/sandbox/rpc/git-credential-proxy.d.ts +66 -0
  261. package/dist/core/sandbox/rpc/git-credential-proxy.d.ts.map +1 -0
  262. package/dist/core/sandbox/rpc/git-credential-proxy.js +209 -0
  263. package/dist/core/sandbox/rpc/git-credential-proxy.js.map +1 -0
  264. package/dist/core/sandbox/supervisor-temp.d.ts +18 -0
  265. package/dist/core/sandbox/supervisor-temp.d.ts.map +1 -0
  266. package/dist/core/sandbox/supervisor-temp.js +21 -0
  267. package/dist/core/sandbox/supervisor-temp.js.map +1 -0
  268. package/dist/core/sandbox/supervisor.d.ts +12 -1
  269. package/dist/core/sandbox/supervisor.d.ts.map +1 -1
  270. package/dist/core/sandbox/supervisor.js +15 -1
  271. package/dist/core/sandbox/supervisor.js.map +1 -1
  272. package/dist/core/sandbox/terminal-handoff.d.ts +68 -0
  273. package/dist/core/sandbox/terminal-handoff.d.ts.map +1 -0
  274. package/dist/core/sandbox/terminal-handoff.js +221 -0
  275. package/dist/core/sandbox/terminal-handoff.js.map +1 -0
  276. package/dist/core/sandbox/terminal-size.d.ts +13 -0
  277. package/dist/core/sandbox/terminal-size.d.ts.map +1 -1
  278. package/dist/core/sandbox/terminal-size.js +23 -2
  279. package/dist/core/sandbox/terminal-size.js.map +1 -1
  280. package/dist/core/sdk.d.ts +22 -0
  281. package/dist/core/sdk.d.ts.map +1 -1
  282. package/dist/core/sdk.js +53 -1
  283. package/dist/core/sdk.js.map +1 -1
  284. package/dist/core/session-manager.d.ts +13 -1
  285. package/dist/core/session-manager.d.ts.map +1 -1
  286. package/dist/core/session-manager.js +32 -5
  287. package/dist/core/session-manager.js.map +1 -1
  288. package/dist/core/settings-manager.d.ts +163 -1
  289. package/dist/core/settings-manager.d.ts.map +1 -1
  290. package/dist/core/settings-manager.js +107 -0
  291. package/dist/core/settings-manager.js.map +1 -1
  292. package/dist/core/slash-commands.d.ts.map +1 -1
  293. package/dist/core/slash-commands.js +2 -0
  294. package/dist/core/slash-commands.js.map +1 -1
  295. package/dist/core/tools/background-shell.d.ts +39 -0
  296. package/dist/core/tools/background-shell.d.ts.map +1 -0
  297. package/dist/core/tools/background-shell.js +81 -0
  298. package/dist/core/tools/background-shell.js.map +1 -0
  299. package/dist/core/tools/bash-command-segments.d.ts.map +1 -1
  300. package/dist/core/tools/bash-command-segments.js +2 -0
  301. package/dist/core/tools/bash-command-segments.js.map +1 -1
  302. package/dist/core/tools/bash.d.ts +30 -15
  303. package/dist/core/tools/bash.d.ts.map +1 -1
  304. package/dist/core/tools/bash.js +228 -12
  305. package/dist/core/tools/bash.js.map +1 -1
  306. package/dist/core/tools/contract-snapshot.d.ts +58 -0
  307. package/dist/core/tools/contract-snapshot.d.ts.map +1 -0
  308. package/dist/core/tools/contract-snapshot.js +51 -0
  309. package/dist/core/tools/contract-snapshot.js.map +1 -0
  310. package/dist/core/tools/contract.d.ts +53 -5
  311. package/dist/core/tools/contract.d.ts.map +1 -1
  312. package/dist/core/tools/contract.js +22 -2
  313. package/dist/core/tools/contract.js.map +1 -1
  314. package/dist/core/tools/delegate.d.ts +6 -4
  315. package/dist/core/tools/delegate.d.ts.map +1 -1
  316. package/dist/core/tools/delegate.js +21 -11
  317. package/dist/core/tools/delegate.js.map +1 -1
  318. package/dist/core/tools/edit-diff.d.ts +48 -0
  319. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  320. package/dist/core/tools/edit-diff.js +215 -3
  321. package/dist/core/tools/edit-diff.js.map +1 -1
  322. package/dist/core/tools/edit.d.ts +8 -0
  323. package/dist/core/tools/edit.d.ts.map +1 -1
  324. package/dist/core/tools/edit.js +53 -7
  325. package/dist/core/tools/edit.js.map +1 -1
  326. package/dist/core/tools/find.d.ts.map +1 -1
  327. package/dist/core/tools/find.js +5 -2
  328. package/dist/core/tools/find.js.map +1 -1
  329. package/dist/core/tools/grep.d.ts.map +1 -1
  330. package/dist/core/tools/grep.js +5 -2
  331. package/dist/core/tools/grep.js.map +1 -1
  332. package/dist/core/tools/index.d.ts +10 -1
  333. package/dist/core/tools/index.d.ts.map +1 -1
  334. package/dist/core/tools/index.js +3 -0
  335. package/dist/core/tools/index.js.map +1 -1
  336. package/dist/core/tools/ls.d.ts.map +1 -1
  337. package/dist/core/tools/ls.js +5 -2
  338. package/dist/core/tools/ls.js.map +1 -1
  339. package/dist/core/tools/lsp.d.ts +19 -1
  340. package/dist/core/tools/lsp.d.ts.map +1 -1
  341. package/dist/core/tools/lsp.js +163 -10
  342. package/dist/core/tools/lsp.js.map +1 -1
  343. package/dist/core/tools/path-permission.d.ts +3 -0
  344. package/dist/core/tools/path-permission.d.ts.map +1 -1
  345. package/dist/core/tools/path-permission.js +16 -4
  346. package/dist/core/tools/path-permission.js.map +1 -1
  347. package/dist/core/tools/path-utils.d.ts +27 -2
  348. package/dist/core/tools/path-utils.d.ts.map +1 -1
  349. package/dist/core/tools/path-utils.js +221 -4
  350. package/dist/core/tools/path-utils.js.map +1 -1
  351. package/dist/core/tools/read.d.ts.map +1 -1
  352. package/dist/core/tools/read.js +20 -11
  353. package/dist/core/tools/read.js.map +1 -1
  354. package/dist/core/tools/test.d.ts +54 -9
  355. package/dist/core/tools/test.d.ts.map +1 -1
  356. package/dist/core/tools/test.js +239 -19
  357. package/dist/core/tools/test.js.map +1 -1
  358. package/dist/core/tools/write.d.ts +7 -0
  359. package/dist/core/tools/write.d.ts.map +1 -1
  360. package/dist/core/tools/write.js +64 -9
  361. package/dist/core/tools/write.js.map +1 -1
  362. package/dist/core/verification-lifecycle.d.ts +77 -0
  363. package/dist/core/verification-lifecycle.d.ts.map +1 -0
  364. package/dist/core/verification-lifecycle.js +146 -0
  365. package/dist/core/verification-lifecycle.js.map +1 -0
  366. package/dist/core/workspace/artifacts.d.ts +45 -0
  367. package/dist/core/workspace/artifacts.d.ts.map +1 -0
  368. package/dist/core/workspace/artifacts.js +165 -0
  369. package/dist/core/workspace/artifacts.js.map +1 -0
  370. package/dist/core/workspace/comparison.d.ts +27 -0
  371. package/dist/core/workspace/comparison.d.ts.map +1 -0
  372. package/dist/core/workspace/comparison.js +94 -0
  373. package/dist/core/workspace/comparison.js.map +1 -0
  374. package/dist/core/workspace/git-observer.d.ts +48 -0
  375. package/dist/core/workspace/git-observer.d.ts.map +1 -0
  376. package/dist/core/workspace/git-observer.js +346 -0
  377. package/dist/core/workspace/git-observer.js.map +1 -0
  378. package/dist/core/workspace/projection.d.ts +3 -0
  379. package/dist/core/workspace/projection.d.ts.map +1 -0
  380. package/dist/core/workspace/projection.js +56 -0
  381. package/dist/core/workspace/projection.js.map +1 -0
  382. package/dist/core/workspace/state.d.ts +133 -0
  383. package/dist/core/workspace/state.d.ts.map +1 -0
  384. package/dist/core/workspace/state.js +96 -0
  385. package/dist/core/workspace/state.js.map +1 -0
  386. package/dist/extensions/llama/client.d.ts +6 -0
  387. package/dist/extensions/llama/client.d.ts.map +1 -1
  388. package/dist/extensions/llama/client.js +7 -0
  389. package/dist/extensions/llama/client.js.map +1 -1
  390. package/dist/extensions/llama/provider.d.ts +3 -1
  391. package/dist/extensions/llama/provider.d.ts.map +1 -1
  392. package/dist/extensions/llama/provider.js +27 -6
  393. package/dist/extensions/llama/provider.js.map +1 -1
  394. package/dist/index.d.ts +4 -1
  395. package/dist/index.d.ts.map +1 -1
  396. package/dist/index.js +3 -0
  397. package/dist/index.js.map +1 -1
  398. package/dist/main.d.ts +3 -1
  399. package/dist/main.d.ts.map +1 -1
  400. package/dist/main.js +41 -9
  401. package/dist/main.js.map +1 -1
  402. package/dist/modes/acp/server.d.ts +55 -0
  403. package/dist/modes/acp/server.d.ts.map +1 -0
  404. package/dist/modes/acp/server.js +244 -0
  405. package/dist/modes/acp/server.js.map +1 -0
  406. package/dist/modes/acp/translate.d.ts +37 -0
  407. package/dist/modes/acp/translate.d.ts.map +1 -0
  408. package/dist/modes/acp/translate.js +50 -0
  409. package/dist/modes/acp/translate.js.map +1 -0
  410. package/dist/modes/index.d.ts +1 -0
  411. package/dist/modes/index.d.ts.map +1 -1
  412. package/dist/modes/index.js +1 -0
  413. package/dist/modes/index.js.map +1 -1
  414. package/dist/modes/interactive/components/assistant-message.d.ts +29 -0
  415. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  416. package/dist/modes/interactive/components/assistant-message.js +182 -23
  417. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  418. package/dist/modes/interactive/components/bash-execution.d.ts +7 -0
  419. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  420. package/dist/modes/interactive/components/bash-execution.js +20 -0
  421. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  422. package/dist/modes/interactive/components/custom-editor.d.ts +9 -4
  423. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  424. package/dist/modes/interactive/components/custom-editor.js +20 -9
  425. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  426. package/dist/modes/interactive/components/extension-selector.d.ts +7 -1
  427. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  428. package/dist/modes/interactive/components/extension-selector.js +7 -3
  429. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  430. package/dist/modes/interactive/components/footer.d.ts +19 -0
  431. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  432. package/dist/modes/interactive/components/footer.js +70 -10
  433. package/dist/modes/interactive/components/footer.js.map +1 -1
  434. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  435. package/dist/modes/interactive/components/model-selector.js +6 -6
  436. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  437. package/dist/modes/interactive/components/permission-preview.d.ts +15 -0
  438. package/dist/modes/interactive/components/permission-preview.d.ts.map +1 -0
  439. package/dist/modes/interactive/components/permission-preview.js +33 -0
  440. package/dist/modes/interactive/components/permission-preview.js.map +1 -0
  441. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  442. package/dist/modes/interactive/components/session-selector.js +2 -2
  443. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  444. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  445. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  446. package/dist/modes/interactive/components/settings-selector.js +10 -0
  447. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  448. package/dist/modes/interactive/components/tool-execution.d.ts +23 -1
  449. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  450. package/dist/modes/interactive/components/tool-execution.js +64 -10
  451. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  452. package/dist/modes/interactive/interactive-mode.d.ts +34 -0
  453. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  454. package/dist/modes/interactive/interactive-mode.js +231 -26
  455. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  456. package/dist/modes/interactive/theme/theme.d.ts +12 -0
  457. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  458. package/dist/modes/interactive/theme/theme.js +15 -5
  459. package/dist/modes/interactive/theme/theme.js.map +1 -1
  460. package/dist/modes/print-mode.d.ts.map +1 -1
  461. package/dist/modes/print-mode.js +27 -12
  462. package/dist/modes/print-mode.js.map +1 -1
  463. package/dist/modes/rpc/rpc-client.d.ts +7 -0
  464. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  465. package/dist/modes/rpc/rpc-client.js +7 -0
  466. package/dist/modes/rpc/rpc-client.js.map +1 -1
  467. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  468. package/dist/modes/rpc/rpc-mode.js +3 -0
  469. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  470. package/dist/modes/rpc/rpc-types.d.ts +12 -0
  471. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  472. package/dist/modes/rpc/rpc-types.js.map +1 -1
  473. package/dist/package-manager-cli.d.ts.map +1 -1
  474. package/dist/package-manager-cli.js +0 -6
  475. package/dist/package-manager-cli.js.map +1 -1
  476. package/dist/utils/shell.d.ts.map +1 -1
  477. package/dist/utils/shell.js +6 -4
  478. package/dist/utils/shell.js.map +1 -1
  479. package/dist/utils/version-check.d.ts.map +1 -1
  480. package/dist/utils/version-check.js +6 -4
  481. package/dist/utils/version-check.js.map +1 -1
  482. package/docs/compaction.md +4 -0
  483. package/docs/custom-provider.md +7 -7
  484. package/docs/environment-variables.md +1 -1
  485. package/docs/extensions.md +18 -0
  486. package/docs/keybindings.md +1 -1
  487. package/docs/quickstart.md +1 -1
  488. package/docs/rpc.md +23 -0
  489. package/docs/sdk.md +25 -0
  490. package/docs/settings.md +12 -4
  491. package/docs/terminal-setup.md +12 -0
  492. package/docs/usage.md +4 -3
  493. package/examples/extensions/built-in-tool-renderer.ts +8 -4
  494. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  495. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  496. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  497. package/examples/extensions/git-checkpoint.ts +24 -36
  498. package/examples/extensions/gondolin/package-lock.json +2 -2
  499. package/examples/extensions/gondolin/package.json +1 -1
  500. package/examples/extensions/minimal-mode.ts +2 -2
  501. package/examples/extensions/permission-gate.ts +1 -1
  502. package/examples/extensions/plan-mode/index.ts +1 -1
  503. package/examples/extensions/rpc-demo.ts +1 -1
  504. package/examples/extensions/sandbox/package-lock.json +2 -2
  505. package/examples/extensions/sandbox/package.json +1 -1
  506. package/examples/extensions/with-deps/package-lock.json +2 -2
  507. package/examples/extensions/with-deps/package.json +1 -1
  508. package/npm-shrinkwrap.json +194 -46
  509. package/package.json +14 -12
  510. package/dist/server/create-harness.d.ts +0 -26
  511. package/dist/server/create-harness.d.ts.map +0 -1
  512. package/dist/server/create-harness.js +0 -107
  513. package/dist/server/create-harness.js.map +0 -1
@@ -13,7 +13,7 @@
13
13
  * Modes use this class and add their own I/O layer on top.
14
14
  */
15
15
  import { readFileSync } from "node:fs";
16
- import { basename, dirname } from "node:path";
16
+ import { basename, dirname, isAbsolute, relative } from "node:path";
17
17
  import { contentText } from "@earendil-works/pi-ai";
18
18
  import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, isRecoverableLength, isRetryableAssistantError, modelsAreEqual, resetApiProviders, } from "@earendil-works/pi-ai/compat";
19
19
  import { getThemeByName, theme } from "../modes/interactive/theme/theme.js";
@@ -22,6 +22,7 @@ import { sleep } from "../utils/sleep.js";
22
22
  import { normalizeToolResultImages } from "../utils/tool-result-images.js";
23
23
  import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
24
24
  import { executeBashWithOperations } from "./bash-executor.js";
25
+ import { createSessionCheckpoints } from "./checkpoints/session-checkpoints.js";
25
26
  import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
26
27
  import { evictionBudget, installContextPipeline, isDefaultStreamFunction } from "./context/pipeline.js";
27
28
  import { DEFAULT_THINKING_LEVEL, THINKING_LEVEL_OPTIONS } from "./defaults.js";
@@ -30,10 +31,13 @@ import { exportSessionToHtml } from "./export-html/index.js";
30
31
  import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
31
32
  import { ExtensionRunner, wrapRegisteredTools, } from "./extensions/index.js";
32
33
  import { emitSessionShutdownEvent } from "./extensions/runner.js";
34
+ import { runFormatterCommand } from "./formatter-lifecycle.js";
33
35
  import { ModelRegistry } from "./model-registry.js";
34
36
  import { evaluateToolCall } from "./permissions/gate.js";
37
+ import { authorizeConfiguredCommand } from "./permissions/policy-command.js";
35
38
  import { createInteractiveResponder } from "./permissions/responder.js";
36
39
  import { resolveEffectiveModeWithOrigin } from "./permissions/startup.js";
40
+ import { loadPolicyConfiguration } from "./policy-loader.js";
37
41
  import { expandPromptTemplate } from "./prompt-templates.js";
38
42
  import { exportSessionToJsonl } from "./session-export.js";
39
43
  import { getLatestCompactionEntry, TODO_CUSTOM_ENTRY_TYPE } from "./session-manager.js";
@@ -41,12 +45,18 @@ import { slugifySkillCommandName } from "./skills.js";
41
45
  import { createSyntheticSourceInfo } from "./source-info.js";
42
46
  import { buildSystemPrompt } from "./system-prompt.js";
43
47
  import { createLocalBashOperations } from "./tools/bash.js";
48
+ import { buildToolContractSnapshot, isUnclassifiedTool, } from "./tools/contract-snapshot.js";
44
49
  import { createAllToolDefinitions } from "./tools/index.js";
45
50
  import { createSkillSearchToolDefinition } from "./tools/skill-search.js";
46
51
  import { createTodoWriteToolDefinition } from "./tools/todo-write.js";
47
52
  import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";
48
53
  import { createToolSchemaToolDefinition } from "./tools/tool-schema.js";
49
54
  import { addUsageToTotals, createUsageTotals } from "./usage-totals.js";
55
+ import { VerificationTracker, } from "./verification-lifecycle.js";
56
+ import { buildWorkspaceComparison, compareWorkspaceObservations } from "./workspace/comparison.js";
57
+ import { observeWorkspaceGit } from "./workspace/git-observer.js";
58
+ import { formatWorkspaceProjection } from "./workspace/projection.js";
59
+ import { appendWorkspaceComparison, appendWorkspaceObservation, findLatestWorkspaceObservation, hasWorkspaceObservationAwaitingComparison, } from "./workspace/state.js";
50
60
  /**
51
61
  * Parse a skill block from message text.
52
62
  * Returns null if the text doesn't contain a skill block.
@@ -77,12 +87,6 @@ function estimateMessagesTokens(messages) {
77
87
  }
78
88
  return tokens;
79
89
  }
80
- // ============================================================================
81
- // Constants
82
- // ============================================================================
83
- // ============================================================================
84
- // AgentSession Class
85
- // ============================================================================
86
90
  export class AgentSession {
87
91
  agent;
88
92
  sessionManager;
@@ -100,10 +104,13 @@ export class AgentSession {
100
104
  _followUpMessages = [];
101
105
  /** Messages queued to be included with the next user prompt as context ("asides"). */
102
106
  _pendingNextTurnMessages = [];
107
+ /** Context-only custom messages queued during a run, flushed once the current turn's tool results are in. */
108
+ _pendingCustomMessages = [];
103
109
  // Compaction state
104
110
  _compactionAbortController = undefined;
105
111
  _autoCompactionAbortController = undefined;
106
112
  _overflowRecoveryAttempted = false;
113
+ _midRunCompactionNeeded = false;
107
114
  // Branch summarization state
108
115
  _branchSummaryAbortController = undefined;
109
116
  // Retry state
@@ -119,6 +126,21 @@ export class AgentSession {
119
126
  _customTools;
120
127
  _baseToolDefinitions = new Map();
121
128
  _cwd;
129
+ /**
130
+ * Workspace observation adapter for compaction capture (WS.4). Injectable
131
+ * so tests can force capture outcomes without a real repository.
132
+ */
133
+ _workspaceObserver = observeWorkspaceGit;
134
+ /**
135
+ * WS.5: one workspace comparison is due at the next model-turn boundary.
136
+ * Set after a compaction stored an observation, or on construction when
137
+ * the session's latest observation has no later comparison (resume).
138
+ */
139
+ _workspaceComparePending = false;
140
+ /** VF.4: verification lifecycle state, built lazily from policy settings. */
141
+ _verificationTracker;
142
+ /** The workspace observation a verification result described, for staleness. */
143
+ _verificationBaseline;
122
144
  _extensionRunnerRef;
123
145
  _initialActiveToolNames;
124
146
  _allowedToolNames;
@@ -129,6 +151,19 @@ export class AgentSession {
129
151
  _evidenceSink;
130
152
  _diagnosticsOperations;
131
153
  _lspOperations;
154
+ _mcpRuntime;
155
+ /** Configured MCP subsystem, or undefined when no server is configured. */
156
+ get mcpRuntime() {
157
+ return this._mcpRuntime;
158
+ }
159
+ _checkpoints;
160
+ _hookRuntime;
161
+ _backgroundShellRegistry;
162
+ _permissionResponderFactory;
163
+ /** Worktree checkpoints for this session. Inert unless `checkpoints.enabled` is set. */
164
+ get checkpoints() {
165
+ return this._checkpoints;
166
+ }
132
167
  _webSearchOperations;
133
168
  _extensionUIContext;
134
169
  _extensionMode = "print";
@@ -167,13 +202,33 @@ export class AgentSession {
167
202
  this._evidenceSink = config.evidenceSink ?? new SessionEvidenceSink(this.sessionManager);
168
203
  this._diagnosticsOperations = config.diagnosticsOperations;
169
204
  this._lspOperations = config.lspOperations;
205
+ this._mcpRuntime = config.mcpRuntime;
170
206
  this._webSearchOperations = config.webSearchOperations;
207
+ this._checkpoints = createSessionCheckpoints({
208
+ workspace: this._cwd,
209
+ sessionId: this.sessionManager.getSessionId(),
210
+ settings: config.checkpointSettings,
211
+ });
212
+ this._hookRuntime = config.hookRuntime;
213
+ this._backgroundShellRegistry = config.backgroundShellRegistry;
214
+ this._permissionResponderFactory = config.permissionResponderFactory;
215
+ this._workspaceComparePending = hasWorkspaceObservationAwaitingComparison(this.sessionManager);
171
216
  // Always subscribe to agent events for internal handling
172
217
  // (session persistence, extensions, auto-compaction, retry logic)
173
218
  this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
174
219
  this._installAgentToolHooks();
175
220
  this._installAgentNextTurnRefresh();
221
+ this._installMidRunCompactionHook();
176
222
  this._installContextPipeline();
223
+ // Declarative hooks: session_start is observe-only and must neither delay
224
+ // nor fail construction, so it fires detached (emitObserve swallows
225
+ // handler failures already).
226
+ if (this._hookRuntime?.hasHandlers("session_start")) {
227
+ void this._hookRuntime.emitObserve("session_start", {
228
+ type: "session_start",
229
+ reason: this._sessionStartEvent.reason,
230
+ });
231
+ }
177
232
  this._buildRuntime({
178
233
  activeToolNames: this._initialActiveToolNames,
179
234
  includeAllExtensionTools: true,
@@ -264,6 +319,20 @@ export class AgentSession {
264
319
  if (extensionResult?.block === true)
265
320
  return extensionResult;
266
321
  }
322
+ // Declarative hooks (spec 2026-08-31-declarative-hooks) ride the same
323
+ // seam, after extensions and before the gate. Restriction-only: a block
324
+ // short-circuits, allow and ask fall through, and the gate below still
325
+ // evaluates the call, so a hook can narrow authority but never widen it.
326
+ if (this._hookRuntime?.hasHandlers("tool_call")) {
327
+ const hookBlock = await this._hookRuntime.decideToolCall(toolCall.name, {
328
+ type: "tool_call",
329
+ toolName: toolCall.name,
330
+ toolCallId: toolCall.id,
331
+ input: args,
332
+ });
333
+ if (hookBlock)
334
+ return { block: true, reason: hookBlock.reason };
335
+ }
267
336
  // Permission gate (roadmap Phase 2a, ADR 0004). Off by default — see
268
337
  // AgentSessionConfig.permissionGate. When present, every registered tool
269
338
  // call passes through it (contracts.md invariant 2): this is the seam,
@@ -272,9 +341,10 @@ export class AgentSession {
272
341
  const decision = await evaluateToolCall(toolCall.name, args, {
273
342
  ...this._permissionGate,
274
343
  getContract: (name) => this.getToolDefinition(name)?.contract,
275
- getResponder: () => this._extensionMode === "tui" && this._extensionUIContext
276
- ? createInteractiveResponder(this._extensionUIContext)
277
- : undefined,
344
+ getResponder: () => this._permissionResponderFactory?.() ??
345
+ (this._extensionMode === "tui" && this._extensionUIContext
346
+ ? createInteractiveResponder(this._extensionUIContext)
347
+ : undefined),
278
348
  });
279
349
  if (decision.block)
280
350
  return { block: true, reason: decision.reason };
@@ -312,6 +382,16 @@ export class AgentSession {
312
382
  usage: result.usage,
313
383
  })
314
384
  : undefined;
385
+ // Declarative hooks observe tool results after extension rewrites and
386
+ // before image normalization; no decision is read from them (spec).
387
+ if (this._hookRuntime?.hasHandlers("tool_result")) {
388
+ await this._hookRuntime.emitObserve("tool_result", {
389
+ type: "tool_result",
390
+ toolName: toolCall.name,
391
+ toolCallId: toolCall.id,
392
+ input: args,
393
+ });
394
+ }
315
395
  const content = hookResult?.content ?? result.content ?? [];
316
396
  // Runs after the extension hook so images injected or replaced by extensions are normalized too.
317
397
  const normalizedContent = await normalizeToolResultImages(content, {
@@ -328,18 +408,33 @@ export class AgentSession {
328
408
  };
329
409
  };
330
410
  }
411
+ async _compactBeforeNextAssistantResponse(context) {
412
+ const model = this.model;
413
+ const settings = this.settingsManager.getCompactionSettings();
414
+ if (!model ||
415
+ model.contextWindow <= 0 ||
416
+ !shouldCompact(estimateContextTokens(context.messages).tokens, model.contextWindow, settings)) {
417
+ return context;
418
+ }
419
+ await this._runAutoCompaction("threshold", false);
420
+ return {
421
+ ...context,
422
+ messages: this.agent.state.messages.slice(),
423
+ };
424
+ }
331
425
  _installAgentNextTurnRefresh() {
332
426
  const previousPrepareNextTurnWithContext = this.agent.prepareNextTurnWithContext ??
333
427
  (this.agent.prepareNextTurn
334
428
  ? async (_turn, signal) => await this.agent.prepareNextTurn?.(signal)
335
429
  : undefined);
336
430
  this.agent.prepareNextTurnWithContext = async (turn, signal) => {
337
- const previousSnapshot = await previousPrepareNextTurnWithContext?.(turn, signal);
338
- const previousContext = previousSnapshot?.context ?? turn.context;
431
+ const context = await this._compactBeforeNextAssistantResponse(turn.context);
432
+ const previousSnapshot = await previousPrepareNextTurnWithContext?.({ ...turn, context }, signal);
433
+ const nextContext = previousSnapshot?.context ?? context;
339
434
  return {
340
435
  ...previousSnapshot,
341
436
  context: {
342
- ...previousContext,
437
+ ...nextContext,
343
438
  systemPrompt: this._systemPromptOverride ?? this._baseSystemPrompt,
344
439
  tools: this.agent.state.tools.slice(),
345
440
  },
@@ -348,6 +443,35 @@ export class AgentSession {
348
443
  };
349
444
  };
350
445
  }
446
+ /** Stop an over-threshold tool loop before it sends another provider request. */
447
+ _installMidRunCompactionHook() {
448
+ const previousShouldStopAfterTurn = this.agent.shouldStopAfterTurn;
449
+ this.agent.shouldStopAfterTurn = async (turn, signal) => {
450
+ if (await previousShouldStopAfterTurn?.(turn, signal)) {
451
+ return true;
452
+ }
453
+ return this._shouldStopForMidRunCompaction(turn);
454
+ };
455
+ }
456
+ _shouldStopForMidRunCompaction(turn) {
457
+ if (!turn.hasMoreToolCalls || turn.toolResults.length === 0) {
458
+ return false;
459
+ }
460
+ const settings = this.settingsManager.getCompactionSettings();
461
+ if (!settings.enabled) {
462
+ return false;
463
+ }
464
+ const contextWindow = this.model?.contextWindow ?? 0;
465
+ if (contextWindow <= 0) {
466
+ return false;
467
+ }
468
+ const contextTokens = estimateContextTokens(turn.context.messages).tokens;
469
+ if (!shouldCompact(contextTokens, contextWindow, settings)) {
470
+ return false;
471
+ }
472
+ this._midRunCompactionNeeded = true;
473
+ return true;
474
+ }
351
475
  /**
352
476
  * Installs the context pipeline settled in `docs/architecture/contracts.md` § 2:
353
477
  * deferred-schema resolution → tool-result eviction → compaction. Compaction
@@ -499,6 +623,14 @@ export class AgentSession {
499
623
  }
500
624
  }
501
625
  }
626
+ // A turn ends after its assistant message and every tool result has been appended,
627
+ // so this is the first point in the run where a context-only custom message can be
628
+ // inserted without landing between a tool call and its result. Flushing after the
629
+ // extension and listener dispatch above also picks up messages that turn_end
630
+ // handlers queued.
631
+ if (event.type === "turn_end") {
632
+ this._flushPendingCustomMessages();
633
+ }
502
634
  };
503
635
  _willRetryAfterAgentEnd(event) {
504
636
  const settings = this.settingsManager.getRetrySettings();
@@ -548,6 +680,18 @@ export class AgentSession {
548
680
  await this._extensionRunner.emit({ type: "agent_end", messages: event.messages });
549
681
  }
550
682
  else if (event.type === "turn_start") {
683
+ // Snapshot before the model gets to act, keyed to the entry `/tree` and `/fork`
684
+ // already navigate to, so the two agree about what an entry means.
685
+ //
686
+ // Awaited, despite the turn latency. Detached, the snapshot races the turn it is
687
+ // supposed to precede: a tool call can edit the worktree while `add -A` is still
688
+ // walking it, and the ref then holds mid-turn state that no `/fork` should ever
689
+ // restore. "The state before the model acted" is the whole guarantee, so paying
690
+ // for it here is the point. The engine's own timeout bounds the wait and a failed
691
+ // capture is never fatal.
692
+ const leafEntryId = this.sessionManager.getLeafEntry()?.id;
693
+ if (leafEntryId)
694
+ await this._checkpoints.capture(leafEntryId);
551
695
  const extensionEvent = {
552
696
  type: "turn_start",
553
697
  turnIndex: this._turnIndex,
@@ -563,6 +707,9 @@ export class AgentSession {
563
707
  toolResults: event.toolResults,
564
708
  };
565
709
  await this._extensionRunner.emit(extensionEvent);
710
+ if (this._hookRuntime?.hasHandlers("turn_end")) {
711
+ await this._hookRuntime.emitObserve("turn_end", { type: "turn_end" });
712
+ }
566
713
  this._turnIndex++;
567
714
  }
568
715
  else if (event.type === "message_start") {
@@ -662,6 +809,9 @@ export class AgentSession {
662
809
  this.abortBranchSummary();
663
810
  this.abortBash();
664
811
  this.agent.abort();
812
+ // Background shell children (spec 2026-08-31-background-shell.md) must
813
+ // never outlive the session that launched them.
814
+ this._backgroundShellRegistry?.dispose();
665
815
  }
666
816
  catch {
667
817
  // Dispose must succeed even if an abort hook throws.
@@ -669,6 +819,9 @@ export class AgentSession {
669
819
  this._extensionRunner.invalidate("This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().");
670
820
  this._disconnectFromAgent();
671
821
  this._eventListeners = [];
822
+ // Every connected MCP server is a child process this session started. Without
823
+ // this, one leaks per server per session, because nothing else owns them.
824
+ this._mcpRuntime?.close().catch(() => undefined);
672
825
  cleanupSessionResources(this.sessionId);
673
826
  }
674
827
  // =========================================================================
@@ -712,6 +865,13 @@ export class AgentSession {
712
865
  /**
713
866
  * Get all configured tools with name, description, parameter schema, prompt guidelines, and source metadata.
714
867
  */
868
+ /**
869
+ * The ADR 0010 projection over this session's registry: the one read every surface
870
+ * that *describes* the tool set consumes. Never an authorization input.
871
+ */
872
+ getToolContractSnapshot() {
873
+ return buildToolContractSnapshot(Array.from(this._toolDefinitions.values()).map(({ definition }) => definition));
874
+ }
715
875
  getAllTools() {
716
876
  const schemaToolActive = this.getActiveToolNames().includes("tool_schema");
717
877
  return Array.from(this._toolDefinitions.values())
@@ -722,7 +882,7 @@ export class AgentSession {
722
882
  parameters: definition.parameters,
723
883
  promptGuidelines: definition.promptGuidelines,
724
884
  sourceInfo,
725
- unclassified: !("contract" in definition),
885
+ unclassified: isUnclassifiedTool(definition),
726
886
  }));
727
887
  }
728
888
  getToolDefinition(name) {
@@ -858,10 +1018,15 @@ export class AgentSession {
858
1018
  finally {
859
1019
  this._systemPromptOverride = undefined;
860
1020
  this._flushPendingBashMessages();
1021
+ this._flushPendingCustomMessages();
861
1022
  await this._emitAgentSettled();
862
1023
  }
863
1024
  }
864
1025
  async _handlePostAgentRun() {
1026
+ if (this._midRunCompactionNeeded) {
1027
+ this._midRunCompactionNeeded = false;
1028
+ return await this._runAutoCompaction("threshold", true);
1029
+ }
865
1030
  const msg = this._lastAssistantMessage;
866
1031
  this._lastAssistantMessage = undefined;
867
1032
  if (!msg) {
@@ -947,8 +1112,9 @@ export class AgentSession {
947
1112
  preflightResult?.(true);
948
1113
  return;
949
1114
  }
950
- // Flush any pending bash messages before the new prompt
1115
+ // Flush any pending bash and custom messages before the new prompt
951
1116
  this._flushPendingBashMessages();
1117
+ this._flushPendingCustomMessages();
952
1118
  // Validate model
953
1119
  if (!this.model) {
954
1120
  throw new Error(formatNoModelSelectedMessage());
@@ -1022,7 +1188,9 @@ export class AgentSession {
1022
1188
  return;
1023
1189
  }
1024
1190
  preflightResult?.(true);
1191
+ await this._runWorkspaceComparisonBoundary();
1025
1192
  await this._runAgentPrompt(messages);
1193
+ await this._runVerificationTurnBoundary();
1026
1194
  }
1027
1195
  /**
1028
1196
  * Try to execute an extension command. Returns true if command was found and executed.
@@ -1169,8 +1337,9 @@ export class AgentSession {
1169
1337
  /**
1170
1338
  * Send a custom message to the session. Creates a CustomMessageEntry.
1171
1339
  *
1172
- * Handles three cases:
1340
+ * Handles four cases:
1173
1341
  * - Streaming: queues message, processed when loop pulls from queue
1342
+ * - Streaming + triggerTurn false: appended to state/session once the current turn ends
1174
1343
  * - Not streaming + triggerTurn: appends to state/session, starts new turn
1175
1344
  * - Not streaming + no trigger: appends to state/session, no turn
1176
1345
  *
@@ -1202,11 +1371,34 @@ export class AgentSession {
1202
1371
  else if (options?.triggerTurn) {
1203
1372
  await this._runAgentPrompt(appMessage);
1204
1373
  }
1374
+ else if (this.isStreaming) {
1375
+ // Appending now would put the message between an assistant tool call and its
1376
+ // result, which providers that validate message order reject on replay. Defer
1377
+ // to the end of the turn. Nothing is emitted yet: message events must not
1378
+ // describe messages the session tree does not contain.
1379
+ this._pendingCustomMessages.push(appMessage);
1380
+ }
1205
1381
  else {
1206
- this.agent.state.messages.push(appMessage);
1207
- this.sessionManager.appendCustomMessageEntry(message.customType, message.content, message.display, message.details);
1208
- this._emit({ type: "message_start", message: appMessage });
1209
- this._emit({ type: "message_end", message: appMessage });
1382
+ this._appendCustomMessage(appMessage);
1383
+ }
1384
+ }
1385
+ _appendCustomMessage(appMessage) {
1386
+ this.agent.state.messages.push(appMessage);
1387
+ this.sessionManager.appendCustomMessageEntry(appMessage.customType, appMessage.content, appMessage.display, appMessage.details);
1388
+ this._emit({ type: "message_start", message: appMessage });
1389
+ this._emit({ type: "message_end", message: appMessage });
1390
+ }
1391
+ /**
1392
+ * Append custom messages queued while the agent was running.
1393
+ * Called once the current turn's tool results are in agent state and session history.
1394
+ */
1395
+ _flushPendingCustomMessages() {
1396
+ if (this._pendingCustomMessages.length === 0)
1397
+ return;
1398
+ const pending = this._pendingCustomMessages;
1399
+ this._pendingCustomMessages = [];
1400
+ for (const appMessage of pending) {
1401
+ this._appendCustomMessage(appMessage);
1210
1402
  }
1211
1403
  }
1212
1404
  /**
@@ -1345,6 +1537,7 @@ export class AgentSession {
1345
1537
  this.sessionManager.appendModelChange(model.provider, model.id);
1346
1538
  if (options.persist) {
1347
1539
  this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
1540
+ this._addPersistedDefaultToNonEmptyScope(model);
1348
1541
  }
1349
1542
  // Apply thinking level for the new model.
1350
1543
  // Per-model thinking level overrides take priority over the global default.
@@ -1352,6 +1545,20 @@ export class AgentSession {
1352
1545
  this.setThinkingLevel(thinkingLevel);
1353
1546
  await this._emitModelSelect(model, previousModel, "set");
1354
1547
  }
1548
+ _addPersistedDefaultToNonEmptyScope(model) {
1549
+ if (this._scopedModels.length === 0)
1550
+ return;
1551
+ if (this._scopedModels.some((scoped) => modelsAreEqual(scoped.model, model)))
1552
+ return;
1553
+ this._scopedModels = [...this._scopedModels, { model }];
1554
+ const enabledModels = this.settingsManager.getEnabledModels();
1555
+ if (!enabledModels?.length)
1556
+ return;
1557
+ const modelReference = `${model.provider}/${model.id}`;
1558
+ if (enabledModels.some((pattern) => pattern.toLowerCase() === modelReference.toLowerCase()))
1559
+ return;
1560
+ this.settingsManager.setEnabledModels([...enabledModels, modelReference]);
1561
+ }
1355
1562
  /**
1356
1563
  * Cycle to next/previous model.
1357
1564
  * Uses scoped models (from --models flag) if available, otherwise all available models.
@@ -1382,6 +1589,7 @@ export class AgentSession {
1382
1589
  this.sessionManager.appendModelChange(next.model.provider, next.model.id);
1383
1590
  if (options.persist) {
1384
1591
  this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id);
1592
+ this._addPersistedDefaultToNonEmptyScope(next.model);
1385
1593
  }
1386
1594
  // Apply thinking level for the new model.
1387
1595
  // - Explicit scoped model thinking level overrides defaults
@@ -1408,6 +1616,7 @@ export class AgentSession {
1408
1616
  this.sessionManager.appendModelChange(nextModel.provider, nextModel.id);
1409
1617
  if (options.persist) {
1410
1618
  this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
1619
+ this._addPersistedDefaultToNonEmptyScope(nextModel);
1411
1620
  }
1412
1621
  // Apply thinking level for the new model.
1413
1622
  // Model persistence does not implicitly rewrite the global thinking default.
@@ -1516,7 +1725,209 @@ export class AgentSession {
1516
1725
  // Compaction
1517
1726
  // =========================================================================
1518
1727
  /** Generate Pi's built-in compaction summary for manual and automatic compaction. */
1728
+ /**
1729
+ * Attempt exactly one workspace observation for a compaction (spec
1730
+ * 2026-09-01-harness-correctness-and-workspace-state.md § 3 step 1).
1731
+ * Returns undefined when observation is impossible; never throws and
1732
+ * never fails the surrounding compaction.
1733
+ */
1734
+ async _captureWorkspaceObservation(signal) {
1735
+ try {
1736
+ return await this._workspaceObserver(this._cwd, {
1737
+ signal,
1738
+ // The session's own state directory is harness bookkeeping, not
1739
+ // workspace content; observing it would report every turn as drift.
1740
+ excludePaths: this._sessionExclusionPaths(),
1741
+ });
1742
+ }
1743
+ catch {
1744
+ return undefined;
1745
+ }
1746
+ }
1747
+ /** Session/artifact directories inside the workspace, relative paths. */
1748
+ _sessionExclusionPaths() {
1749
+ const exclusions = [];
1750
+ const sessionDir = this.sessionManager.getSessionDir();
1751
+ if (sessionDir) {
1752
+ const rel = relative(this._cwd, sessionDir);
1753
+ if (rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel))
1754
+ exclusions.push(rel);
1755
+ }
1756
+ // Harness runtime bookkeeping (auth, model store, durable state) is
1757
+ // not workspace content either: when the agent directory sits inside
1758
+ // the workspace, its runtime files would report every turn as drift.
1759
+ // Same rule and same name list the fork/workspace fingerprints use.
1760
+ const agentDir = this.settingsManager.getAgentDir();
1761
+ if (agentDir !== undefined) {
1762
+ const rel = relative(this._cwd, agentDir);
1763
+ if (rel === "") {
1764
+ exclusions.push("auth.json", "models-store.json", "state.sqlite");
1765
+ }
1766
+ else if (rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel)) {
1767
+ exclusions.push(rel);
1768
+ }
1769
+ }
1770
+ return exclusions;
1771
+ }
1772
+ // ── VF.4: verification lifecycle (spec
1773
+ // 2026-09-01-configured-verification-and-formatting.md § 4) ──────────
1774
+ /**
1775
+ * Build (once) and refresh the tracker from the raw per-source policy
1776
+ * settings. ADR 0028: the loader consumes the sources separately and the
1777
+ * trust gate decides whether the project source exists at all.
1778
+ */
1779
+ /**
1780
+ * PS.1. Configured commands reach the same permission authority tool calls
1781
+ * use. Undefined only when the session has no gate at all, which is the
1782
+ * documented SDK default; a gated session always authorizes.
1783
+ */
1784
+ _authorizeConfiguredCommand() {
1785
+ const gate = this._permissionGate;
1786
+ if (gate === undefined)
1787
+ return undefined;
1788
+ return (operation) => authorizeConfiguredCommand(operation, {
1789
+ getMode: gate.getMode,
1790
+ getResponder: () => this._permissionResponderFactory?.() ??
1791
+ (this._extensionMode === "tui" && this._extensionUIContext
1792
+ ? createInteractiveResponder(this._extensionUIContext)
1793
+ : undefined),
1794
+ });
1795
+ }
1796
+ _ensureVerificationTracker() {
1797
+ if (this._verificationTracker === undefined) {
1798
+ this._verificationTracker = new VerificationTracker({
1799
+ workspaceRoot: this._cwd,
1800
+ ...(this._authorizeConfiguredCommand() !== undefined
1801
+ ? { authorize: this._authorizeConfiguredCommand() }
1802
+ : {}),
1803
+ });
1804
+ }
1805
+ const snapshot = loadPolicyConfiguration({
1806
+ globalSettings: this.settingsManager.getGlobalSettings().policies,
1807
+ projectSettings: this.settingsManager.getProjectSettings().policies,
1808
+ projectTrusted: this.settingsManager.isProjectTrusted(),
1809
+ });
1810
+ const boundary = this.settingsManager.getPolicySettings()?.boundary === "post-turn" ? "post-turn" : "explicit";
1811
+ this._verificationTracker.configure(snapshot.verification, boundary);
1812
+ return this._verificationTracker;
1813
+ }
1814
+ /**
1815
+ * Run one configured verification policy now and report the resulting
1816
+ * completion status. Unavailable when nothing is configured; the
1817
+ * workspace observation after the run becomes the staleness baseline.
1818
+ */
1819
+ async requestVerification(options = {}) {
1820
+ const tracker = this._ensureVerificationTracker();
1821
+ if (tracker.policyIds().length === 0) {
1822
+ return "unavailable";
1823
+ }
1824
+ const record = await tracker.runExplicit(options.policyId, { signal: options.signal });
1825
+ if (record !== undefined) {
1826
+ this._verificationBaseline = await this._captureWorkspaceObservation(options.signal);
1827
+ }
1828
+ return this.verificationStatus();
1829
+ }
1830
+ /**
1831
+ * VF.5: run one configured formatter policy now and report what it
1832
+ * actually mutated. Undefined when no formatter policy matches. Any real
1833
+ * mutation retires a prior verification record: the workspace the
1834
+ * verification described no longer exists.
1835
+ */
1836
+ async runConfiguredFormatter(options = {}) {
1837
+ const snapshot = loadPolicyConfiguration({
1838
+ globalSettings: this.settingsManager.getGlobalSettings().policies,
1839
+ projectSettings: this.settingsManager.getProjectSettings().policies,
1840
+ projectTrusted: this.settingsManager.isProjectTrusted(),
1841
+ });
1842
+ const policy = options.policyId === undefined
1843
+ ? snapshot.formatter[0]
1844
+ : snapshot.formatter.find((entry) => entry.id === options.policyId);
1845
+ if (policy === undefined)
1846
+ return undefined;
1847
+ const authorize = this._authorizeConfiguredCommand();
1848
+ const outcome = await runFormatterCommand(policy, {
1849
+ workspaceRoot: this._cwd,
1850
+ signal: options.signal,
1851
+ ...(authorize !== undefined ? { authorize } : {}),
1852
+ });
1853
+ if (outcome.mutations.changedPaths.length > 0) {
1854
+ this._verificationTracker?.noteWorkspaceChange(new Set(outcome.mutations.changedPaths));
1855
+ }
1856
+ return outcome;
1857
+ }
1858
+ /** The current completion status: verified, failed, unavailable, interrupted, or continued-unverified. */
1859
+ verificationStatus() {
1860
+ return this._verificationTracker?.completionStatus() ?? "unavailable";
1861
+ }
1862
+ /** The live verification record with its bounded evidence, if any. */
1863
+ verificationRecord() {
1864
+ return this._verificationTracker?.latest();
1865
+ }
1866
+ /** Record that the user chose to finish without verification. */
1867
+ continueWithoutVerification() {
1868
+ this._ensureVerificationTracker().recordContinuedWithoutVerification();
1869
+ }
1870
+ /**
1871
+ * Turn boundary: retire a verification result the workspace has moved
1872
+ * past (conservatively — anything not provably "same" retires it), then
1873
+ * run the configured post-turn boundary. Never throws: verification is
1874
+ * advisory and the turn already completed.
1875
+ */
1876
+ async _runVerificationTurnBoundary() {
1877
+ let tracker = this._verificationTracker;
1878
+ if (tracker === undefined) {
1879
+ tracker = this._ensureVerificationTracker();
1880
+ }
1881
+ try {
1882
+ if (tracker.latest() !== undefined) {
1883
+ const fresh = await this._captureWorkspaceObservation(undefined);
1884
+ if (this._verificationBaseline === undefined) {
1885
+ tracker.noteWorkspaceChange(new Set(["unbaseline"]));
1886
+ }
1887
+ else {
1888
+ const comparison = compareWorkspaceObservations(this._verificationBaseline, fresh);
1889
+ tracker.noteWorkspaceChange(comparison.result === "same" ? new Set() : new Set(["workspace-changed"]));
1890
+ }
1891
+ }
1892
+ if (tracker.boundary === "post-turn" && tracker.policyIds().length > 0) {
1893
+ await tracker.runExplicit();
1894
+ this._verificationBaseline = await this._captureWorkspaceObservation(undefined);
1895
+ }
1896
+ }
1897
+ catch {
1898
+ // Verification is advisory; the turn already completed.
1899
+ }
1900
+ }
1901
+ /**
1902
+ * WS.5 (spec 2026-09-01-harness-correctness-and-workspace-state.md § 3):
1903
+ * at the first model turn after a compaction, or after resume onto a
1904
+ * session with an uncompared observation, compare fresh workspace state
1905
+ * against the stored observation once and persist the outcome. The
1906
+ * observation stays historical. Never throws: comparison problems must
1907
+ * not fail the user's turn.
1908
+ */
1909
+ async _runWorkspaceComparisonBoundary() {
1910
+ if (!this._workspaceComparePending)
1911
+ return;
1912
+ this._workspaceComparePending = false;
1913
+ try {
1914
+ const baseline = findLatestWorkspaceObservation(this.sessionManager);
1915
+ if (!baseline)
1916
+ return;
1917
+ const fresh = await this._captureWorkspaceObservation(undefined);
1918
+ const outcome = compareWorkspaceObservations(baseline.record, fresh);
1919
+ appendWorkspaceComparison(this.sessionManager, buildWorkspaceComparison(baseline.record.observationId, outcome));
1920
+ }
1921
+ catch {
1922
+ // Comparison is advisory; the turn proceeds.
1923
+ }
1924
+ }
1519
1925
  async _runDefaultCompaction(preparation, requestModel, apiKey, headers, customInstructions, signal, env, reason) {
1926
+ // Maintenance budget (spec 2026-09-01-tool-reliability-and-execution-budgets.md):
1927
+ // a compaction summarization is context maintenance, not task progress. It is
1928
+ // recorded on the run's budget for observability and never consumes
1929
+ // maxProviderRequests.
1930
+ this.agent.recordMaintenanceRequest();
1520
1931
  return compact(preparation, requestModel, apiKey, headers, customInstructions, signal, this.thinkingLevel, this.agent.streamFunction, env, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason }), undefined);
1521
1932
  }
1522
1933
  /**
@@ -1556,6 +1967,12 @@ export class AgentSession {
1556
1967
  throw new Error("Nothing to compact (session too small)");
1557
1968
  }
1558
1969
  let extensionCompaction;
1970
+ if (this._hookRuntime?.hasHandlers("session_before_compact")) {
1971
+ await this._hookRuntime.emitObserve("session_before_compact", {
1972
+ type: "session_before_compact",
1973
+ reason: "manual",
1974
+ });
1975
+ }
1559
1976
  if (this._extensionRunner.hasHandlers("session_before_compact")) {
1560
1977
  const result = (await this._extensionRunner.emit({
1561
1978
  type: "session_before_compact",
@@ -1579,6 +1996,12 @@ export class AgentSession {
1579
1996
  let tokensBefore;
1580
1997
  let usage;
1581
1998
  let details;
1999
+ // One workspace observation attempt per compaction (spec
2000
+ // 2026-09-01-harness-correctness-and-workspace-state.md § 3),
2001
+ // before summary generation so the bounded projection can join
2002
+ // the model-facing summary. Capture problems never fail the
2003
+ // compaction.
2004
+ const workspaceRecord = await this._captureWorkspaceObservation(this._compactionAbortController?.signal ?? this._autoCompactionAbortController?.signal);
1582
2005
  if (extensionCompaction) {
1583
2006
  // Extension provided compaction content
1584
2007
  summary = extensionCompaction.summary;
@@ -1595,11 +2018,28 @@ export class AgentSession {
1595
2018
  tokensBefore = result.tokensBefore;
1596
2019
  usage = result.usage;
1597
2020
  details = result.details;
2021
+ if (workspaceRecord) {
2022
+ summary += `\n\n${formatWorkspaceProjection(workspaceRecord)}`;
2023
+ }
1598
2024
  }
1599
2025
  if (this._compactionAbortController.signal.aborted) {
1600
2026
  throw new Error("Compaction cancelled");
1601
2027
  }
1602
2028
  this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension, usage);
2029
+ if (workspaceRecord) {
2030
+ try {
2031
+ // Historical record, child of the compaction entry; never in
2032
+ // the model context and never a rewrite of history.
2033
+ appendWorkspaceObservation(this.sessionManager, workspaceRecord);
2034
+ // WS.5: the next model turn compares fresh state against
2035
+ // this observation and records the drift outcome.
2036
+ this._workspaceComparePending = true;
2037
+ }
2038
+ catch {
2039
+ // Artifact/entry persistence problems downgrade the
2040
+ // workspace status; they never fail transcript compaction.
2041
+ }
2042
+ }
1603
2043
  const newEntries = this.sessionManager.getEntries();
1604
2044
  const sessionContext = this.sessionManager.buildSessionContext();
1605
2045
  this.agent.state.messages = sessionContext.messages;
@@ -1817,6 +2257,9 @@ export class AgentSession {
1817
2257
  this._autoCompactionAbortController = new AbortController();
1818
2258
  started = true;
1819
2259
  let extensionCompaction;
2260
+ if (this._hookRuntime?.hasHandlers("session_before_compact")) {
2261
+ await this._hookRuntime.emitObserve("session_before_compact", { type: "session_before_compact", reason });
2262
+ }
1820
2263
  if (this._extensionRunner.hasHandlers("session_before_compact")) {
1821
2264
  const extensionResult = (await this._extensionRunner.emit({
1822
2265
  type: "session_before_compact",
@@ -1853,6 +2296,12 @@ export class AgentSession {
1853
2296
  let tokensBefore;
1854
2297
  let usage;
1855
2298
  let details;
2299
+ // One workspace observation attempt per compaction (spec
2300
+ // 2026-09-01-harness-correctness-and-workspace-state.md § 3),
2301
+ // before summary generation so the bounded projection can join
2302
+ // the model-facing summary. Capture problems never fail the
2303
+ // compaction.
2304
+ const workspaceRecord = await this._captureWorkspaceObservation(this._compactionAbortController?.signal ?? this._autoCompactionAbortController?.signal);
1856
2305
  if (extensionCompaction) {
1857
2306
  // Extension provided compaction content
1858
2307
  summary = extensionCompaction.summary;
@@ -1869,6 +2318,9 @@ export class AgentSession {
1869
2318
  tokensBefore = compactResult.tokensBefore;
1870
2319
  usage = compactResult.usage;
1871
2320
  details = compactResult.details;
2321
+ if (workspaceRecord) {
2322
+ summary += `\n\n${formatWorkspaceProjection(workspaceRecord)}`;
2323
+ }
1872
2324
  }
1873
2325
  if (this._autoCompactionAbortController.signal.aborted) {
1874
2326
  this._emit({
@@ -1887,6 +2339,20 @@ export class AgentSession {
1887
2339
  return false;
1888
2340
  }
1889
2341
  this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension, usage);
2342
+ if (workspaceRecord) {
2343
+ try {
2344
+ // Historical record, child of the compaction entry; never in
2345
+ // the model context and never a rewrite of history.
2346
+ appendWorkspaceObservation(this.sessionManager, workspaceRecord);
2347
+ // WS.5: the next model turn compares fresh state against
2348
+ // this observation and records the drift outcome.
2349
+ this._workspaceComparePending = true;
2350
+ }
2351
+ catch {
2352
+ // Artifact/entry persistence problems downgrade the
2353
+ // workspace status; they never fail transcript compaction.
2354
+ }
2355
+ }
1890
2356
  const newEntries = this.sessionManager.getEntries();
1891
2357
  const sessionContext = this.sessionManager.buildSessionContext();
1892
2358
  this.agent.state.messages = sessionContext.messages;
@@ -2260,10 +2726,15 @@ export class AgentSession {
2260
2726
  ]))
2261
2727
  : createAllToolDefinitions(this._cwd, {
2262
2728
  read: { autoResizeImages },
2263
- bash: { commandPrefix: shellCommandPrefix, shellPath },
2729
+ bash: {
2730
+ commandPrefix: shellCommandPrefix,
2731
+ shellPath,
2732
+ backgroundRegistry: this._backgroundShellRegistry,
2733
+ },
2264
2734
  edit: { diagnosticsOperations: this._diagnosticsOperations },
2265
2735
  write: { diagnosticsOperations: this._diagnosticsOperations },
2266
2736
  ...(this._lspOperations ? { lsp: { operations: this._lspOperations } } : {}),
2737
+ ...(this._mcpRuntime ? { mcp: this._mcpRuntime } : {}),
2267
2738
  ...(this._webSearchOperations ? { web_search: { operations: this._webSearchOperations } } : {}),
2268
2739
  });
2269
2740
  baseToolDefinitions.tool_schema = createToolSchemaToolDefinition({
@@ -2575,6 +3046,13 @@ export class AgentSession {
2575
3046
  if (targetId === oldLeafId) {
2576
3047
  return { cancelled: false };
2577
3048
  }
3049
+ // WS.6 (spec 2026-09-01-harness-correctness-and-workspace-state.md § 4):
3050
+ // files move only under an explicit policy, and a refused or cancelled
3051
+ // restore leaves the conversation untouched too.
3052
+ const workspaceStep = await this._resolveTreeWorkspaceStep(targetId, options.workspacePolicy ?? "keep");
3053
+ if (workspaceStep.cancelled) {
3054
+ return { cancelled: true, workspace: workspaceStep.outcome };
3055
+ }
2578
3056
  // Model required for summarization
2579
3057
  if (options.summarize && !this.model) {
2580
3058
  throw new Error("No model available for summarization");
@@ -2721,12 +3199,77 @@ export class AgentSession {
2721
3199
  fromExtension: summaryText ? fromExtension : undefined,
2722
3200
  });
2723
3201
  // Emit to custom tools
2724
- return { editorText, cancelled: false, summaryEntry };
3202
+ return { editorText, cancelled: false, summaryEntry, workspace: workspaceStep.outcome };
2725
3203
  }
2726
3204
  finally {
2727
3205
  this._branchSummaryAbortController = undefined;
2728
3206
  }
2729
3207
  }
3208
+ /**
3209
+ * WS.6: resolve what navigation may do to the workspace before the
3210
+ * conversation moves. `keep` never touches files; `restore` overwrites
3211
+ * only through the engine's pinned pre-restore checkpoint; `fail-if-
3212
+ * drifted` and `cancel` refuse when the workspace moved; a missing
3213
+ * checkpoint leaves the workspace unchanged and says so. Never throws.
3214
+ */
3215
+ async _resolveTreeWorkspaceStep(targetId, policy) {
3216
+ const unchanged = { policy, outcome: "unchanged", warnings: [] };
3217
+ if (policy === "keep")
3218
+ return { outcome: unchanged };
3219
+ try {
3220
+ const engine = await this._checkpoints.engine();
3221
+ if (!engine) {
3222
+ return {
3223
+ outcome: {
3224
+ policy,
3225
+ outcome: "missing-checkpoint",
3226
+ warnings: ["checkpoints are unavailable; workspace left unchanged"],
3227
+ },
3228
+ };
3229
+ }
3230
+ const checkpoint = await engine.lookup(targetId);
3231
+ if (!checkpoint)
3232
+ return { outcome: { policy, outcome: "missing-checkpoint", warnings: [] } };
3233
+ const matches = await engine.matchesWorktree(checkpoint);
3234
+ if (matches === undefined) {
3235
+ return {
3236
+ outcome: {
3237
+ policy,
3238
+ outcome: "failed",
3239
+ warnings: ["could not compare the workspace against the checkpoint"],
3240
+ },
3241
+ };
3242
+ }
3243
+ if (matches)
3244
+ return { outcome: unchanged };
3245
+ if (policy === "fail-if-drifted" || policy === "cancel") {
3246
+ return { cancelled: true, outcome: { policy, outcome: "refused-drifted", warnings: [] } };
3247
+ }
3248
+ const preRestore = await engine.restore(checkpoint);
3249
+ if (!preRestore) {
3250
+ return {
3251
+ outcome: { policy, outcome: "failed", warnings: ["restore failed; workspace left unchanged"] },
3252
+ };
3253
+ }
3254
+ return {
3255
+ outcome: {
3256
+ policy,
3257
+ outcome: "restored",
3258
+ preRestoreCheckpoint: { entryId: preRestore.entryId, commit: preRestore.commit },
3259
+ warnings: [],
3260
+ },
3261
+ };
3262
+ }
3263
+ catch (error) {
3264
+ return {
3265
+ outcome: {
3266
+ policy,
3267
+ outcome: "failed",
3268
+ warnings: [`workspace policy check failed: ${error instanceof Error ? error.message : String(error)}`],
3269
+ },
3270
+ };
3271
+ }
3272
+ }
2730
3273
  /**
2731
3274
  * Get all user messages from session for fork selector.
2732
3275
  */