synorch 0.2.0 → 0.4.0-beta.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 (1137) hide show
  1. package/CHANGELOG.md +55 -1
  2. package/README.md +338 -28
  3. package/dist/application/doctor-service.d.ts +16 -0
  4. package/dist/application/doctor-service.d.ts.map +1 -1
  5. package/dist/application/doctor-service.js +230 -32
  6. package/dist/application/doctor-service.js.map +1 -1
  7. package/dist/application/generated-skill-doctor.d.ts +9 -0
  8. package/dist/application/generated-skill-doctor.d.ts.map +1 -0
  9. package/dist/application/generated-skill-doctor.js +433 -0
  10. package/dist/application/generated-skill-doctor.js.map +1 -0
  11. package/dist/application/project-discovery.d.ts +26 -1
  12. package/dist/application/project-discovery.d.ts.map +1 -1
  13. package/dist/application/project-discovery.js +99 -6
  14. package/dist/application/project-discovery.js.map +1 -1
  15. package/dist/application/safe-path.d.ts +19 -0
  16. package/dist/application/safe-path.d.ts.map +1 -0
  17. package/dist/application/safe-path.js +35 -0
  18. package/dist/application/safe-path.js.map +1 -0
  19. package/dist/application/size-ceiling.d.ts +15 -0
  20. package/dist/application/size-ceiling.d.ts.map +1 -0
  21. package/dist/application/size-ceiling.js +32 -0
  22. package/dist/application/size-ceiling.js.map +1 -0
  23. package/dist/application/structure-service.d.ts.map +1 -1
  24. package/dist/application/structure-service.js +10 -1
  25. package/dist/application/structure-service.js.map +1 -1
  26. package/dist/build-info.json +4 -0
  27. package/dist/cli.js +135 -18
  28. package/dist/cli.js.map +1 -1
  29. package/dist/domain/canonical-contracts.d.ts +69 -0
  30. package/dist/domain/canonical-contracts.d.ts.map +1 -0
  31. package/dist/domain/canonical-contracts.js +92 -0
  32. package/dist/domain/canonical-contracts.js.map +1 -0
  33. package/dist/domain/generated-skill.d.ts +55 -0
  34. package/dist/domain/generated-skill.d.ts.map +1 -0
  35. package/dist/domain/generated-skill.js +90 -0
  36. package/dist/domain/generated-skill.js.map +1 -0
  37. package/dist/domain/generation.d.ts +15 -2
  38. package/dist/domain/generation.d.ts.map +1 -1
  39. package/dist/domain/observation-ledger.d.ts +118 -0
  40. package/dist/domain/observation-ledger.d.ts.map +1 -0
  41. package/dist/domain/observation-ledger.js +183 -0
  42. package/dist/domain/observation-ledger.js.map +1 -0
  43. package/dist/domain/product.d.ts +1 -1
  44. package/dist/domain/product.d.ts.map +1 -1
  45. package/dist/domain/product.js +1 -1
  46. package/dist/domain/product.js.map +1 -1
  47. package/dist/domain/relative-path.d.ts +21 -0
  48. package/dist/domain/relative-path.d.ts.map +1 -0
  49. package/dist/domain/relative-path.js +43 -0
  50. package/dist/domain/relative-path.js.map +1 -0
  51. package/dist/domain/skill-packs.d.ts.map +1 -1
  52. package/dist/domain/skill-packs.js +1 -0
  53. package/dist/domain/skill-packs.js.map +1 -1
  54. package/dist/domain/suggest.d.ts +5 -0
  55. package/dist/domain/suggest.d.ts.map +1 -0
  56. package/dist/domain/suggest.js +32 -0
  57. package/dist/domain/suggest.js.map +1 -0
  58. package/dist/domain/zod-issues.d.ts +21 -0
  59. package/dist/domain/zod-issues.d.ts.map +1 -0
  60. package/dist/domain/zod-issues.js +27 -0
  61. package/dist/domain/zod-issues.js.map +1 -0
  62. package/dist/harness/auth/api-key.d.ts +13 -0
  63. package/dist/harness/auth/api-key.d.ts.map +1 -0
  64. package/dist/harness/auth/api-key.js +87 -0
  65. package/dist/harness/auth/api-key.js.map +1 -0
  66. package/dist/harness/auth/claude-bridge.d.ts +22 -0
  67. package/dist/harness/auth/claude-bridge.d.ts.map +1 -0
  68. package/dist/harness/auth/claude-bridge.js +92 -0
  69. package/dist/harness/auth/claude-bridge.js.map +1 -0
  70. package/dist/harness/auth/command.d.ts +17 -0
  71. package/dist/harness/auth/command.d.ts.map +1 -0
  72. package/dist/harness/auth/command.js +223 -0
  73. package/dist/harness/auth/command.js.map +1 -0
  74. package/dist/harness/auth/credential-store.d.ts +30 -0
  75. package/dist/harness/auth/credential-store.d.ts.map +1 -0
  76. package/dist/harness/auth/credential-store.js +270 -0
  77. package/dist/harness/auth/credential-store.js.map +1 -0
  78. package/dist/harness/auth/index.d.ts +13 -0
  79. package/dist/harness/auth/index.d.ts.map +1 -0
  80. package/dist/harness/auth/index.js +10 -0
  81. package/dist/harness/auth/index.js.map +1 -0
  82. package/dist/harness/auth/json-file.d.ts +8 -0
  83. package/dist/harness/auth/json-file.d.ts.map +1 -0
  84. package/dist/harness/auth/json-file.js +57 -0
  85. package/dist/harness/auth/json-file.js.map +1 -0
  86. package/dist/harness/auth/jwt.d.ts +11 -0
  87. package/dist/harness/auth/jwt.d.ts.map +1 -0
  88. package/dist/harness/auth/jwt.js +37 -0
  89. package/dist/harness/auth/jwt.js.map +1 -0
  90. package/dist/harness/auth/keychain.d.ts +39 -0
  91. package/dist/harness/auth/keychain.d.ts.map +1 -0
  92. package/dist/harness/auth/keychain.js +191 -0
  93. package/dist/harness/auth/keychain.js.map +1 -0
  94. package/dist/harness/auth/locks.d.ts +19 -0
  95. package/dist/harness/auth/locks.d.ts.map +1 -0
  96. package/dist/harness/auth/locks.js +102 -0
  97. package/dist/harness/auth/locks.js.map +1 -0
  98. package/dist/harness/auth/notices.d.ts +6 -0
  99. package/dist/harness/auth/notices.d.ts.map +1 -0
  100. package/dist/harness/auth/notices.js +29 -0
  101. package/dist/harness/auth/notices.js.map +1 -0
  102. package/dist/harness/auth/openai-chatgpt.d.ts +32 -0
  103. package/dist/harness/auth/openai-chatgpt.d.ts.map +1 -0
  104. package/dist/harness/auth/openai-chatgpt.js +446 -0
  105. package/dist/harness/auth/openai-chatgpt.js.map +1 -0
  106. package/dist/harness/auth/profile-state.d.ts +25 -0
  107. package/dist/harness/auth/profile-state.d.ts.map +1 -0
  108. package/dist/harness/auth/profile-state.js +77 -0
  109. package/dist/harness/auth/profile-state.js.map +1 -0
  110. package/dist/harness/auth/providers.d.ts +27 -0
  111. package/dist/harness/auth/providers.d.ts.map +1 -0
  112. package/dist/harness/auth/providers.js +66 -0
  113. package/dist/harness/auth/providers.js.map +1 -0
  114. package/dist/harness/auth/resolved-credential.d.ts +11 -0
  115. package/dist/harness/auth/resolved-credential.d.ts.map +1 -0
  116. package/dist/harness/auth/resolved-credential.js +35 -0
  117. package/dist/harness/auth/resolved-credential.js.map +1 -0
  118. package/dist/harness/cli/appearance.d.ts +28 -0
  119. package/dist/harness/cli/appearance.d.ts.map +1 -0
  120. package/dist/harness/cli/appearance.js +123 -0
  121. package/dist/harness/cli/appearance.js.map +1 -0
  122. package/dist/harness/cli/args.d.ts +429 -0
  123. package/dist/harness/cli/args.d.ts.map +1 -0
  124. package/dist/harness/cli/args.js +372 -0
  125. package/dist/harness/cli/args.js.map +1 -0
  126. package/dist/harness/cli/attachments.d.ts +44 -0
  127. package/dist/harness/cli/attachments.d.ts.map +1 -0
  128. package/dist/harness/cli/attachments.js +176 -0
  129. package/dist/harness/cli/attachments.js.map +1 -0
  130. package/dist/harness/cli/canonical.d.ts +64 -0
  131. package/dist/harness/cli/canonical.d.ts.map +1 -0
  132. package/dist/harness/cli/canonical.js +455 -0
  133. package/dist/harness/cli/canonical.js.map +1 -0
  134. package/dist/harness/cli/claude-native-approvals.d.ts +45 -0
  135. package/dist/harness/cli/claude-native-approvals.d.ts.map +1 -0
  136. package/dist/harness/cli/claude-native-approvals.js +183 -0
  137. package/dist/harness/cli/claude-native-approvals.js.map +1 -0
  138. package/dist/harness/cli/command-grants.d.ts +23 -0
  139. package/dist/harness/cli/command-grants.d.ts.map +1 -0
  140. package/dist/harness/cli/command-grants.js +76 -0
  141. package/dist/harness/cli/command-grants.js.map +1 -0
  142. package/dist/harness/cli/config-command.d.ts +98 -0
  143. package/dist/harness/cli/config-command.d.ts.map +1 -0
  144. package/dist/harness/cli/config-command.js +561 -0
  145. package/dist/harness/cli/config-command.js.map +1 -0
  146. package/dist/harness/cli/config.d.ts +429 -0
  147. package/dist/harness/cli/config.d.ts.map +1 -0
  148. package/dist/harness/cli/config.js +504 -0
  149. package/dist/harness/cli/config.js.map +1 -0
  150. package/dist/harness/cli/conversation.d.ts +35 -0
  151. package/dist/harness/cli/conversation.d.ts.map +1 -0
  152. package/dist/harness/cli/conversation.js +3755 -0
  153. package/dist/harness/cli/conversation.js.map +1 -0
  154. package/dist/harness/cli/doctor.d.ts +32 -0
  155. package/dist/harness/cli/doctor.d.ts.map +1 -0
  156. package/dist/harness/cli/doctor.js +329 -0
  157. package/dist/harness/cli/doctor.js.map +1 -0
  158. package/dist/harness/cli/effort-command.d.ts +41 -0
  159. package/dist/harness/cli/effort-command.d.ts.map +1 -0
  160. package/dist/harness/cli/effort-command.js +133 -0
  161. package/dist/harness/cli/effort-command.js.map +1 -0
  162. package/dist/harness/cli/extensions/agents.d.ts +40 -0
  163. package/dist/harness/cli/extensions/agents.d.ts.map +1 -0
  164. package/dist/harness/cli/extensions/agents.js +66 -0
  165. package/dist/harness/cli/extensions/agents.js.map +1 -0
  166. package/dist/harness/cli/extensions/catalog.d.ts +134 -0
  167. package/dist/harness/cli/extensions/catalog.d.ts.map +1 -0
  168. package/dist/harness/cli/extensions/catalog.js +213 -0
  169. package/dist/harness/cli/extensions/catalog.js.map +1 -0
  170. package/dist/harness/cli/extensions/claude-home.d.ts +33 -0
  171. package/dist/harness/cli/extensions/claude-home.d.ts.map +1 -0
  172. package/dist/harness/cli/extensions/claude-home.js +78 -0
  173. package/dist/harness/cli/extensions/claude-home.js.map +1 -0
  174. package/dist/harness/cli/extensions/controller.d.ts +51 -0
  175. package/dist/harness/cli/extensions/controller.d.ts.map +1 -0
  176. package/dist/harness/cli/extensions/controller.js +67 -0
  177. package/dist/harness/cli/extensions/controller.js.map +1 -0
  178. package/dist/harness/cli/extensions/hooks.d.ts +164 -0
  179. package/dist/harness/cli/extensions/hooks.d.ts.map +1 -0
  180. package/dist/harness/cli/extensions/hooks.js +442 -0
  181. package/dist/harness/cli/extensions/hooks.js.map +1 -0
  182. package/dist/harness/cli/extensions/index.d.ts +10 -0
  183. package/dist/harness/cli/extensions/index.d.ts.map +1 -0
  184. package/dist/harness/cli/extensions/index.js +10 -0
  185. package/dist/harness/cli/extensions/index.js.map +1 -0
  186. package/dist/harness/cli/extensions/markdown.d.ts +46 -0
  187. package/dist/harness/cli/extensions/markdown.d.ts.map +1 -0
  188. package/dist/harness/cli/extensions/markdown.js +139 -0
  189. package/dist/harness/cli/extensions/markdown.js.map +1 -0
  190. package/dist/harness/cli/extensions/plugins.d.ts +160 -0
  191. package/dist/harness/cli/extensions/plugins.d.ts.map +1 -0
  192. package/dist/harness/cli/extensions/plugins.js +645 -0
  193. package/dist/harness/cli/extensions/plugins.js.map +1 -0
  194. package/dist/harness/cli/extensions/tool-hooks.d.ts +28 -0
  195. package/dist/harness/cli/extensions/tool-hooks.d.ts.map +1 -0
  196. package/dist/harness/cli/extensions/tool-hooks.js +63 -0
  197. package/dist/harness/cli/extensions/tool-hooks.js.map +1 -0
  198. package/dist/harness/cli/extensions-command.d.ts +64 -0
  199. package/dist/harness/cli/extensions-command.d.ts.map +1 -0
  200. package/dist/harness/cli/extensions-command.js +639 -0
  201. package/dist/harness/cli/extensions-command.js.map +1 -0
  202. package/dist/harness/cli/help.d.ts +9 -0
  203. package/dist/harness/cli/help.d.ts.map +1 -0
  204. package/dist/harness/cli/help.js +191 -0
  205. package/dist/harness/cli/help.js.map +1 -0
  206. package/dist/harness/cli/index.d.ts +52 -0
  207. package/dist/harness/cli/index.d.ts.map +1 -0
  208. package/dist/harness/cli/index.js +297 -0
  209. package/dist/harness/cli/index.js.map +1 -0
  210. package/dist/harness/cli/inspect.d.ts +40 -0
  211. package/dist/harness/cli/inspect.d.ts.map +1 -0
  212. package/dist/harness/cli/inspect.js +185 -0
  213. package/dist/harness/cli/inspect.js.map +1 -0
  214. package/dist/harness/cli/mcp-command.d.ts +61 -0
  215. package/dist/harness/cli/mcp-command.d.ts.map +1 -0
  216. package/dist/harness/cli/mcp-command.js +519 -0
  217. package/dist/harness/cli/mcp-command.js.map +1 -0
  218. package/dist/harness/cli/memory-desk.d.ts +28 -0
  219. package/dist/harness/cli/memory-desk.d.ts.map +1 -0
  220. package/dist/harness/cli/memory-desk.js +275 -0
  221. package/dist/harness/cli/memory-desk.js.map +1 -0
  222. package/dist/harness/cli/model-picker.d.ts +44 -0
  223. package/dist/harness/cli/model-picker.d.ts.map +1 -0
  224. package/dist/harness/cli/model-picker.js +267 -0
  225. package/dist/harness/cli/model-picker.js.map +1 -0
  226. package/dist/harness/cli/onboarding.d.ts +29 -0
  227. package/dist/harness/cli/onboarding.d.ts.map +1 -0
  228. package/dist/harness/cli/onboarding.js +101 -0
  229. package/dist/harness/cli/onboarding.js.map +1 -0
  230. package/dist/harness/cli/orchestrate-tool.d.ts +60 -0
  231. package/dist/harness/cli/orchestrate-tool.d.ts.map +1 -0
  232. package/dist/harness/cli/orchestrate-tool.js +167 -0
  233. package/dist/harness/cli/orchestrate-tool.js.map +1 -0
  234. package/dist/harness/cli/orchestration-view.d.ts +57 -0
  235. package/dist/harness/cli/orchestration-view.d.ts.map +1 -0
  236. package/dist/harness/cli/orchestration-view.js +371 -0
  237. package/dist/harness/cli/orchestration-view.js.map +1 -0
  238. package/dist/harness/cli/outcome.d.ts +11 -0
  239. package/dist/harness/cli/outcome.d.ts.map +1 -0
  240. package/dist/harness/cli/outcome.js +70 -0
  241. package/dist/harness/cli/outcome.js.map +1 -0
  242. package/dist/harness/cli/panels/common.d.ts +30 -0
  243. package/dist/harness/cli/panels/common.d.ts.map +1 -0
  244. package/dist/harness/cli/panels/common.js +143 -0
  245. package/dist/harness/cli/panels/common.js.map +1 -0
  246. package/dist/harness/cli/panels/mcp.d.ts +18 -0
  247. package/dist/harness/cli/panels/mcp.d.ts.map +1 -0
  248. package/dist/harness/cli/panels/mcp.js +132 -0
  249. package/dist/harness/cli/panels/mcp.js.map +1 -0
  250. package/dist/harness/cli/panels/plugins.d.ts +8 -0
  251. package/dist/harness/cli/panels/plugins.d.ts.map +1 -0
  252. package/dist/harness/cli/panels/plugins.js +114 -0
  253. package/dist/harness/cli/panels/plugins.js.map +1 -0
  254. package/dist/harness/cli/panels/session.d.ts +54 -0
  255. package/dist/harness/cli/panels/session.d.ts.map +1 -0
  256. package/dist/harness/cli/panels/session.js +221 -0
  257. package/dist/harness/cli/panels/session.js.map +1 -0
  258. package/dist/harness/cli/panels/skills.d.ts +16 -0
  259. package/dist/harness/cli/panels/skills.d.ts.map +1 -0
  260. package/dist/harness/cli/panels/skills.js +146 -0
  261. package/dist/harness/cli/panels/skills.js.map +1 -0
  262. package/dist/harness/cli/project-profile.d.ts +87 -0
  263. package/dist/harness/cli/project-profile.d.ts.map +1 -0
  264. package/dist/harness/cli/project-profile.js +709 -0
  265. package/dist/harness/cli/project-profile.js.map +1 -0
  266. package/dist/harness/cli/project-scope.d.ts +2 -0
  267. package/dist/harness/cli/project-scope.d.ts.map +1 -0
  268. package/dist/harness/cli/project-scope.js +55 -0
  269. package/dist/harness/cli/project-scope.js.map +1 -0
  270. package/dist/harness/cli/renderers.d.ts +75 -0
  271. package/dist/harness/cli/renderers.d.ts.map +1 -0
  272. package/dist/harness/cli/renderers.js +162 -0
  273. package/dist/harness/cli/renderers.js.map +1 -0
  274. package/dist/harness/cli/rewind.d.ts +54 -0
  275. package/dist/harness/cli/rewind.d.ts.map +1 -0
  276. package/dist/harness/cli/rewind.js +80 -0
  277. package/dist/harness/cli/rewind.js.map +1 -0
  278. package/dist/harness/cli/role-policy.d.ts +8 -0
  279. package/dist/harness/cli/role-policy.d.ts.map +1 -0
  280. package/dist/harness/cli/role-policy.js +53 -0
  281. package/dist/harness/cli/role-policy.js.map +1 -0
  282. package/dist/harness/cli/run-summary.d.ts +16 -0
  283. package/dist/harness/cli/run-summary.d.ts.map +1 -0
  284. package/dist/harness/cli/run-summary.js +105 -0
  285. package/dist/harness/cli/run-summary.js.map +1 -0
  286. package/dist/harness/cli/runtime.d.ts +182 -0
  287. package/dist/harness/cli/runtime.d.ts.map +1 -0
  288. package/dist/harness/cli/runtime.js +999 -0
  289. package/dist/harness/cli/runtime.js.map +1 -0
  290. package/dist/harness/cli/scripted-script.d.ts +50 -0
  291. package/dist/harness/cli/scripted-script.d.ts.map +1 -0
  292. package/dist/harness/cli/scripted-script.js +110 -0
  293. package/dist/harness/cli/scripted-script.js.map +1 -0
  294. package/dist/harness/cli/session-git.d.ts +76 -0
  295. package/dist/harness/cli/session-git.d.ts.map +1 -0
  296. package/dist/harness/cli/session-git.js +146 -0
  297. package/dist/harness/cli/session-git.js.map +1 -0
  298. package/dist/harness/cli/session.d.ts +22 -0
  299. package/dist/harness/cli/session.d.ts.map +1 -0
  300. package/dist/harness/cli/session.js +203 -0
  301. package/dist/harness/cli/session.js.map +1 -0
  302. package/dist/harness/cli/slash-commands.d.ts +85 -0
  303. package/dist/harness/cli/slash-commands.d.ts.map +1 -0
  304. package/dist/harness/cli/slash-commands.js +114 -0
  305. package/dist/harness/cli/slash-commands.js.map +1 -0
  306. package/dist/harness/cli/startup-notices.d.ts +32 -0
  307. package/dist/harness/cli/startup-notices.d.ts.map +1 -0
  308. package/dist/harness/cli/startup-notices.js +93 -0
  309. package/dist/harness/cli/startup-notices.js.map +1 -0
  310. package/dist/harness/cli/terminal.d.ts +32 -0
  311. package/dist/harness/cli/terminal.d.ts.map +1 -0
  312. package/dist/harness/cli/terminal.js +21 -0
  313. package/dist/harness/cli/terminal.js.map +1 -0
  314. package/dist/harness/cli/transparency.d.ts +39 -0
  315. package/dist/harness/cli/transparency.d.ts.map +1 -0
  316. package/dist/harness/cli/transparency.js +491 -0
  317. package/dist/harness/cli/transparency.js.map +1 -0
  318. package/dist/harness/cli/trust.d.ts +40 -0
  319. package/dist/harness/cli/trust.d.ts.map +1 -0
  320. package/dist/harness/cli/trust.js +133 -0
  321. package/dist/harness/cli/trust.js.map +1 -0
  322. package/dist/harness/cli/usage-stats.d.ts +57 -0
  323. package/dist/harness/cli/usage-stats.d.ts.map +1 -0
  324. package/dist/harness/cli/usage-stats.js +341 -0
  325. package/dist/harness/cli/usage-stats.js.map +1 -0
  326. package/dist/harness/context/compaction.d.ts +69 -0
  327. package/dist/harness/context/compaction.d.ts.map +1 -0
  328. package/dist/harness/context/compaction.js +149 -0
  329. package/dist/harness/context/compaction.js.map +1 -0
  330. package/dist/harness/context/context-builder.d.ts +76 -0
  331. package/dist/harness/context/context-builder.d.ts.map +1 -0
  332. package/dist/harness/context/context-builder.js +367 -0
  333. package/dist/harness/context/context-builder.js.map +1 -0
  334. package/dist/harness/context/history.d.ts +31 -0
  335. package/dist/harness/context/history.d.ts.map +1 -0
  336. package/dist/harness/context/history.js +105 -0
  337. package/dist/harness/context/history.js.map +1 -0
  338. package/dist/harness/context/index.d.ts +11 -0
  339. package/dist/harness/context/index.d.ts.map +1 -0
  340. package/dist/harness/context/index.js +9 -0
  341. package/dist/harness/context/index.js.map +1 -0
  342. package/dist/harness/context/instructions.d.ts +30 -0
  343. package/dist/harness/context/instructions.d.ts.map +1 -0
  344. package/dist/harness/context/instructions.js +82 -0
  345. package/dist/harness/context/instructions.js.map +1 -0
  346. package/dist/harness/context/model-view.d.ts +46 -0
  347. package/dist/harness/context/model-view.d.ts.map +1 -0
  348. package/dist/harness/context/model-view.js +136 -0
  349. package/dist/harness/context/model-view.js.map +1 -0
  350. package/dist/harness/context/scope.d.ts +9 -0
  351. package/dist/harness/context/scope.d.ts.map +1 -0
  352. package/dist/harness/context/scope.js +50 -0
  353. package/dist/harness/context/scope.js.map +1 -0
  354. package/dist/harness/context/skills.d.ts +75 -0
  355. package/dist/harness/context/skills.d.ts.map +1 -0
  356. package/dist/harness/context/skills.js +109 -0
  357. package/dist/harness/context/skills.js.map +1 -0
  358. package/dist/harness/context/tokens.d.ts +10 -0
  359. package/dist/harness/context/tokens.d.ts.map +1 -0
  360. package/dist/harness/context/tokens.js +39 -0
  361. package/dist/harness/context/tokens.js.map +1 -0
  362. package/dist/harness/contracts/auth.d.ts +214 -0
  363. package/dist/harness/contracts/auth.d.ts.map +1 -0
  364. package/dist/harness/contracts/auth.js +120 -0
  365. package/dist/harness/contracts/auth.js.map +1 -0
  366. package/dist/harness/contracts/choice.d.ts +115 -0
  367. package/dist/harness/contracts/choice.d.ts.map +1 -0
  368. package/dist/harness/contracts/choice.js +115 -0
  369. package/dist/harness/contracts/choice.js.map +1 -0
  370. package/dist/harness/contracts/common.d.ts +119 -0
  371. package/dist/harness/contracts/common.d.ts.map +1 -0
  372. package/dist/harness/contracts/common.js +66 -0
  373. package/dist/harness/contracts/common.js.map +1 -0
  374. package/dist/harness/contracts/digest.d.ts +59 -0
  375. package/dist/harness/contracts/digest.d.ts.map +1 -0
  376. package/dist/harness/contracts/digest.js +88 -0
  377. package/dist/harness/contracts/digest.js.map +1 -0
  378. package/dist/harness/contracts/errors.d.ts +70 -0
  379. package/dist/harness/contracts/errors.d.ts.map +1 -0
  380. package/dist/harness/contracts/errors.js +87 -0
  381. package/dist/harness/contracts/errors.js.map +1 -0
  382. package/dist/harness/contracts/events.d.ts +3094 -0
  383. package/dist/harness/contracts/events.d.ts.map +1 -0
  384. package/dist/harness/contracts/events.js +426 -0
  385. package/dist/harness/contracts/events.js.map +1 -0
  386. package/dist/harness/contracts/evidence.d.ts +286 -0
  387. package/dist/harness/contracts/evidence.d.ts.map +1 -0
  388. package/dist/harness/contracts/evidence.js +234 -0
  389. package/dist/harness/contracts/evidence.js.map +1 -0
  390. package/dist/harness/contracts/ids.d.ts +77 -0
  391. package/dist/harness/contracts/ids.d.ts.map +1 -0
  392. package/dist/harness/contracts/ids.js +126 -0
  393. package/dist/harness/contracts/ids.js.map +1 -0
  394. package/dist/harness/contracts/index.d.ts +23 -0
  395. package/dist/harness/contracts/index.d.ts.map +1 -0
  396. package/dist/harness/contracts/index.js +23 -0
  397. package/dist/harness/contracts/index.js.map +1 -0
  398. package/dist/harness/contracts/jsonl.d.ts +3386 -0
  399. package/dist/harness/contracts/jsonl.d.ts.map +1 -0
  400. package/dist/harness/contracts/jsonl.js +87 -0
  401. package/dist/harness/contracts/jsonl.js.map +1 -0
  402. package/dist/harness/contracts/memory.d.ts +271 -0
  403. package/dist/harness/contracts/memory.d.ts.map +1 -0
  404. package/dist/harness/contracts/memory.js +152 -0
  405. package/dist/harness/contracts/memory.js.map +1 -0
  406. package/dist/harness/contracts/model.d.ts +1201 -0
  407. package/dist/harness/contracts/model.d.ts.map +1 -0
  408. package/dist/harness/contracts/model.js +386 -0
  409. package/dist/harness/contracts/model.js.map +1 -0
  410. package/dist/harness/contracts/packets.d.ts +945 -0
  411. package/dist/harness/contracts/packets.d.ts.map +1 -0
  412. package/dist/harness/contracts/packets.js +535 -0
  413. package/dist/harness/contracts/packets.js.map +1 -0
  414. package/dist/harness/contracts/panel.d.ts +140 -0
  415. package/dist/harness/contracts/panel.d.ts.map +1 -0
  416. package/dist/harness/contracts/panel.js +19 -0
  417. package/dist/harness/contracts/panel.js.map +1 -0
  418. package/dist/harness/contracts/paths.d.ts +58 -0
  419. package/dist/harness/contracts/paths.d.ts.map +1 -0
  420. package/dist/harness/contracts/paths.js +204 -0
  421. package/dist/harness/contracts/paths.js.map +1 -0
  422. package/dist/harness/contracts/policy.d.ts +459 -0
  423. package/dist/harness/contracts/policy.d.ts.map +1 -0
  424. package/dist/harness/contracts/policy.js +338 -0
  425. package/dist/harness/contracts/policy.js.map +1 -0
  426. package/dist/harness/contracts/projection.d.ts +111 -0
  427. package/dist/harness/contracts/projection.d.ts.map +1 -0
  428. package/dist/harness/contracts/projection.js +2 -0
  429. package/dist/harness/contracts/projection.js.map +1 -0
  430. package/dist/harness/contracts/renderer.d.ts +291 -0
  431. package/dist/harness/contracts/renderer.d.ts.map +1 -0
  432. package/dist/harness/contracts/renderer.js +36 -0
  433. package/dist/harness/contracts/renderer.js.map +1 -0
  434. package/dist/harness/contracts/roles.d.ts +28 -0
  435. package/dist/harness/contracts/roles.d.ts.map +1 -0
  436. package/dist/harness/contracts/roles.js +36 -0
  437. package/dist/harness/contracts/roles.js.map +1 -0
  438. package/dist/harness/contracts/runtime.d.ts +283 -0
  439. package/dist/harness/contracts/runtime.d.ts.map +1 -0
  440. package/dist/harness/contracts/runtime.js +13 -0
  441. package/dist/harness/contracts/runtime.js.map +1 -0
  442. package/dist/harness/contracts/state.d.ts +124 -0
  443. package/dist/harness/contracts/state.d.ts.map +1 -0
  444. package/dist/harness/contracts/state.js +172 -0
  445. package/dist/harness/contracts/state.js.map +1 -0
  446. package/dist/harness/contracts/store.d.ts +117 -0
  447. package/dist/harness/contracts/store.d.ts.map +1 -0
  448. package/dist/harness/contracts/store.js +54 -0
  449. package/dist/harness/contracts/store.js.map +1 -0
  450. package/dist/harness/contracts/tools.d.ts +296 -0
  451. package/dist/harness/contracts/tools.d.ts.map +1 -0
  452. package/dist/harness/contracts/tools.js +117 -0
  453. package/dist/harness/contracts/tools.js.map +1 -0
  454. package/dist/harness/contracts/views.d.ts +468 -0
  455. package/dist/harness/contracts/views.d.ts.map +1 -0
  456. package/dist/harness/contracts/views.js +2 -0
  457. package/dist/harness/contracts/views.js.map +1 -0
  458. package/dist/harness/core/driver.d.ts +53 -0
  459. package/dist/harness/core/driver.d.ts.map +1 -0
  460. package/dist/harness/core/driver.js +566 -0
  461. package/dist/harness/core/driver.js.map +1 -0
  462. package/dist/harness/core/envelope.d.ts +11 -0
  463. package/dist/harness/core/envelope.d.ts.map +1 -0
  464. package/dist/harness/core/envelope.js +35 -0
  465. package/dist/harness/core/envelope.js.map +1 -0
  466. package/dist/harness/core/index.d.ts +7 -0
  467. package/dist/harness/core/index.d.ts.map +1 -0
  468. package/dist/harness/core/index.js +5 -0
  469. package/dist/harness/core/index.js.map +1 -0
  470. package/dist/harness/core/projection.d.ts +10 -0
  471. package/dist/harness/core/projection.d.ts.map +1 -0
  472. package/dist/harness/core/projection.js +268 -0
  473. package/dist/harness/core/projection.js.map +1 -0
  474. package/dist/harness/core/recovery.d.ts +14 -0
  475. package/dist/harness/core/recovery.d.ts.map +1 -0
  476. package/dist/harness/core/recovery.js +115 -0
  477. package/dist/harness/core/recovery.js.map +1 -0
  478. package/dist/harness/core/stream.d.ts +23 -0
  479. package/dist/harness/core/stream.d.ts.map +1 -0
  480. package/dist/harness/core/stream.js +102 -0
  481. package/dist/harness/core/stream.js.map +1 -0
  482. package/dist/harness/core/testing.d.ts +80 -0
  483. package/dist/harness/core/testing.d.ts.map +1 -0
  484. package/dist/harness/core/testing.js +258 -0
  485. package/dist/harness/core/testing.js.map +1 -0
  486. package/dist/harness/mcp/approvals.d.ts +14 -0
  487. package/dist/harness/mcp/approvals.d.ts.map +1 -0
  488. package/dist/harness/mcp/approvals.js +60 -0
  489. package/dist/harness/mcp/approvals.js.map +1 -0
  490. package/dist/harness/mcp/client.d.ts +74 -0
  491. package/dist/harness/mcp/client.d.ts.map +1 -0
  492. package/dist/harness/mcp/client.js +214 -0
  493. package/dist/harness/mcp/client.js.map +1 -0
  494. package/dist/harness/mcp/config.d.ts +156 -0
  495. package/dist/harness/mcp/config.d.ts.map +1 -0
  496. package/dist/harness/mcp/config.js +180 -0
  497. package/dist/harness/mcp/config.js.map +1 -0
  498. package/dist/harness/mcp/errors.d.ts +24 -0
  499. package/dist/harness/mcp/errors.d.ts.map +1 -0
  500. package/dist/harness/mcp/errors.js +67 -0
  501. package/dist/harness/mcp/errors.js.map +1 -0
  502. package/dist/harness/mcp/index.d.ts +9 -0
  503. package/dist/harness/mcp/index.d.ts.map +1 -0
  504. package/dist/harness/mcp/index.js +9 -0
  505. package/dist/harness/mcp/index.js.map +1 -0
  506. package/dist/harness/mcp/manager.d.ts +115 -0
  507. package/dist/harness/mcp/manager.d.ts.map +1 -0
  508. package/dist/harness/mcp/manager.js +361 -0
  509. package/dist/harness/mcp/manager.js.map +1 -0
  510. package/dist/harness/mcp/oauth.d.ts +54 -0
  511. package/dist/harness/mcp/oauth.d.ts.map +1 -0
  512. package/dist/harness/mcp/oauth.js +253 -0
  513. package/dist/harness/mcp/oauth.js.map +1 -0
  514. package/dist/harness/mcp/tools.d.ts +24 -0
  515. package/dist/harness/mcp/tools.d.ts.map +1 -0
  516. package/dist/harness/mcp/tools.js +142 -0
  517. package/dist/harness/mcp/tools.js.map +1 -0
  518. package/dist/harness/memory/bootstrap.d.ts +55 -0
  519. package/dist/harness/memory/bootstrap.d.ts.map +1 -0
  520. package/dist/harness/memory/bootstrap.js +124 -0
  521. package/dist/harness/memory/bootstrap.js.map +1 -0
  522. package/dist/harness/memory/desk.d.ts +74 -0
  523. package/dist/harness/memory/desk.d.ts.map +1 -0
  524. package/dist/harness/memory/desk.js +211 -0
  525. package/dist/harness/memory/desk.js.map +1 -0
  526. package/dist/harness/memory/graph.d.ts +24 -0
  527. package/dist/harness/memory/graph.d.ts.map +1 -0
  528. package/dist/harness/memory/graph.js +92 -0
  529. package/dist/harness/memory/graph.js.map +1 -0
  530. package/dist/harness/memory/index.d.ts +12 -0
  531. package/dist/harness/memory/index.d.ts.map +1 -0
  532. package/dist/harness/memory/index.js +10 -0
  533. package/dist/harness/memory/index.js.map +1 -0
  534. package/dist/harness/memory/markdown-memory-store.d.ts +122 -0
  535. package/dist/harness/memory/markdown-memory-store.d.ts.map +1 -0
  536. package/dist/harness/memory/markdown-memory-store.js +419 -0
  537. package/dist/harness/memory/markdown-memory-store.js.map +1 -0
  538. package/dist/harness/memory/memory-command.d.ts +22 -0
  539. package/dist/harness/memory/memory-command.d.ts.map +1 -0
  540. package/dist/harness/memory/memory-command.js +335 -0
  541. package/dist/harness/memory/memory-command.js.map +1 -0
  542. package/dist/harness/memory/memory-index.d.ts +71 -0
  543. package/dist/harness/memory/memory-index.d.ts.map +1 -0
  544. package/dist/harness/memory/memory-index.js +206 -0
  545. package/dist/harness/memory/memory-index.js.map +1 -0
  546. package/dist/harness/memory/note-format.d.ts +20 -0
  547. package/dist/harness/memory/note-format.d.ts.map +1 -0
  548. package/dist/harness/memory/note-format.js +69 -0
  549. package/dist/harness/memory/note-format.js.map +1 -0
  550. package/dist/harness/memory/obsidian.d.ts +14 -0
  551. package/dist/harness/memory/obsidian.d.ts.map +1 -0
  552. package/dist/harness/memory/obsidian.js +64 -0
  553. package/dist/harness/memory/obsidian.js.map +1 -0
  554. package/dist/harness/memory/redaction.d.ts +15 -0
  555. package/dist/harness/memory/redaction.d.ts.map +1 -0
  556. package/dist/harness/memory/redaction.js +64 -0
  557. package/dist/harness/memory/redaction.js.map +1 -0
  558. package/dist/harness/memory/relations.d.ts +29 -0
  559. package/dist/harness/memory/relations.d.ts.map +1 -0
  560. package/dist/harness/memory/relations.js +117 -0
  561. package/dist/harness/memory/relations.js.map +1 -0
  562. package/dist/harness/memory/vault.d.ts +35 -0
  563. package/dist/harness/memory/vault.d.ts.map +1 -0
  564. package/dist/harness/memory/vault.js +194 -0
  565. package/dist/harness/memory/vault.js.map +1 -0
  566. package/dist/harness/orchestration/approval.d.ts +29 -0
  567. package/dist/harness/orchestration/approval.d.ts.map +1 -0
  568. package/dist/harness/orchestration/approval.js +86 -0
  569. package/dist/harness/orchestration/approval.js.map +1 -0
  570. package/dist/harness/orchestration/artifact-review.d.ts +219 -0
  571. package/dist/harness/orchestration/artifact-review.d.ts.map +1 -0
  572. package/dist/harness/orchestration/artifact-review.js +319 -0
  573. package/dist/harness/orchestration/artifact-review.js.map +1 -0
  574. package/dist/harness/orchestration/attempt-log.d.ts +53 -0
  575. package/dist/harness/orchestration/attempt-log.d.ts.map +1 -0
  576. package/dist/harness/orchestration/attempt-log.js +138 -0
  577. package/dist/harness/orchestration/attempt-log.js.map +1 -0
  578. package/dist/harness/orchestration/budget.d.ts +110 -0
  579. package/dist/harness/orchestration/budget.d.ts.map +1 -0
  580. package/dist/harness/orchestration/budget.js +197 -0
  581. package/dist/harness/orchestration/budget.js.map +1 -0
  582. package/dist/harness/orchestration/capabilities.d.ts +6 -0
  583. package/dist/harness/orchestration/capabilities.d.ts.map +1 -0
  584. package/dist/harness/orchestration/capabilities.js +80 -0
  585. package/dist/harness/orchestration/capabilities.js.map +1 -0
  586. package/dist/harness/orchestration/claims.d.ts +148 -0
  587. package/dist/harness/orchestration/claims.d.ts.map +1 -0
  588. package/dist/harness/orchestration/claims.js +64 -0
  589. package/dist/harness/orchestration/claims.js.map +1 -0
  590. package/dist/harness/orchestration/control-plane.d.ts +18 -0
  591. package/dist/harness/orchestration/control-plane.d.ts.map +1 -0
  592. package/dist/harness/orchestration/control-plane.js +74 -0
  593. package/dist/harness/orchestration/control-plane.js.map +1 -0
  594. package/dist/harness/orchestration/coordinator.d.ts +132 -0
  595. package/dist/harness/orchestration/coordinator.d.ts.map +1 -0
  596. package/dist/harness/orchestration/coordinator.js +1628 -0
  597. package/dist/harness/orchestration/coordinator.js.map +1 -0
  598. package/dist/harness/orchestration/delegation.d.ts +80 -0
  599. package/dist/harness/orchestration/delegation.d.ts.map +1 -0
  600. package/dist/harness/orchestration/delegation.js +81 -0
  601. package/dist/harness/orchestration/delegation.js.map +1 -0
  602. package/dist/harness/orchestration/evidence.d.ts +124 -0
  603. package/dist/harness/orchestration/evidence.d.ts.map +1 -0
  604. package/dist/harness/orchestration/evidence.js +576 -0
  605. package/dist/harness/orchestration/evidence.js.map +1 -0
  606. package/dist/harness/orchestration/factories.d.ts +13 -0
  607. package/dist/harness/orchestration/factories.d.ts.map +1 -0
  608. package/dist/harness/orchestration/factories.js +20 -0
  609. package/dist/harness/orchestration/factories.js.map +1 -0
  610. package/dist/harness/orchestration/git.d.ts +77 -0
  611. package/dist/harness/orchestration/git.d.ts.map +1 -0
  612. package/dist/harness/orchestration/git.js +214 -0
  613. package/dist/harness/orchestration/git.js.map +1 -0
  614. package/dist/harness/orchestration/index.d.ts +25 -0
  615. package/dist/harness/orchestration/index.d.ts.map +1 -0
  616. package/dist/harness/orchestration/index.js +23 -0
  617. package/dist/harness/orchestration/index.js.map +1 -0
  618. package/dist/harness/orchestration/isolation.d.ts +181 -0
  619. package/dist/harness/orchestration/isolation.d.ts.map +1 -0
  620. package/dist/harness/orchestration/isolation.js +1357 -0
  621. package/dist/harness/orchestration/isolation.js.map +1 -0
  622. package/dist/harness/orchestration/paths.d.ts +18 -0
  623. package/dist/harness/orchestration/paths.d.ts.map +1 -0
  624. package/dist/harness/orchestration/paths.js +56 -0
  625. package/dist/harness/orchestration/paths.js.map +1 -0
  626. package/dist/harness/orchestration/plan.d.ts +219 -0
  627. package/dist/harness/orchestration/plan.d.ts.map +1 -0
  628. package/dist/harness/orchestration/plan.js +537 -0
  629. package/dist/harness/orchestration/plan.js.map +1 -0
  630. package/dist/harness/orchestration/planner.d.ts +137 -0
  631. package/dist/harness/orchestration/planner.d.ts.map +1 -0
  632. package/dist/harness/orchestration/planner.js +162 -0
  633. package/dist/harness/orchestration/planner.js.map +1 -0
  634. package/dist/harness/orchestration/recorder.d.ts +23 -0
  635. package/dist/harness/orchestration/recorder.d.ts.map +1 -0
  636. package/dist/harness/orchestration/recorder.js +50 -0
  637. package/dist/harness/orchestration/recorder.js.map +1 -0
  638. package/dist/harness/orchestration/scheduler.d.ts +40 -0
  639. package/dist/harness/orchestration/scheduler.d.ts.map +1 -0
  640. package/dist/harness/orchestration/scheduler.js +157 -0
  641. package/dist/harness/orchestration/scheduler.js.map +1 -0
  642. package/dist/harness/orchestration/testing.d.ts +233 -0
  643. package/dist/harness/orchestration/testing.d.ts.map +1 -0
  644. package/dist/harness/orchestration/testing.js +734 -0
  645. package/dist/harness/orchestration/testing.js.map +1 -0
  646. package/dist/harness/orchestration/verification-preflight.d.ts +46 -0
  647. package/dist/harness/orchestration/verification-preflight.d.ts.map +1 -0
  648. package/dist/harness/orchestration/verification-preflight.js +104 -0
  649. package/dist/harness/orchestration/verification-preflight.js.map +1 -0
  650. package/dist/harness/orchestration/worker-control.d.ts +64 -0
  651. package/dist/harness/orchestration/worker-control.d.ts.map +1 -0
  652. package/dist/harness/orchestration/worker-control.js +199 -0
  653. package/dist/harness/orchestration/worker-control.js.map +1 -0
  654. package/dist/harness/orchestration/worker-manager.d.ts +225 -0
  655. package/dist/harness/orchestration/worker-manager.d.ts.map +1 -0
  656. package/dist/harness/orchestration/worker-manager.js +1347 -0
  657. package/dist/harness/orchestration/worker-manager.js.map +1 -0
  658. package/dist/harness/orchestration/workspace-digest.d.ts +39 -0
  659. package/dist/harness/orchestration/workspace-digest.d.ts.map +1 -0
  660. package/dist/harness/orchestration/workspace-digest.js +147 -0
  661. package/dist/harness/orchestration/workspace-digest.js.map +1 -0
  662. package/dist/harness/policy/approval-broker.d.ts +13 -0
  663. package/dist/harness/policy/approval-broker.d.ts.map +1 -0
  664. package/dist/harness/policy/approval-broker.js +26 -0
  665. package/dist/harness/policy/approval-broker.js.map +1 -0
  666. package/dist/harness/policy/command-classifier.d.ts +29 -0
  667. package/dist/harness/policy/command-classifier.d.ts.map +1 -0
  668. package/dist/harness/policy/command-classifier.js +459 -0
  669. package/dist/harness/policy/command-classifier.js.map +1 -0
  670. package/dist/harness/policy/command-rules.d.ts +56 -0
  671. package/dist/harness/policy/command-rules.d.ts.map +1 -0
  672. package/dist/harness/policy/command-rules.js +588 -0
  673. package/dist/harness/policy/command-rules.js.map +1 -0
  674. package/dist/harness/policy/config.d.ts +27 -0
  675. package/dist/harness/policy/config.d.ts.map +1 -0
  676. package/dist/harness/policy/config.js +37 -0
  677. package/dist/harness/policy/config.js.map +1 -0
  678. package/dist/harness/policy/engine.d.ts +67 -0
  679. package/dist/harness/policy/engine.d.ts.map +1 -0
  680. package/dist/harness/policy/engine.js +483 -0
  681. package/dist/harness/policy/engine.js.map +1 -0
  682. package/dist/harness/policy/exec-allowlist.d.ts +66 -0
  683. package/dist/harness/policy/exec-allowlist.d.ts.map +1 -0
  684. package/dist/harness/policy/exec-allowlist.js +449 -0
  685. package/dist/harness/policy/exec-allowlist.js.map +1 -0
  686. package/dist/harness/policy/index.d.ts +10 -0
  687. package/dist/harness/policy/index.d.ts.map +1 -0
  688. package/dist/harness/policy/index.js +8 -0
  689. package/dist/harness/policy/index.js.map +1 -0
  690. package/dist/harness/policy/secret-patterns.d.ts +12 -0
  691. package/dist/harness/policy/secret-patterns.d.ts.map +1 -0
  692. package/dist/harness/policy/secret-patterns.js +48 -0
  693. package/dist/harness/policy/secret-patterns.js.map +1 -0
  694. package/dist/harness/policy/shell-parser.d.ts +40 -0
  695. package/dist/harness/policy/shell-parser.d.ts.map +1 -0
  696. package/dist/harness/policy/shell-parser.js +278 -0
  697. package/dist/harness/policy/shell-parser.js.map +1 -0
  698. package/dist/harness/policy/workspace-trust.d.ts +37 -0
  699. package/dist/harness/policy/workspace-trust.d.ts.map +1 -0
  700. package/dist/harness/policy/workspace-trust.js +164 -0
  701. package/dist/harness/policy/workspace-trust.js.map +1 -0
  702. package/dist/harness/providers/anthropic-messages.d.ts +48 -0
  703. package/dist/harness/providers/anthropic-messages.d.ts.map +1 -0
  704. package/dist/harness/providers/anthropic-messages.js +360 -0
  705. package/dist/harness/providers/anthropic-messages.js.map +1 -0
  706. package/dist/harness/providers/assembler.d.ts +29 -0
  707. package/dist/harness/providers/assembler.d.ts.map +1 -0
  708. package/dist/harness/providers/assembler.js +130 -0
  709. package/dist/harness/providers/assembler.js.map +1 -0
  710. package/dist/harness/providers/authenticated-stream.d.ts +9 -0
  711. package/dist/harness/providers/authenticated-stream.d.ts.map +1 -0
  712. package/dist/harness/providers/authenticated-stream.js +81 -0
  713. package/dist/harness/providers/authenticated-stream.js.map +1 -0
  714. package/dist/harness/providers/catalog.d.ts +75 -0
  715. package/dist/harness/providers/catalog.d.ts.map +1 -0
  716. package/dist/harness/providers/catalog.js +124 -0
  717. package/dist/harness/providers/catalog.js.map +1 -0
  718. package/dist/harness/providers/claude-code/adapter.d.ts +96 -0
  719. package/dist/harness/providers/claude-code/adapter.d.ts.map +1 -0
  720. package/dist/harness/providers/claude-code/adapter.js +863 -0
  721. package/dist/harness/providers/claude-code/adapter.js.map +1 -0
  722. package/dist/harness/providers/claude-code/mcp-relay.d.ts +2 -0
  723. package/dist/harness/providers/claude-code/mcp-relay.d.ts.map +1 -0
  724. package/dist/harness/providers/claude-code/mcp-relay.js +27 -0
  725. package/dist/harness/providers/claude-code/mcp-relay.js.map +1 -0
  726. package/dist/harness/providers/claude-code/mcp-server.d.ts +40 -0
  727. package/dist/harness/providers/claude-code/mcp-server.d.ts.map +1 -0
  728. package/dist/harness/providers/claude-code/mcp-server.js +171 -0
  729. package/dist/harness/providers/claude-code/mcp-server.js.map +1 -0
  730. package/dist/harness/providers/claude-code/native.d.ts +36 -0
  731. package/dist/harness/providers/claude-code/native.d.ts.map +1 -0
  732. package/dist/harness/providers/claude-code/native.js +153 -0
  733. package/dist/harness/providers/claude-code/native.js.map +1 -0
  734. package/dist/harness/providers/claude-code/process.d.ts +48 -0
  735. package/dist/harness/providers/claude-code/process.d.ts.map +1 -0
  736. package/dist/harness/providers/claude-code/process.js +128 -0
  737. package/dist/harness/providers/claude-code/process.js.map +1 -0
  738. package/dist/harness/providers/codex-app-server.d.ts +8 -0
  739. package/dist/harness/providers/codex-app-server.d.ts.map +1 -0
  740. package/dist/harness/providers/codex-app-server.js +45 -0
  741. package/dist/harness/providers/codex-app-server.js.map +1 -0
  742. package/dist/harness/providers/effort.d.ts +51 -0
  743. package/dist/harness/providers/effort.d.ts.map +1 -0
  744. package/dist/harness/providers/effort.js +108 -0
  745. package/dist/harness/providers/effort.js.map +1 -0
  746. package/dist/harness/providers/errors.d.ts +34 -0
  747. package/dist/harness/providers/errors.d.ts.map +1 -0
  748. package/dist/harness/providers/errors.js +143 -0
  749. package/dist/harness/providers/errors.js.map +1 -0
  750. package/dist/harness/providers/grammar.d.ts +17 -0
  751. package/dist/harness/providers/grammar.d.ts.map +1 -0
  752. package/dist/harness/providers/grammar.js +89 -0
  753. package/dist/harness/providers/grammar.js.map +1 -0
  754. package/dist/harness/providers/http-stream.d.ts +40 -0
  755. package/dist/harness/providers/http-stream.d.ts.map +1 -0
  756. package/dist/harness/providers/http-stream.js +134 -0
  757. package/dist/harness/providers/http-stream.js.map +1 -0
  758. package/dist/harness/providers/index.d.ts +20 -0
  759. package/dist/harness/providers/index.d.ts.map +1 -0
  760. package/dist/harness/providers/index.js +17 -0
  761. package/dist/harness/providers/index.js.map +1 -0
  762. package/dist/harness/providers/responses.d.ts +36 -0
  763. package/dist/harness/providers/responses.d.ts.map +1 -0
  764. package/dist/harness/providers/responses.js +435 -0
  765. package/dist/harness/providers/responses.js.map +1 -0
  766. package/dist/harness/providers/reviewer-route.d.ts +14 -0
  767. package/dist/harness/providers/reviewer-route.d.ts.map +1 -0
  768. package/dist/harness/providers/reviewer-route.js +31 -0
  769. package/dist/harness/providers/reviewer-route.js.map +1 -0
  770. package/dist/harness/providers/router.d.ts +50 -0
  771. package/dist/harness/providers/router.d.ts.map +1 -0
  772. package/dist/harness/providers/router.js +295 -0
  773. package/dist/harness/providers/router.js.map +1 -0
  774. package/dist/harness/providers/scripted.d.ts +22 -0
  775. package/dist/harness/providers/scripted.d.ts.map +1 -0
  776. package/dist/harness/providers/scripted.js +111 -0
  777. package/dist/harness/providers/scripted.js.map +1 -0
  778. package/dist/harness/providers/sse.d.ts +13 -0
  779. package/dist/harness/providers/sse.d.ts.map +1 -0
  780. package/dist/harness/providers/sse.js +82 -0
  781. package/dist/harness/providers/sse.js.map +1 -0
  782. package/dist/harness/providers/testing.d.ts +35 -0
  783. package/dist/harness/providers/testing.d.ts.map +1 -0
  784. package/dist/harness/providers/testing.js +93 -0
  785. package/dist/harness/providers/testing.js.map +1 -0
  786. package/dist/harness/providers/web-search.d.ts +110 -0
  787. package/dist/harness/providers/web-search.d.ts.map +1 -0
  788. package/dist/harness/providers/web-search.js +476 -0
  789. package/dist/harness/providers/web-search.js.map +1 -0
  790. package/dist/harness/store/blob-store.d.ts +4 -0
  791. package/dist/harness/store/blob-store.d.ts.map +1 -0
  792. package/dist/harness/store/blob-store.js +79 -0
  793. package/dist/harness/store/blob-store.js.map +1 -0
  794. package/dist/harness/store/durable-file.d.ts +17 -0
  795. package/dist/harness/store/durable-file.d.ts.map +1 -0
  796. package/dist/harness/store/durable-file.js +84 -0
  797. package/dist/harness/store/durable-file.js.map +1 -0
  798. package/dist/harness/store/event-store.d.ts +56 -0
  799. package/dist/harness/store/event-store.d.ts.map +1 -0
  800. package/dist/harness/store/event-store.js +210 -0
  801. package/dist/harness/store/event-store.js.map +1 -0
  802. package/dist/harness/store/index.d.ts +8 -0
  803. package/dist/harness/store/index.d.ts.map +1 -0
  804. package/dist/harness/store/index.js +4 -0
  805. package/dist/harness/store/index.js.map +1 -0
  806. package/dist/harness/store/layout.d.ts +12 -0
  807. package/dist/harness/store/layout.d.ts.map +1 -0
  808. package/dist/harness/store/layout.js +27 -0
  809. package/dist/harness/store/layout.js.map +1 -0
  810. package/dist/harness/store/lease.d.ts +37 -0
  811. package/dist/harness/store/lease.d.ts.map +1 -0
  812. package/dist/harness/store/lease.js +326 -0
  813. package/dist/harness/store/lease.js.map +1 -0
  814. package/dist/harness/store/segments.d.ts +49 -0
  815. package/dist/harness/store/segments.d.ts.map +1 -0
  816. package/dist/harness/store/segments.js +216 -0
  817. package/dist/harness/store/segments.js.map +1 -0
  818. package/dist/harness/store/session-store.d.ts +18 -0
  819. package/dist/harness/store/session-store.d.ts.map +1 -0
  820. package/dist/harness/store/session-store.js +319 -0
  821. package/dist/harness/store/session-store.js.map +1 -0
  822. package/dist/harness/tools/builtin/control-tools.d.ts +57 -0
  823. package/dist/harness/tools/builtin/control-tools.d.ts.map +1 -0
  824. package/dist/harness/tools/builtin/control-tools.js +72 -0
  825. package/dist/harness/tools/builtin/control-tools.js.map +1 -0
  826. package/dist/harness/tools/builtin/glob.d.ts +15 -0
  827. package/dist/harness/tools/builtin/glob.d.ts.map +1 -0
  828. package/dist/harness/tools/builtin/glob.js +164 -0
  829. package/dist/harness/tools/builtin/glob.js.map +1 -0
  830. package/dist/harness/tools/builtin/process-bg.d.ts +72 -0
  831. package/dist/harness/tools/builtin/process-bg.d.ts.map +1 -0
  832. package/dist/harness/tools/builtin/process-bg.js +360 -0
  833. package/dist/harness/tools/builtin/process-bg.js.map +1 -0
  834. package/dist/harness/tools/builtin/process-tools.d.ts +46 -0
  835. package/dist/harness/tools/builtin/process-tools.d.ts.map +1 -0
  836. package/dist/harness/tools/builtin/process-tools.js +263 -0
  837. package/dist/harness/tools/builtin/process-tools.js.map +1 -0
  838. package/dist/harness/tools/builtin/read-media.d.ts +10 -0
  839. package/dist/harness/tools/builtin/read-media.d.ts.map +1 -0
  840. package/dist/harness/tools/builtin/read-media.js +128 -0
  841. package/dist/harness/tools/builtin/read-media.js.map +1 -0
  842. package/dist/harness/tools/builtin/read-tools.d.ts +29 -0
  843. package/dist/harness/tools/builtin/read-tools.d.ts.map +1 -0
  844. package/dist/harness/tools/builtin/read-tools.js +329 -0
  845. package/dist/harness/tools/builtin/read-tools.js.map +1 -0
  846. package/dist/harness/tools/builtin/shared.d.ts +47 -0
  847. package/dist/harness/tools/builtin/shared.d.ts.map +1 -0
  848. package/dist/harness/tools/builtin/shared.js +94 -0
  849. package/dist/harness/tools/builtin/shared.js.map +1 -0
  850. package/dist/harness/tools/builtin/todo.d.ts +38 -0
  851. package/dist/harness/tools/builtin/todo.d.ts.map +1 -0
  852. package/dist/harness/tools/builtin/todo.js +55 -0
  853. package/dist/harness/tools/builtin/todo.js.map +1 -0
  854. package/dist/harness/tools/builtin/web-common.d.ts +18 -0
  855. package/dist/harness/tools/builtin/web-common.d.ts.map +1 -0
  856. package/dist/harness/tools/builtin/web-common.js +53 -0
  857. package/dist/harness/tools/builtin/web-common.js.map +1 -0
  858. package/dist/harness/tools/builtin/web-fetch.d.ts +33 -0
  859. package/dist/harness/tools/builtin/web-fetch.d.ts.map +1 -0
  860. package/dist/harness/tools/builtin/web-fetch.js +173 -0
  861. package/dist/harness/tools/builtin/web-fetch.js.map +1 -0
  862. package/dist/harness/tools/builtin/web-html.d.ts +14 -0
  863. package/dist/harness/tools/builtin/web-html.d.ts.map +1 -0
  864. package/dist/harness/tools/builtin/web-html.js +314 -0
  865. package/dist/harness/tools/builtin/web-html.js.map +1 -0
  866. package/dist/harness/tools/builtin/web-net.d.ts +59 -0
  867. package/dist/harness/tools/builtin/web-net.d.ts.map +1 -0
  868. package/dist/harness/tools/builtin/web-net.js +347 -0
  869. package/dist/harness/tools/builtin/web-net.js.map +1 -0
  870. package/dist/harness/tools/builtin/web-search.d.ts +40 -0
  871. package/dist/harness/tools/builtin/web-search.d.ts.map +1 -0
  872. package/dist/harness/tools/builtin/web-search.js +60 -0
  873. package/dist/harness/tools/builtin/web-search.js.map +1 -0
  874. package/dist/harness/tools/builtin/web-state.d.ts +67 -0
  875. package/dist/harness/tools/builtin/web-state.d.ts.map +1 -0
  876. package/dist/harness/tools/builtin/web-state.js +227 -0
  877. package/dist/harness/tools/builtin/web-state.js.map +1 -0
  878. package/dist/harness/tools/builtin/write-tools.d.ts +18 -0
  879. package/dist/harness/tools/builtin/write-tools.d.ts.map +1 -0
  880. package/dist/harness/tools/builtin/write-tools.js +335 -0
  881. package/dist/harness/tools/builtin/write-tools.js.map +1 -0
  882. package/dist/harness/tools/environment.d.ts +9 -0
  883. package/dist/harness/tools/environment.d.ts.map +1 -0
  884. package/dist/harness/tools/environment.js +59 -0
  885. package/dist/harness/tools/environment.js.map +1 -0
  886. package/dist/harness/tools/file-ledger.d.ts +9 -0
  887. package/dist/harness/tools/file-ledger.d.ts.map +1 -0
  888. package/dist/harness/tools/file-ledger.js +26 -0
  889. package/dist/harness/tools/file-ledger.js.map +1 -0
  890. package/dist/harness/tools/gateway.d.ts +52 -0
  891. package/dist/harness/tools/gateway.d.ts.map +1 -0
  892. package/dist/harness/tools/gateway.js +435 -0
  893. package/dist/harness/tools/gateway.js.map +1 -0
  894. package/dist/harness/tools/index.d.ts +20 -0
  895. package/dist/harness/tools/index.d.ts.map +1 -0
  896. package/dist/harness/tools/index.js +17 -0
  897. package/dist/harness/tools/index.js.map +1 -0
  898. package/dist/harness/tools/patch.d.ts +51 -0
  899. package/dist/harness/tools/patch.d.ts.map +1 -0
  900. package/dist/harness/tools/patch.js +450 -0
  901. package/dist/harness/tools/patch.js.map +1 -0
  902. package/dist/harness/tools/process.d.ts +28 -0
  903. package/dist/harness/tools/process.d.ts.map +1 -0
  904. package/dist/harness/tools/process.js +263 -0
  905. package/dist/harness/tools/process.js.map +1 -0
  906. package/dist/harness/tools/redaction.d.ts +26 -0
  907. package/dist/harness/tools/redaction.d.ts.map +1 -0
  908. package/dist/harness/tools/redaction.js +77 -0
  909. package/dist/harness/tools/redaction.js.map +1 -0
  910. package/dist/harness/tools/registry.d.ts +26 -0
  911. package/dist/harness/tools/registry.d.ts.map +1 -0
  912. package/dist/harness/tools/registry.js +57 -0
  913. package/dist/harness/tools/registry.js.map +1 -0
  914. package/dist/harness/tools/sandbox.d.ts +23 -0
  915. package/dist/harness/tools/sandbox.d.ts.map +1 -0
  916. package/dist/harness/tools/sandbox.js +117 -0
  917. package/dist/harness/tools/sandbox.js.map +1 -0
  918. package/dist/harness/tools/testing.d.ts +42 -0
  919. package/dist/harness/tools/testing.d.ts.map +1 -0
  920. package/dist/harness/tools/testing.js +143 -0
  921. package/dist/harness/tools/testing.js.map +1 -0
  922. package/dist/harness/tools/text-file.d.ts +38 -0
  923. package/dist/harness/tools/text-file.d.ts.map +1 -0
  924. package/dist/harness/tools/text-file.js +90 -0
  925. package/dist/harness/tools/text-file.js.map +1 -0
  926. package/dist/harness/tools/windows-launch.d.ts +52 -0
  927. package/dist/harness/tools/windows-launch.d.ts.map +1 -0
  928. package/dist/harness/tools/windows-launch.js +261 -0
  929. package/dist/harness/tools/windows-launch.js.map +1 -0
  930. package/dist/harness/tools/workspace-path.d.ts +34 -0
  931. package/dist/harness/tools/workspace-path.d.ts.map +1 -0
  932. package/dist/harness/tools/workspace-path.js +133 -0
  933. package/dist/harness/tools/workspace-path.js.map +1 -0
  934. package/dist/harness/tui/approvals.d.ts +44 -0
  935. package/dist/harness/tui/approvals.d.ts.map +1 -0
  936. package/dist/harness/tui/approvals.js +127 -0
  937. package/dist/harness/tui/approvals.js.map +1 -0
  938. package/dist/harness/tui/auth-interaction.d.ts +36 -0
  939. package/dist/harness/tui/auth-interaction.d.ts.map +1 -0
  940. package/dist/harness/tui/auth-interaction.js +76 -0
  941. package/dist/harness/tui/auth-interaction.js.map +1 -0
  942. package/dist/harness/tui/choice-modal.d.ts +58 -0
  943. package/dist/harness/tui/choice-modal.d.ts.map +1 -0
  944. package/dist/harness/tui/choice-modal.js +244 -0
  945. package/dist/harness/tui/choice-modal.js.map +1 -0
  946. package/dist/harness/tui/conversation-view.d.ts +250 -0
  947. package/dist/harness/tui/conversation-view.d.ts.map +1 -0
  948. package/dist/harness/tui/conversation-view.js +1115 -0
  949. package/dist/harness/tui/conversation-view.js.map +1 -0
  950. package/dist/harness/tui/describe.d.ts +21 -0
  951. package/dist/harness/tui/describe.d.ts.map +1 -0
  952. package/dist/harness/tui/describe.js +110 -0
  953. package/dist/harness/tui/describe.js.map +1 -0
  954. package/dist/harness/tui/index.d.ts +30 -0
  955. package/dist/harness/tui/index.d.ts.map +1 -0
  956. package/dist/harness/tui/index.js +26 -0
  957. package/dist/harness/tui/index.js.map +1 -0
  958. package/dist/harness/tui/input/attachments.d.ts +20 -0
  959. package/dist/harness/tui/input/attachments.d.ts.map +1 -0
  960. package/dist/harness/tui/input/attachments.js +100 -0
  961. package/dist/harness/tui/input/attachments.js.map +1 -0
  962. package/dist/harness/tui/input/autocomplete.d.ts +41 -0
  963. package/dist/harness/tui/input/autocomplete.d.ts.map +1 -0
  964. package/dist/harness/tui/input/autocomplete.js +85 -0
  965. package/dist/harness/tui/input/autocomplete.js.map +1 -0
  966. package/dist/harness/tui/input/clipboard.d.ts +44 -0
  967. package/dist/harness/tui/input/clipboard.d.ts.map +1 -0
  968. package/dist/harness/tui/input/clipboard.js +181 -0
  969. package/dist/harness/tui/input/clipboard.js.map +1 -0
  970. package/dist/harness/tui/input/commands.d.ts +23 -0
  971. package/dist/harness/tui/input/commands.d.ts.map +1 -0
  972. package/dist/harness/tui/input/commands.js +71 -0
  973. package/dist/harness/tui/input/commands.js.map +1 -0
  974. package/dist/harness/tui/input/file-index.d.ts +49 -0
  975. package/dist/harness/tui/input/file-index.d.ts.map +1 -0
  976. package/dist/harness/tui/input/file-index.js +170 -0
  977. package/dist/harness/tui/input/file-index.js.map +1 -0
  978. package/dist/harness/tui/input/index.d.ts +8 -0
  979. package/dist/harness/tui/input/index.d.ts.map +1 -0
  980. package/dist/harness/tui/input/index.js +8 -0
  981. package/dist/harness/tui/input/index.js.map +1 -0
  982. package/dist/harness/tui/input/mouse.d.ts +68 -0
  983. package/dist/harness/tui/input/mouse.d.ts.map +1 -0
  984. package/dist/harness/tui/input/mouse.js +141 -0
  985. package/dist/harness/tui/input/mouse.js.map +1 -0
  986. package/dist/harness/tui/interrupt.d.ts +25 -0
  987. package/dist/harness/tui/interrupt.d.ts.map +1 -0
  988. package/dist/harness/tui/interrupt.js +49 -0
  989. package/dist/harness/tui/interrupt.js.map +1 -0
  990. package/dist/harness/tui/jsonl-renderer.d.ts +68 -0
  991. package/dist/harness/tui/jsonl-renderer.d.ts.map +1 -0
  992. package/dist/harness/tui/jsonl-renderer.js +172 -0
  993. package/dist/harness/tui/jsonl-renderer.js.map +1 -0
  994. package/dist/harness/tui/line-source.d.ts +39 -0
  995. package/dist/harness/tui/line-source.d.ts.map +1 -0
  996. package/dist/harness/tui/line-source.js +137 -0
  997. package/dist/harness/tui/line-source.js.map +1 -0
  998. package/dist/harness/tui/logo-art.d.ts +9 -0
  999. package/dist/harness/tui/logo-art.d.ts.map +1 -0
  1000. package/dist/harness/tui/logo-art.js +24 -0
  1001. package/dist/harness/tui/logo-art.js.map +1 -0
  1002. package/dist/harness/tui/logo.d.ts +28 -0
  1003. package/dist/harness/tui/logo.d.ts.map +1 -0
  1004. package/dist/harness/tui/logo.js +88 -0
  1005. package/dist/harness/tui/logo.js.map +1 -0
  1006. package/dist/harness/tui/open-browser.d.ts +17 -0
  1007. package/dist/harness/tui/open-browser.d.ts.map +1 -0
  1008. package/dist/harness/tui/open-browser.js +45 -0
  1009. package/dist/harness/tui/open-browser.js.map +1 -0
  1010. package/dist/harness/tui/output-chunks.d.ts +8 -0
  1011. package/dist/harness/tui/output-chunks.d.ts.map +1 -0
  1012. package/dist/harness/tui/output-chunks.js +48 -0
  1013. package/dist/harness/tui/output-chunks.js.map +1 -0
  1014. package/dist/harness/tui/panels/index.d.ts +2 -0
  1015. package/dist/harness/tui/panels/index.d.ts.map +1 -0
  1016. package/dist/harness/tui/panels/index.js +2 -0
  1017. package/dist/harness/tui/panels/index.js.map +1 -0
  1018. package/dist/harness/tui/panels/panel-view.d.ts +98 -0
  1019. package/dist/harness/tui/panels/panel-view.d.ts.map +1 -0
  1020. package/dist/harness/tui/panels/panel-view.js +596 -0
  1021. package/dist/harness/tui/panels/panel-view.js.map +1 -0
  1022. package/dist/harness/tui/pi-tui-renderer.d.ts +363 -0
  1023. package/dist/harness/tui/pi-tui-renderer.d.ts.map +1 -0
  1024. package/dist/harness/tui/pi-tui-renderer.js +2696 -0
  1025. package/dist/harness/tui/pi-tui-renderer.js.map +1 -0
  1026. package/dist/harness/tui/plain-line-renderer.d.ts +102 -0
  1027. package/dist/harness/tui/plain-line-renderer.d.ts.map +1 -0
  1028. package/dist/harness/tui/plain-line-renderer.js +491 -0
  1029. package/dist/harness/tui/plain-line-renderer.js.map +1 -0
  1030. package/dist/harness/tui/render-queue.d.ts +34 -0
  1031. package/dist/harness/tui/render-queue.d.ts.map +1 -0
  1032. package/dist/harness/tui/render-queue.js +92 -0
  1033. package/dist/harness/tui/render-queue.js.map +1 -0
  1034. package/dist/harness/tui/sanitize.d.ts +9 -0
  1035. package/dist/harness/tui/sanitize.d.ts.map +1 -0
  1036. package/dist/harness/tui/sanitize.js +31 -0
  1037. package/dist/harness/tui/sanitize.js.map +1 -0
  1038. package/dist/harness/tui/style.d.ts +54 -0
  1039. package/dist/harness/tui/style.d.ts.map +1 -0
  1040. package/dist/harness/tui/style.js +64 -0
  1041. package/dist/harness/tui/style.js.map +1 -0
  1042. package/dist/harness/tui/terminal-lifecycle.d.ts +49 -0
  1043. package/dist/harness/tui/terminal-lifecycle.d.ts.map +1 -0
  1044. package/dist/harness/tui/terminal-lifecycle.js +100 -0
  1045. package/dist/harness/tui/terminal-lifecycle.js.map +1 -0
  1046. package/dist/harness/tui/theme.d.ts +77 -0
  1047. package/dist/harness/tui/theme.d.ts.map +1 -0
  1048. package/dist/harness/tui/theme.js +501 -0
  1049. package/dist/harness/tui/theme.js.map +1 -0
  1050. package/dist/harness/tui/tool-cards.d.ts +35 -0
  1051. package/dist/harness/tui/tool-cards.d.ts.map +1 -0
  1052. package/dist/harness/tui/tool-cards.js +122 -0
  1053. package/dist/harness/tui/tool-cards.js.map +1 -0
  1054. package/dist/harness/tui/tool-row.d.ts +51 -0
  1055. package/dist/harness/tui/tool-row.d.ts.map +1 -0
  1056. package/dist/harness/tui/tool-row.js +98 -0
  1057. package/dist/harness/tui/tool-row.js.map +1 -0
  1058. package/dist/harness/tui/views/board.d.ts +49 -0
  1059. package/dist/harness/tui/views/board.d.ts.map +1 -0
  1060. package/dist/harness/tui/views/board.js +293 -0
  1061. package/dist/harness/tui/views/board.js.map +1 -0
  1062. package/dist/harness/tui/views/cards.d.ts +8 -0
  1063. package/dist/harness/tui/views/cards.d.ts.map +1 -0
  1064. package/dist/harness/tui/views/cards.js +206 -0
  1065. package/dist/harness/tui/views/cards.js.map +1 -0
  1066. package/dist/harness/tui/views/components.d.ts +101 -0
  1067. package/dist/harness/tui/views/components.d.ts.map +1 -0
  1068. package/dist/harness/tui/views/components.js +186 -0
  1069. package/dist/harness/tui/views/components.js.map +1 -0
  1070. package/dist/harness/tui/views/diff.d.ts +11 -0
  1071. package/dist/harness/tui/views/diff.d.ts.map +1 -0
  1072. package/dist/harness/tui/views/diff.js +150 -0
  1073. package/dist/harness/tui/views/diff.js.map +1 -0
  1074. package/dist/harness/tui/views/graph.d.ts +49 -0
  1075. package/dist/harness/tui/views/graph.d.ts.map +1 -0
  1076. package/dist/harness/tui/views/graph.js +361 -0
  1077. package/dist/harness/tui/views/graph.js.map +1 -0
  1078. package/dist/harness/tui/views/index.d.ts +15 -0
  1079. package/dist/harness/tui/views/index.d.ts.map +1 -0
  1080. package/dist/harness/tui/views/index.js +15 -0
  1081. package/dist/harness/tui/views/index.js.map +1 -0
  1082. package/dist/harness/tui/views/kit.d.ts +112 -0
  1083. package/dist/harness/tui/views/kit.d.ts.map +1 -0
  1084. package/dist/harness/tui/views/kit.js +282 -0
  1085. package/dist/harness/tui/views/kit.js.map +1 -0
  1086. package/dist/harness/tui/views/trust-cards.d.ts +27 -0
  1087. package/dist/harness/tui/views/trust-cards.d.ts.map +1 -0
  1088. package/dist/harness/tui/views/trust-cards.js +181 -0
  1089. package/dist/harness/tui/views/trust-cards.js.map +1 -0
  1090. package/dist/harness/tui/views/usage.d.ts +4 -0
  1091. package/dist/harness/tui/views/usage.d.ts.map +1 -0
  1092. package/dist/harness/tui/views/usage.js +182 -0
  1093. package/dist/harness/tui/views/usage.js.map +1 -0
  1094. package/dist/harness/tui/views/worker.d.ts +47 -0
  1095. package/dist/harness/tui/views/worker.d.ts.map +1 -0
  1096. package/dist/harness/tui/views/worker.js +221 -0
  1097. package/dist/harness/tui/views/worker.js.map +1 -0
  1098. package/dist/harness/tui/welcome.d.ts +97 -0
  1099. package/dist/harness/tui/welcome.d.ts.map +1 -0
  1100. package/dist/harness/tui/welcome.js +271 -0
  1101. package/dist/harness/tui/welcome.js.map +1 -0
  1102. package/dist/harness/tui/wrap.d.ts +26 -0
  1103. package/dist/harness/tui/wrap.d.ts.map +1 -0
  1104. package/dist/harness/tui/wrap.js +38 -0
  1105. package/dist/harness/tui/wrap.js.map +1 -0
  1106. package/dist/infrastructure/build-info.d.ts +5 -0
  1107. package/dist/infrastructure/build-info.d.ts.map +1 -0
  1108. package/dist/infrastructure/build-info.js +21 -0
  1109. package/dist/infrastructure/build-info.js.map +1 -0
  1110. package/dist/infrastructure/frontmatter.d.ts +25 -0
  1111. package/dist/infrastructure/frontmatter.d.ts.map +1 -0
  1112. package/dist/infrastructure/frontmatter.js +75 -0
  1113. package/dist/infrastructure/frontmatter.js.map +1 -0
  1114. package/dist/templates/agent-manifests.d.ts +19 -0
  1115. package/dist/templates/agent-manifests.d.ts.map +1 -0
  1116. package/dist/templates/agent-manifests.js +302 -0
  1117. package/dist/templates/agent-manifests.js.map +1 -0
  1118. package/dist/templates/base-skills.d.ts +23 -0
  1119. package/dist/templates/base-skills.d.ts.map +1 -0
  1120. package/dist/templates/base-skills.js +477 -0
  1121. package/dist/templates/base-skills.js.map +1 -0
  1122. package/dist/templates/skill-creator-skill.d.ts +23 -0
  1123. package/dist/templates/skill-creator-skill.d.ts.map +1 -0
  1124. package/dist/templates/skill-creator-skill.js +235 -0
  1125. package/dist/templates/skill-creator-skill.js.map +1 -0
  1126. package/dist/templates/skill-references.d.ts +13 -0
  1127. package/dist/templates/skill-references.d.ts.map +1 -0
  1128. package/dist/templates/skill-references.js +280 -0
  1129. package/dist/templates/skill-references.js.map +1 -0
  1130. package/dist/templates/structure-templates.d.ts.map +1 -1
  1131. package/dist/templates/structure-templates.js +20 -144
  1132. package/dist/templates/structure-templates.js.map +1 -1
  1133. package/dist/templates/task-conductor-skill.d.ts +1 -1
  1134. package/dist/templates/task-conductor-skill.d.ts.map +1 -1
  1135. package/dist/templates/task-conductor-skill.js +50 -22
  1136. package/dist/templates/task-conductor-skill.js.map +1 -1
  1137. package/package.json +8 -2
@@ -0,0 +1,3755 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { performance } from "node:perf_hooks";
5
+ import { setTimeout as delay } from "node:timers/promises";
6
+ import { askUserSummaryLine, choiceQuestionLines, parseTypedChoice, createId, digestOf, effectivePolicySchema, EVENT_VERSIONS, DEFAULT_PERMISSION_MODE, EXIT_CODES, exitCodeFor, HarnessError, MODEL_TIERS, EFFORT_SLOTS, HARD_RAILS, WORKSPACE_UNTRUSTED_CODE, workspaceDigest, } from "../contracts/index.js";
7
+ import { SYNORCH_VERSION } from "../../domain/product.js";
8
+ import { createCompactor, DEFAULT_CONTEXT_WINDOW, extractiveSummarizer, messageTokens, reconstructHistory } from "../context/index.js";
9
+ import { actionableFindings, artifactReviewGoal, fixFollowUpMessage, isGeneratedPath, parseReviewArgument, renderArtifactReviewBrief, renderReviewCard, WorkerStreamHub, } from "../orchestration/index.js";
10
+ import { classifyCommand, createHeadlessApprovalBroker, evaluateExecAllowlist } from "../policy/index.js";
11
+ import { BUILTIN_THEMES, bindBackgroundStatus, defaultThemeName, describeEvent, formatHarnessError, GLYPH_SETS, lineDiff, patchPaths, pickHint, quotaProviderLabel, runPrompts, selectGlyphs, shortenPath, suggestedCommandPrefix } from "../tui/index.js";
12
+ import { buildCommit, loadAppearance, planLabel, subscriptionPlan, userHome, workerModels } from "./appearance.js";
13
+ import { DEFAULT_WEB_DOMAINS, describeProcess } from "../tools/index.js";
14
+ import { ATTACHMENTS_OPEN, mayContainImage, resolveAttachments } from "./attachments.js";
15
+ import { askHookApproval, runPluginsSlash, runSkillsSlash } from "./extensions-command.js";
16
+ /** Adapters that turn `image` message parts into provider image input. */
17
+ const IMAGE_ADAPTERS = new Set(["openai-chatgpt", "openai-responses", "anthropic-messages", "claude-code"]);
18
+ import { profileHintsFor } from "./canonical.js";
19
+ import { createCommandGrantStore, normalizeGrant } from "./command-grants.js";
20
+ import { formatListing, listSettings, setUserSetting, settingFor, unsetUserSetting } from "./config-command.js";
21
+ import { DEFAULT_ADAPTER_FOR_PROVIDER } from "./config.js";
22
+ import { OrchestrationTracker } from "./orchestration-view.js";
23
+ import { applyRestore, planRestore, restoreConflicts, rewindPoints } from "./rewind.js";
24
+ import { runModelCommand } from "./model-picker.js";
25
+ import { effortLabel, effortTarget, pickEffort, runEffortCommand } from "./effort-command.js";
26
+ import { runMcpSlash } from "./mcp-command.js";
27
+ import { mcpPanel } from "./panels/mcp.js";
28
+ import { pluginsPanel } from "./panels/plugins.js";
29
+ import { cardPanel, configPanel, helpPanel, memoryPanel, resumePanel, runsPanel, statusPanel } from "./panels/session.js";
30
+ import { skillsPanel } from "./panels/skills.js";
31
+ import { defaultEffort, isReasoningEffort } from "../providers/index.js";
32
+ import { openBrowser } from "../tui/open-browser.js";
33
+ import { acknowledgedNotices, acknowledgeNotice, approvalNotice, mcpStartNotices, StartupNotices } from "./startup-notices.js";
34
+ import { failureInfo } from "./outcome.js";
35
+ import { createSessionRenderer } from "./renderers.js";
36
+ import { createRuntime } from "./runtime.js";
37
+ import { changePathspecs, commitAll, commitSelected, commitsSince, reviewDiff, uncommittedChanges } from "./session-git.js";
38
+ import { CONVERSATION_COMMANDS, contextReport, conversationHelp, conversationPaletteEntries, findConversationCommand, reservedCommandNames, unknownConversationCommand, tasksReport, } from "./slash-commands.js";
39
+ import { resolveTerminalSettings, streamHasColors } from "./terminal.js";
40
+ import { buildContextView, buildEvidence, buildWhy } from "./transparency.js";
41
+ import { memorySeam, runMemoryDesk } from "./memory-desk.js";
42
+ import { createSystemObsidianLauncher, ledgerSummary, readLedger } from "../memory/index.js";
43
+ import { promptTrustForCommand } from "./trust.js";
44
+ import { runInitCommand, runMemoryInit, startOnboarding } from "./onboarding.js";
45
+ import { profileSummaryLines } from "./project-profile.js";
46
+ import { UsageLedger } from "./usage-stats.js";
47
+ const MAX_STEPS = 50;
48
+ /** K7: consecutive turns a Stop hook may add before Synorch stops asking it. */
49
+ const MAX_STOP_CONTINUATIONS = 3;
50
+ const CONVERSATION_TITLE = "chat: ";
51
+ const WRITE_TOOLS = new Set(["apply_patch", "write_file"]);
52
+ const REPLAY_EXCHANGES = 3;
53
+ const REVIEW_DIFF_LIMIT = 48 * 1024;
54
+ const ESC_ARM_MS = 3_000;
55
+ const GO_WORDS = /^(go|go ahead|do it|proceed|yes|ok|okay|ship it|start|evet|başla|basla|yap|devam|tamam)[.! ]*$/iu;
56
+ const SESSION_MODEL_FILE = "session-model.json";
57
+ const ORCHESTRATION_SESSION = /session (ses_[0-9A-HJKMNP-TV-Z]{26})/g;
58
+ /** K3: a worker prompt waits until the user has not typed for this long. */
59
+ const TYPING_PAUSE_MS = 1_200;
60
+ /** K3: how long a cancelled run gets to stop its workers and clean up at exit. */
61
+ const SETTLE_MS = 15_000;
62
+ function toolError(code, message, text = "") {
63
+ return { status: "error", text: text.slice(0, 16 * 1024), truncated: false, redactions: 0, error: { code, message: message.slice(0, 2000) } };
64
+ }
65
+ function linked(outer) {
66
+ const controller = new AbortController();
67
+ if (outer.aborted)
68
+ controller.abort(outer.reason);
69
+ else
70
+ outer.addEventListener("abort", () => controller.abort(outer.reason), { once: true });
71
+ return controller;
72
+ }
73
+ async function readOptional(file) {
74
+ try {
75
+ return await readFile(file);
76
+ }
77
+ catch (error) {
78
+ if (error.code === "ENOENT")
79
+ return undefined;
80
+ throw error;
81
+ }
82
+ }
83
+ /** `/diff` as text lines when no view host is attached. */
84
+ function diffLines(view) {
85
+ if (view.files.length === 0 && (view.integrated === undefined || view.integrated.length === 0))
86
+ return ["Synorch has not changed any file in this conversation."];
87
+ const lines = [];
88
+ if (view.files.length > 0)
89
+ lines.push("Changed by Synorch (not independently reviewed):", ...view.files.map((file) => ` ${file.path} +${file.added} -${file.removed}${file.change === "modified" ? "" : ` (${file.change})`}`));
90
+ if (view.integrated !== undefined && view.integrated.length > 0)
91
+ lines.push("Integrated by workers (checked by Synorch):", ...view.integrated.map((entry) => ` ${entry.path}${entry.reviewed ? " · independently reviewed" : ""}`));
92
+ return lines;
93
+ }
94
+ /** A `git push` with no destructive form (no force, no history rewrite): approving one lets auto push for the session. */
95
+ /** Whether `root` lies in a git work tree (a `.git` entry here or in a parent). */
96
+ function insideGitRepository(root) {
97
+ for (let directory = path.resolve(root);;) {
98
+ if (existsSync(path.join(directory, ".git")))
99
+ return true;
100
+ const parent = path.dirname(directory);
101
+ if (parent === directory)
102
+ return false;
103
+ directory = parent;
104
+ }
105
+ }
106
+ function isPlainGitPush(argv) {
107
+ if (argv === undefined || argv.length === 0)
108
+ return false;
109
+ const classified = classifyCommand(argv, { cwd: ".", writeScope: ["**"], forbidden: [] });
110
+ return classified.findings.length === 0 && classified.external.length > 0 && classified.external.every((entry) => entry.code === "git-push");
111
+ }
112
+ function strings(value) {
113
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string") ? value : undefined;
114
+ }
115
+ function snippet(text, length) {
116
+ const flat = text.replace(/\s+/g, " ").trim();
117
+ return flat.length <= length ? flat : `${flat.slice(0, length - 1)}…`;
118
+ }
119
+ function sessionRouteRule(runtime, preferred) {
120
+ const rules = runtime.routeRules();
121
+ const fits = (rule, tier) => rule.tier === tier && (rule.role === undefined || rule.role === "session");
122
+ if (preferred !== undefined) {
123
+ const chosen = rules.find((rule) => fits(rule, preferred));
124
+ if (chosen !== undefined)
125
+ return chosen;
126
+ }
127
+ return rules.find((rule) => fits(rule, "session")) ?? rules.find((rule) => fits(rule, "orchestrator"));
128
+ }
129
+ function missingRoute(runtime) {
130
+ const hints = profileHintsFor(runtime.canonical, "orchestrator").map((hint) => `${hint.provider}/${hint.model}`);
131
+ return new HarnessError({
132
+ code: "config_invalid",
133
+ message: `no model route is configured for the conversation: add a routes entry with tier session (or orchestrator) to ${path.join(runtime.home, "config.yaml")}, or pass --profile session=<provider>/<model>${hints.length === 0 ? "" : ` (the canonical model profiles suggest ${hints.join(" or ")})`}`,
134
+ workspace_effect: "none",
135
+ retry_safe: true,
136
+ next_command: "syn doctor --runtime",
137
+ });
138
+ }
139
+ /** The conversation model the user saved with `/model <tier> --save` (a preference, not a route). */
140
+ async function savedSessionTier(home) {
141
+ try {
142
+ const parsed = JSON.parse(await readFile(path.join(home, SESSION_MODEL_FILE), "utf8"));
143
+ return MODEL_TIERS.includes(String(parsed.tier)) ? parsed.tier : undefined;
144
+ }
145
+ catch {
146
+ return undefined;
147
+ }
148
+ }
149
+ /** Plan mode (ADR-21 D2): the session policy with workspace writes, commands and external writes denied. */
150
+ export function planModePolicy(base) {
151
+ return effectivePolicySchema.parse({
152
+ ...base,
153
+ effects: { ...base.effects, "workspace-write": "deny", exec: "deny", "external-write": "deny" },
154
+ layers: [...base.layers, { layer: "task", source: "plan-mode", digest: digestOf({ plan_mode: true }) }],
155
+ });
156
+ }
157
+ /**
158
+ * K3: the session policy with workspace writes denied, for one edit that touches a path a live task
159
+ * of a background run owns. The gateway records the denial like any other; the conversation words it.
160
+ */
161
+ export function runLockPolicy(base) {
162
+ return effectivePolicySchema.parse({
163
+ ...base,
164
+ effects: { ...base.effects, "workspace-write": "deny" },
165
+ layers: [...base.layers, { layer: "task", source: "background-run-owned-path", digest: digestOf({ background_run_lock: true }) }],
166
+ });
167
+ }
168
+ /** `ask_user` while the TUI reads input concurrently: the next typed message answers the question. */
169
+ class QuestionDesk {
170
+ pending;
171
+ get waiting() {
172
+ return this.pending !== undefined;
173
+ }
174
+ ask(signal) {
175
+ return new Promise((resolve, reject) => {
176
+ if (signal.aborted) {
177
+ reject(new DOMException("aborted", "AbortError"));
178
+ return;
179
+ }
180
+ const onAbort = () => {
181
+ this.pending = undefined;
182
+ reject(new DOMException("aborted", "AbortError"));
183
+ };
184
+ signal.addEventListener("abort", onAbort, { once: true });
185
+ this.pending = (answer) => {
186
+ signal.removeEventListener("abort", onAbort);
187
+ this.pending = undefined;
188
+ resolve(answer);
189
+ };
190
+ });
191
+ }
192
+ answer(text) {
193
+ const pending = this.pending;
194
+ if (pending === undefined)
195
+ return false;
196
+ pending(text);
197
+ return true;
198
+ }
199
+ }
200
+ export async function conversationCommand(parsed, io, overrides) {
201
+ return new Conversation(parsed, io, overrides).run();
202
+ }
203
+ class Conversation {
204
+ parsed;
205
+ io;
206
+ overrides;
207
+ outer;
208
+ desk = new QuestionDesk();
209
+ requests = new Set();
210
+ pendingNotes = [];
211
+ recoveryNotes = [];
212
+ queued = [];
213
+ runtime;
214
+ renderer;
215
+ usageLedger;
216
+ glyphs = GLYPH_SETS.ascii;
217
+ grants;
218
+ grantList = [];
219
+ routePromise;
220
+ routeRecorded = false;
221
+ sessionTier;
222
+ currentModel;
223
+ sessionId;
224
+ log;
225
+ driver;
226
+ coordinator;
227
+ /** K1.7: live streams of this process's worker attempts (the conversation's own session is never kept). */
228
+ hub = new WorkerStreamHub((sessionId) => sessionId === this.sessionId);
229
+ workerDirectory;
230
+ workerSeam;
231
+ /** Plain interactive mode: input is read while workers run (steering, answers, /worker). */
232
+ readingDuringWorkers = false;
233
+ policyCache;
234
+ active;
235
+ turnRunning = false;
236
+ orchestration;
237
+ orchestratedSessions = [];
238
+ /** K3: every worker run of this process, oldest first (`/runs`, `run_status`). */
239
+ workerRuns = [];
240
+ /** K3: background completions the agent has not been told about yet (a follow-up turn reports them when idle). */
241
+ unreportedRuns = 0;
242
+ /** K3: aborted to wake the idle loop (a background run finished). */
243
+ wakeIdle = new AbortController();
244
+ /** K3: prompts (approvals, questions) are shown one at a time while a background run is active. */
245
+ promptQueue = Promise.resolve();
246
+ /** The mode to return to when plan mode ends (Shift+Tab, /go, a go-word). */
247
+ modeBeforePlan;
248
+ /** Set while the current exec call follows a declined trust question: its prompt is answered "no" for the user. */
249
+ trustDeclinedNow = false;
250
+ sessionBroker;
251
+ startedAt = Date.now();
252
+ lastTracker;
253
+ /** The last `/review` result of this process (`/review fix`). */
254
+ lastReview;
255
+ turnId;
256
+ trustAsked = false;
257
+ exiting = false;
258
+ /** Startup notices: held until the first-run setup is done, then at most two lines (/status keeps them all). */
259
+ notices;
260
+ mcpStarted = Promise.resolve();
261
+ debug = false;
262
+ submittedAt;
263
+ sessionStartHooked = false;
264
+ stopContinuations = 0;
265
+ timings = [];
266
+ /** K4.2: the first Esc (with background processes running) armed "Esc again stops them". */
267
+ processStopArmedAt;
268
+ constructor(parsed, io, overrides) {
269
+ this.parsed = parsed;
270
+ this.io = io;
271
+ this.overrides = overrides;
272
+ this.outer = new AbortController();
273
+ const outer = io.signal;
274
+ if (outer?.aborted === true)
275
+ this.outer.abort(outer.reason);
276
+ else
277
+ outer?.addEventListener("abort", () => this.outer.abort(outer.reason), { once: true });
278
+ }
279
+ async run() {
280
+ const started = performance.now();
281
+ const io = this.io;
282
+ const workspaceRoot = path.resolve(io.cwd, this.parsed.common.target ?? ".");
283
+ let failure;
284
+ let runtime;
285
+ try {
286
+ runtime = await createRuntime({
287
+ workspaceRoot,
288
+ env: io.env,
289
+ policyMode: this.parsed.session.policy,
290
+ routes: this.parsed.session.profiles,
291
+ efforts: this.parsed.session.efforts,
292
+ overrides: this.overrides,
293
+ ...(this.parsed.session.permission === undefined ? {} : { permissionMode: this.parsed.session.permission }),
294
+ });
295
+ }
296
+ catch (error) {
297
+ failure = failureInfo(error);
298
+ }
299
+ const runtimeMs = Math.round(performance.now() - started);
300
+ const settings = resolveTerminalSettings({ jsonl: false, plain: this.parsed.common.plain, color: this.parsed.common.color, configColor: runtime?.config.color }, {
301
+ env: io.env,
302
+ stdinIsTTY: io.stdinIsTTY,
303
+ stdoutIsTTY: io.stdout.isTTY === true,
304
+ stdoutHasColors: streamHasColors(io.stdout),
305
+ stderrHasColors: streamHasColors(io.stderr),
306
+ });
307
+ const configuredGlyphs = runtime?.config.glyphs;
308
+ this.glyphs =
309
+ configuredGlyphs !== undefined && configuredGlyphs !== "auto" && (io.env.SYN_GLYPHS ?? "").trim() === "" && settings.kind !== "plain"
310
+ ? GLYPH_SETS[configuredGlyphs]
311
+ : selectGlyphs(io.env, io.platform, settings.kind === "plain");
312
+ const debugEnv = io.env.SYN_DEBUG;
313
+ this.debug = this.parsed.debug || (debugEnv !== undefined && debugEnv !== "" && debugEnv !== "0");
314
+ // K8: theme, colour depth and welcome header of the interactive view (plain and JSONL keep their own).
315
+ const appearance = runtime !== undefined && settings.kind === "tui" ? await loadAppearance(runtime.home, runtime.config, io.env, io.platform).catch(() => undefined) : undefined;
316
+ this.renderer = await createSessionRenderer(io, {
317
+ kind: settings.kind === "jsonl" ? "plain" : settings.kind,
318
+ color: settings.color,
319
+ policyMode: this.parsed.session.policy,
320
+ streamDeltas: false,
321
+ wantsInput: true,
322
+ interactive: io.stdinIsTTY,
323
+ onInterrupt: () => this.interrupt(),
324
+ onExit: () => {
325
+ // K3: with workers in the background the exit request reaches the input loop, which asks first.
326
+ if (this.backgroundRun() !== undefined)
327
+ return;
328
+ this.hardExit();
329
+ },
330
+ view: "conversation",
331
+ glyphs: this.glyphs,
332
+ debug: this.debug,
333
+ ...(runtime?.config.mouse === undefined ? {} : { mouse: runtime.config.mouse }),
334
+ ...(appearance === undefined ? {} : { appearance: { theme: appearance.theme, themes: appearance.themes, colorDepth: appearance.colorDepth, welcome: appearance.welcome } }),
335
+ });
336
+ if (runtime === undefined || failure !== undefined)
337
+ return this.fail(failure ?? failureInfo(new Error("runtime unavailable")));
338
+ this.runtime = runtime;
339
+ // ADR-08 revision (2026-09-24): an interactive conversation starts in `ui.permission_mode` (default auto);
340
+ // headless keeps default-deny unless --permission-mode was passed; --policy ask means ask mode.
341
+ const interactive = io.stdinIsTTY && this.renderer.approvals.availability === "interactive";
342
+ const startMode = interactive ? (runtime.config.permissionMode ?? DEFAULT_PERMISSION_MODE) : undefined;
343
+ if (this.parsed.session.permission === undefined)
344
+ runtime.setPermissionMode(this.parsed.session.policy === "ask" ? "ask" : startMode);
345
+ if (runtime.permissionMode() === "plan")
346
+ this.modeBeforePlan = startMode === "plan" ? DEFAULT_PERMISSION_MODE : startMode;
347
+ this.sessionTier = await savedSessionTier(runtime.home);
348
+ const rule = sessionRouteRule(runtime, this.sessionTier);
349
+ if (rule === undefined)
350
+ return this.fail(failureInfo(missingRoute(runtime)));
351
+ this.sessionTier = rule.tier;
352
+ this.currentModel = rule.route.model_id;
353
+ // Resolving the route may probe the provider's capabilities: it runs while the user types.
354
+ this.routePromise = runtime.router.resolve({ tier: rule.tier, role: "session" }, this.outer.signal);
355
+ this.routePromise.catch(() => undefined);
356
+ const ledger = new UsageLedger(runtime.home);
357
+ this.usageLedger = ledger;
358
+ const unsubscribe = runtime.subscribe((event) => {
359
+ if (event.kind === "session-event")
360
+ ledger.observe(event.event);
361
+ this.hub.feed(event);
362
+ this.forward(event);
363
+ });
364
+ const unbind = io.stdinIsTTY && this.renderer.input !== undefined ? runtime.bindUserPrompt((question, options, signal) => this.gated(signal, () => this.askUser(question, options, signal)), (question, signal) => this.gated(signal, () => this.chooseForAgent(question, signal))) : () => undefined;
365
+ runtime.orchestrate.set((input, context) => this.runOrchestration(input, context));
366
+ runtime.orchestrate.setControl({
367
+ status: (input, context) => this.runStatus(input, context.signal),
368
+ steer: async (input) => this.runSteer(input),
369
+ cancel: async (input) => this.runCancel(input),
370
+ });
371
+ let unbindControls = () => undefined;
372
+ const unwatchProcesses = this.watchProcesses();
373
+ this.startedAt = Date.now();
374
+ try {
375
+ this.grants = createCommandGrantStore(runtime.home, runtime.trust.state().root);
376
+ this.grantList = await this.grants.list();
377
+ const resumed = await this.openResumed();
378
+ const sandboxNoticeSeen = runtime.sandbox.enforcement === "full" || (await acknowledgedNotices(runtime.home)).has("sandbox-partial");
379
+ this.notices = new StartupNotices((notice) => this.note(notice.level, notice.text), this.glyphs.sep);
380
+ await this.renderer.start({ ...this.header(rule, await this.welcomeHint(resumed !== undefined)), sandboxNoticeSeen });
381
+ // The partial-sandbox warning shows once per machine; /permissions keeps it.
382
+ if (!sandboxNoticeSeen && this.renderer.controls?.appearance !== undefined)
383
+ void acknowledgeNotice(runtime.home, "sandbox-partial");
384
+ const controls = this.renderer.controls;
385
+ this.refreshPalette();
386
+ this.refreshStatus();
387
+ // K3: MCP servers start in the background (never before the editor); problems become notes.
388
+ this.mcpStarted = this.startMcp().catch(() => undefined);
389
+ // Shift+Tab / Alt+M in the renderer cycles the permission mode; the session applies the policy.
390
+ unbindControls = controls?.onPermissionModeChange((mode) => this.setMode(mode)) ?? (() => undefined);
391
+ if (runtime.permissionMode() === "full")
392
+ this.note("error", this.fullAccessNotice());
393
+ if (resumed !== undefined)
394
+ await this.showResumed(resumed);
395
+ else
396
+ await this.memoryStartLine();
397
+ // Zero-config onboarding: the quiet "Synorch ready · …" line, the memory vault and the first-session memory bootstrap (user scope only).
398
+ void startOnboarding(runtime, (line) => this.startupLine({ level: "info", text: line, context: true }), this.glyphs.sep);
399
+ if (this.debug)
400
+ this.note("info", `harness: runtime ready in ${runtimeMs} ms`);
401
+ for (const problem of appearance?.problems ?? [])
402
+ this.startupLine({ level: "warning", text: `${this.glyphs.warn} ${problem}` });
403
+ // K8: the subscription's plan label (ChatGPT Plus) resolves from the stored login after the first frame.
404
+ void subscriptionPlan(runtime, rule.route, this.outer.signal)
405
+ .then((plan) => {
406
+ if (plan !== undefined)
407
+ this.renderer.controls?.appearance?.updateWelcome({ plan });
408
+ })
409
+ .catch(() => undefined);
410
+ // K8 first-run setup (interactive terminal only, once; Esc skips) or syn setup.
411
+ if (this.parsed.setup === true || (resumed === undefined && this.shouldOnboard()))
412
+ await this.setup(true);
413
+ // Notices after welcome and setup; MCP servers get a moment to settle so their lines join the same group.
414
+ if (this.renderer.controls?.appearance === undefined)
415
+ this.notices?.flush();
416
+ else
417
+ void Promise.race([this.mcpStarted, delay(1500)]).then(() => this.notices?.flush());
418
+ // Credential pre-resolution (keychain, token refresh) happens in the background, never before the editor.
419
+ void this.routePromise.then((decision) => runtime.credentials(decision.route, this.outer.signal)).catch(() => undefined);
420
+ const input = this.renderer.input;
421
+ if (input === undefined) {
422
+ this.note("warning", "syn agent needs input: attach a terminal or pipe messages on stdin, one per line");
423
+ return EXIT_CODES.usage;
424
+ }
425
+ await this.loop(input);
426
+ await this.renderer.stop("completed");
427
+ if (this.debug && this.timings.length > 0)
428
+ io.stderr.write(`timing: ${this.timings.join("; ")}\n`);
429
+ if (this.sessionId !== undefined && this.log !== undefined)
430
+ io.stderr.write(`Saved · resume with syn agent --continue (or --resume ${this.sessionId})\n`);
431
+ return EXIT_CODES.success;
432
+ }
433
+ catch (error) {
434
+ return await this.fail(failureInfo(error));
435
+ }
436
+ finally {
437
+ // Background processes never outlive the session (K4.2): the whole tree of each is killed.
438
+ this.exiting = true;
439
+ // K3: a worker run never outlives the session either: it is cancelled and given time to clean up its worktrees.
440
+ await this.settleRuns();
441
+ await runtime.processes.killAll().catch(() => undefined);
442
+ await runtime.mcp.close().catch(() => undefined);
443
+ unwatchProcesses();
444
+ unbindControls();
445
+ runtime.orchestrate.set(undefined);
446
+ runtime.orchestrate.setControl(undefined);
447
+ unbind();
448
+ unsubscribe();
449
+ await ledger.flush().catch(() => undefined);
450
+ await this.log?.close().catch(() => undefined);
451
+ }
452
+ }
453
+ async fail(error) {
454
+ await this.renderer.stop("error").catch(() => undefined);
455
+ this.io.stderr.write(formatHarnessError(error));
456
+ return exitCodeFor(error.code);
457
+ }
458
+ header(rule, hint) {
459
+ const permissionMode = this.runtime.permissionMode();
460
+ const runtime = this.runtime;
461
+ const configured = effortLabel(runtime.effortFor(rule.tier, "session", rule.route), rule.route.adapter_id);
462
+ // Nothing set: the welcome shows the level the model runs at by default (K6), not a blank.
463
+ const effort = configured === "default" ? (defaultEffort({ provider: rule.route.provider_id, model: rule.route.model_id, adapterId: rule.route.adapter_id }) ?? configured) : configured;
464
+ const commit = buildCommit();
465
+ const plan = planLabel(runtime, rule.route);
466
+ const workers = workerModels(runtime, rule.tier, rule.route.model_id);
467
+ return {
468
+ welcome: {
469
+ ...(commit === undefined ? {} : { commit }),
470
+ ...(effort === "default" || effort === "n/a" ? {} : { effort }),
471
+ ...(plan === undefined ? {} : { plan }),
472
+ ...(workers.length === 0 ? {} : { workers }),
473
+ path: shortenPath(runtime.workspaceRoot, userHome(this.io.env)),
474
+ ...(hint === undefined ? {} : { hint }),
475
+ },
476
+ workspaceRoot: runtime.workspaceRoot,
477
+ gitBranch: runtime.gitBranch,
478
+ policyMode: runtime.policyMode,
479
+ routes: [],
480
+ sandboxEnforcement: runtime.sandbox.enforcement,
481
+ // Quiet by default: a successful load says nothing; only problems reach the header.
482
+ notices: [...runtime.config.warnings.map((warning) => warning.message), ...runtime.canonical.diagnostics.map((diagnostic) => `canonical .ai: ${diagnostic}`)],
483
+ version: SYNORCH_VERSION,
484
+ model: rule.route.model_id,
485
+ contextWindowTokens: DEFAULT_CONTEXT_WINDOW,
486
+ ...(permissionMode === undefined ? {} : { permissionMode }),
487
+ };
488
+ }
489
+ note(level, message) {
490
+ this.renderer.render({ kind: "notice", level, message });
491
+ }
492
+ print(lines) {
493
+ for (const line of lines)
494
+ this.note("info", line);
495
+ }
496
+ /**
497
+ * Layered panels: in the TUI a report command opens its panel (resolves true once it is closed);
498
+ * plain and JSONL modes, or a renderer without panels, get false and print the text report.
499
+ */
500
+ async showPanel(build) {
501
+ const controls = this.renderer.controls;
502
+ if (this.renderer.kind !== "tui" || controls?.openPanel === undefined)
503
+ return false;
504
+ const page = await build();
505
+ await controls.openPanel(page, this.outer.signal);
506
+ return true;
507
+ }
508
+ /** `/help`: the command browser in the TUI, the list otherwise. */
509
+ async help() {
510
+ const invocable = this.runtime.extensions.invocable(reservedCommandNames()).map((entry) => ({ name: entry.name, description: entry.meta.description, argsHint: entry.meta.argumentHint, source: entry.source }));
511
+ const keys = conversationHelp().at(-1) ?? "";
512
+ if (await this.showPanel(() => helpPanel(CONVERSATION_COMMANDS, invocable, keys, this.glyphs.sep)))
513
+ return;
514
+ this.print(conversationHelp());
515
+ }
516
+ /** Only this conversation's events and model streams reach the view; worker and coordinator traffic feeds the board instead. */
517
+ forward(event) {
518
+ if (event.kind === "session-event") {
519
+ const recorded = event.event;
520
+ const orchestration = this.orchestration;
521
+ // Worker, reviewer and planner requests of this process count toward the footer's quota %, cost
522
+ // and activity tokens: their provider/usage events (with `x-codex-*` quota) reach the view too.
523
+ if (recorded.session_id !== this.sessionId && recorded.type === "provider/usage" && this.renderer.kind !== "jsonl")
524
+ this.renderer.render(event);
525
+ // `/review` runs feed their own trackers (`/runs`, `/evidence`); they never touch the board.
526
+ if (recorded.session_id !== this.sessionId) {
527
+ for (const run of this.workerRuns) {
528
+ if (run.kind !== "review" || run.status !== "running")
529
+ continue;
530
+ const before = run.tracker.sessionId;
531
+ if (run.tracker.observe(recorded) && before === undefined && run.tracker.sessionId !== undefined && !this.orchestratedSessions.includes(run.tracker.sessionId))
532
+ this.orchestratedSessions.push(run.tracker.sessionId);
533
+ }
534
+ }
535
+ if (orchestration !== undefined && recorded.session_id !== this.sessionId) {
536
+ this.workerLine(recorded);
537
+ this.observeOrchestration(orchestration, recorded);
538
+ return;
539
+ }
540
+ if (recorded.session_id !== this.sessionId)
541
+ return;
542
+ if (recorded.type === "turn/started")
543
+ this.turnId = recorded.data.turn_id;
544
+ if (recorded.type === "model/request_prepared") {
545
+ this.requests.add(recorded.data.request_id);
546
+ if (this.submittedAt !== undefined) {
547
+ this.timings.push(`Enter → request ${Math.round(performance.now() - this.submittedAt)} ms`);
548
+ this.submittedAt = undefined;
549
+ }
550
+ }
551
+ this.renderer.render(event);
552
+ return;
553
+ }
554
+ if (event.kind === "stream") {
555
+ if (this.requests.has(event.requestId))
556
+ this.renderer.render(event);
557
+ return;
558
+ }
559
+ // Coordinator notices while workers run stay on the board; everything else is shown.
560
+ if (event.kind === "notice" && this.orchestration !== undefined && event.level === "info")
561
+ return;
562
+ this.renderer.render(event);
563
+ }
564
+ // ---- sessions -------------------------------------------------------------------------------
565
+ async conversations() {
566
+ const summaries = await this.runtime.sessions.list(this.runtime.projectId);
567
+ const titles = new Map(summaries.map((summary) => [summary.manifest.session_id, (summary.manifest.title ?? "").replace(CONVERSATION_TITLE, "")]));
568
+ return summaries
569
+ .filter((summary) => summary.manifest.title?.startsWith(CONVERSATION_TITLE) === true && !summary.locked)
570
+ .sort((left, right) => (right.lastEventAt ?? right.manifest.created_at).localeCompare(left.lastEventAt ?? left.manifest.created_at))
571
+ .map((summary) => ({
572
+ sessionId: summary.manifest.session_id,
573
+ title: (summary.manifest.title ?? "").slice(CONVERSATION_TITLE.length),
574
+ at: summary.lastEventAt ?? summary.manifest.created_at,
575
+ forked: summary.manifest.parent !== undefined,
576
+ parentTitle: summary.manifest.parent === undefined ? undefined : titles.get(summary.manifest.parent.session_id),
577
+ }));
578
+ }
579
+ async openResumed() {
580
+ let sessionId = this.parsed.resume;
581
+ if (sessionId === undefined && this.parsed.fork !== undefined) {
582
+ const source = this.parsed.fork.sessionId;
583
+ let upTo = this.parsed.fork.upToSeq;
584
+ if (upTo === undefined) {
585
+ upTo = 0;
586
+ for await (const item of (await this.runtime.sessions.openForRead(source)).read())
587
+ if (item.status === "ok")
588
+ upTo = item.event.seq;
589
+ }
590
+ const forked = await this.runtime.sessions.fork(source, upTo);
591
+ sessionId = forked.sessionId;
592
+ await forked.close();
593
+ }
594
+ if (sessionId === undefined && this.parsed.continue)
595
+ sessionId = (await this.conversations())[0]?.sessionId;
596
+ if (sessionId === undefined)
597
+ return undefined;
598
+ return this.openSession(sessionId);
599
+ }
600
+ async openSession(sessionId) {
601
+ this.sessionId = sessionId;
602
+ // Crash recovery (I1 AC-4): what was closed is shown with the resume card; a clean resume stays quiet.
603
+ this.recoveryNotes = (await this.runtime.recover(sessionId))
604
+ .filter((report) => report.recovered.length + report.interruptedToolCalls.length + report.cancelledToolCalls.length > 0 || report.tornTail !== undefined)
605
+ .map((report) => `recovered ${report.sessionId}: ${report.recovered.length} open item(s) closed, ${report.interruptedToolCalls.length} tool call(s) interrupted with unknown outcome (not re-run), ${report.cancelledToolCalls.length} cancelled` +
606
+ (report.tornTail === undefined ? "" : `, torn tail of ${report.tornTail.bytes} bytes quarantined`));
607
+ this.log = await this.runtime.sessions.openForWrite(sessionId);
608
+ this.driver = undefined;
609
+ const events = [];
610
+ for await (const item of this.log.read())
611
+ if (item.status === "ok")
612
+ events.push(item.event);
613
+ this.routeRecorded = false;
614
+ return events;
615
+ }
616
+ /** The resume card (UX-06): the replayed tail, then where we were, what changed since, and the next step. */
617
+ async showResumed(all) {
618
+ const g = this.glyphs;
619
+ // A `syn run` session: the planner's prompts are not the person's messages; the goal stands in for them.
620
+ const run = runPrompts(all);
621
+ const events = run.hidden.size === 0 ? all : all.filter((event) => !run.hidden.has(event));
622
+ const users = events.flatMap((event, index) => (event.type === "message/recorded" && event.data.role === "user" ? [index] : []));
623
+ const last = events.at(-1);
624
+ const ago = last === undefined ? "" : ` ${g.sep} ${relativeTime(Date.parse(last.timestamp))}`;
625
+ const firstShown = users.length > REPLAY_EXCHANGES ? (users[users.length - REPLAY_EXCHANGES] ?? 0) : 0;
626
+ const folded = users.length > REPLAY_EXCHANGES ? users.length - REPLAY_EXCHANGES : 0;
627
+ this.note("info", `${g.resume} Resumed${ago} ${g.sep} ${users.length} message${users.length === 1 ? "" : "s"}${folded === 0 ? "" : ` ${g.sep} ${folded} earlier not shown`}`);
628
+ if (run.goal !== undefined)
629
+ this.note("info", ` run: ${snippet(run.goal, 160)}`);
630
+ this.renderer.replay?.(events.slice(firstShown));
631
+ for (const line of this.recoveryNotes.splice(0))
632
+ this.note("info", line);
633
+ for (const line of await this.continuity(events, run.goal))
634
+ this.note("info", line);
635
+ }
636
+ async continuity(events, runGoal) {
637
+ const texts = (role) => events.flatMap((event) => {
638
+ if (event.type !== "message/recorded" || event.data.role !== role || event.data.message === undefined)
639
+ return [];
640
+ const text = event.data.message.content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join(" ");
641
+ return text.trim() === "" ? [] : [text.replace(/^\[Synorch note:[^\]]*\]\s*/u, "").split("<synorch-attachments>")[0] ?? ""];
642
+ });
643
+ const lastUser = texts("user").at(-1);
644
+ const lastAnswer = texts("assistant").at(-1);
645
+ const where = lastUser === undefined ? (runGoal === undefined ? "nothing asked yet" : `run "${snippet(runGoal, 70)}"${lastAnswer === undefined ? "" : ` → ${snippet(lastAnswer, 80)}`}`) : `"${snippet(lastUser, 70)}"${lastAnswer === undefined ? "" : ` → ${snippet(lastAnswer, 80)}`}`;
646
+ const changes = [];
647
+ const restored = new Set(events.flatMap((event) => (event.type === "checkpoint/restored" ? [event.data.checkpoint_seq] : [])));
648
+ const latestAfter = new Map();
649
+ for (const event of events)
650
+ if (event.type === "checkpoint/recorded" && !restored.has(event.seq))
651
+ for (const file of event.data.files)
652
+ latestAfter.set(file.path, file.after);
653
+ const touched = [];
654
+ for (const [file, after] of latestAfter) {
655
+ const place = this.inside(file);
656
+ if (place === undefined)
657
+ continue;
658
+ const current = await readOptional(place.absolute).catch(() => undefined);
659
+ const digest = current === undefined ? null : workspaceDigest(new Uint8Array(current));
660
+ if (digest !== after)
661
+ touched.push(file);
662
+ }
663
+ if (touched.length > 0)
664
+ changes.push(`${touched.slice(0, 3).join(", ")}${touched.length > 3 ? ` +${touched.length - 3}` : ""} changed outside Synorch`);
665
+ const lastAt = events.at(-1)?.timestamp;
666
+ if (lastAt !== undefined) {
667
+ const commits = await commitsSince(this.runtime.workspaceRoot, lastAt).catch(() => []);
668
+ if (commits.length > 0)
669
+ changes.push(`${commits.length} commit${commits.length === 1 ? "" : "s"} (latest: ${snippet(commits[0] ?? "", 50)})`);
670
+ }
671
+ // K2 (UX-06): what changed in memory since, and the todo list the agent kept, if any.
672
+ const since = lastAt ?? "";
673
+ const pending = await this.runtime.memory.pending().catch(() => []);
674
+ const ledger = await readLedger(this.runtime.memory, this.runtime.projectId, this.runtime.gitBranch).catch(() => undefined);
675
+ const index = await this.runtime.memory.index().catch(() => undefined);
676
+ const lastMs = Date.parse(since);
677
+ const freshNotes = index === undefined || !Number.isFinite(lastMs) ? 0 : index.notes.filter((note) => note.mtimeMs > lastMs && (note.project_id === this.runtime.projectId || note.scope === "user")).length;
678
+ if (freshNotes > 0 || pending.length > 0)
679
+ changes.push(`memory: ${freshNotes > 0 ? `${freshNotes} note${freshNotes === 1 ? "" : "s"} changed` : ""}${freshNotes > 0 && pending.length > 0 ? ", " : ""}${pending.length > 0 ? `${pending.length} proposal${pending.length === 1 ? "" : "s"} waiting` : ""}`);
680
+ const todo = lastTodo(events);
681
+ const lastTurn = [...events].reverse().find((event) => event.type === "turn/ended");
682
+ const next = lastTurn !== undefined && lastTurn.data.outcome !== "completed"
683
+ ? `the last turn ended ${lastTurn.data.outcome.replaceAll("_", " ")}; say "continue" to pick it up`
684
+ : todo?.next !== undefined
685
+ ? `continue with "${snippet(todo.next, 60)}"`
686
+ : pending.length > 0
687
+ ? `/memory review (${pending.length} waiting), then continue where we left off`
688
+ : "continue where we left off, or ask something new";
689
+ const summary = ledger === undefined ? undefined : ledgerSummary(ledger);
690
+ const extra = [...(todo === undefined ? [] : [` Todo ${todo.done}/${todo.total} done${todo.next === undefined ? "" : ` · next: ${snippet(todo.next, 60)}`}`]), ...(summary === undefined ? [] : [` Memory ${summary.replace(/^memory: /, "")}`])];
691
+ if (todo !== undefined && todo.next !== undefined)
692
+ this.pendingNotes.push(`your todo list from before the resume: ${todo.done}/${todo.total} done; next open item: ${snippet(todo.next, 200)}.`);
693
+ if (lastTurn !== undefined && lastTurn.data.outcome !== "completed")
694
+ this.pendingNotes.push(`the previous turn ended ${lastTurn.data.outcome}; the conversation was resumed.`);
695
+ if (touched.length > 0)
696
+ this.pendingNotes.push(`since the last turn, ${touched.join(", ")} changed outside Synorch (re-read before editing).`);
697
+ return [` Where we were ${where}`, ` Since then ${changes.length === 0 ? "no changes to files Synorch edited, no new commits" : changes.join(" · ")}`, ...extra, ` Next ${next}`];
698
+ }
699
+ async ensureLog(firstMessage) {
700
+ if (this.log !== undefined)
701
+ return this.log;
702
+ const runtime = this.runtime;
703
+ const sessionId = this.sessionId ?? createId("session");
704
+ this.sessionId = sessionId;
705
+ this.log = await runtime.sessions.create({
706
+ session_id: sessionId,
707
+ project_id: runtime.projectId,
708
+ workspace_root: runtime.workspaceRoot,
709
+ created_at: new Date().toISOString(),
710
+ title: `${CONVERSATION_TITLE}${firstMessage.replace(/\s+/g, " ").slice(0, 120)}`,
711
+ });
712
+ return this.log;
713
+ }
714
+ async append(type, data, actor) {
715
+ const log = this.log;
716
+ if (log === undefined)
717
+ return undefined;
718
+ return log.append({
719
+ type,
720
+ event_version: EVENT_VERSIONS[type],
721
+ actor: actor === "user" ? { kind: "user" } : { kind: "agent", role: "session" },
722
+ data,
723
+ });
724
+ }
725
+ async readEvents() {
726
+ const events = [];
727
+ if (this.log === undefined)
728
+ return events;
729
+ for await (const item of this.log.read())
730
+ if (item.status === "ok")
731
+ events.push(item.event);
732
+ return events;
733
+ }
734
+ // ---- policy, trust, checkpoints --------------------------------------------------------------
735
+ basePolicy() {
736
+ this.policyCache ??= this.runtime.sessionPolicy(this.grantList);
737
+ return this.policyCache;
738
+ }
739
+ get planOn() {
740
+ return this.runtime.permissionMode() === "plan";
741
+ }
742
+ policy() {
743
+ // Plan mode is computed into the policy itself (permission_mode plan); without a mode (headless) nothing narrows further.
744
+ return this.basePolicy();
745
+ }
746
+ ensureDriver(log) {
747
+ this.driver ??= this.runtime.createSessionDriver(this.broker(), log, (gateway) => this.wrap(gateway));
748
+ return this.driver;
749
+ }
750
+ /**
751
+ * The conversation's approval broker (ADR-08 revision 2026-09-24): with a permission mode and a
752
+ * human attached, prompts go to the renderer's action card, and "Always allow <prefix>" is
753
+ * persisted per workspace (user scope, audited as `command/allowed`). Headless, a prompt is a
754
+ * refusal. `--policy ask` without a mode keeps the renderer broker too.
755
+ */
756
+ broker() {
757
+ if (this.sessionBroker !== undefined)
758
+ return this.sessionBroker;
759
+ const interactive = this.renderer.approvals;
760
+ const headless = createHeadlessApprovalBroker({ mode: this.runtime.policyMode });
761
+ this.sessionBroker = {
762
+ availability: interactive.availability,
763
+ request: async (request, signal) => {
764
+ const mode = this.runtime.permissionMode();
765
+ const human = interactive.availability === "interactive" && (mode !== undefined || this.runtime.policyMode === "ask");
766
+ if (!human)
767
+ return headless.request(request, signal);
768
+ if (this.trustDeclinedNow && request.subject_kind === "action") {
769
+ return {
770
+ approval_id: request.approval_id,
771
+ subject_kind: request.subject_kind,
772
+ subject_digest: request.subject_digest,
773
+ outcome: "rejected",
774
+ decided_by: "user",
775
+ mode: this.basePolicy().mode,
776
+ decided_at: new Date().toISOString(),
777
+ reason: "the user chose not to trust this folder (Not now), so this command was not run",
778
+ };
779
+ }
780
+ const decision = await this.gated(signal, () => interactive.request(request, signal));
781
+ await this.afterDecision(request, decision);
782
+ return decision;
783
+ },
784
+ };
785
+ return this.sessionBroker;
786
+ }
787
+ /** "Always allow <prefix>" persists a grant; "Allow all edits" switches to auto mode. */
788
+ async afterDecision(request, decision) {
789
+ if (decision.decided_by !== "user" || request.subject_kind !== "action")
790
+ return;
791
+ if ((decision.outcome === "allowed-once" || decision.outcome === "allowed-for-scope") && isPlainGitPush(request.command))
792
+ this.runtime.approveGitPushForSession();
793
+ if (decision.outcome !== "allowed-for-scope")
794
+ return;
795
+ const prefix = suggestedCommandPrefix(request.command);
796
+ if (prefix !== undefined) {
797
+ await this.addGrant(prefix, "prompt");
798
+ return;
799
+ }
800
+ if (request.effect === "workspace-write" && this.runtime.permissionMode() === "ask")
801
+ this.setMode("auto");
802
+ }
803
+ fullAccessNotice() {
804
+ const trusted = this.runtime.trust.recorded().trusted;
805
+ return `${this.glyphs.warn} Full access: Synorch edits and runs any command in this folder without asking, except destructive commands (hard rails still apply)${trusted ? "" : "; the folder is trusted for this session only (not saved)"} ${this.glyphs.sep} Shift+Tab leaves`;
806
+ }
807
+ /**
808
+ * The conversation's decorations around the one gateway: the trust question at the first command
809
+ * that runs repository code, the current policy (a trust, /allow or plan-mode change applies at
810
+ * once), and a checkpoint of every successful edit. Policy, rails and audit stay in the gateway.
811
+ */
812
+ wrap(inner) {
813
+ return {
814
+ invoke: async (request, scope, signal) => {
815
+ const declined = request.tool_name === "exec" && !this.planOn ? await this.trustGate(request, signal) : false;
816
+ const captured = WRITE_TOOLS.has(request.tool_name) && !this.planOn ? await this.capture(request).catch(() => undefined) : undefined;
817
+ // K3: a file a live task of a background run owns is refused (the gateway records the denial).
818
+ const owned = captured === undefined ? undefined : this.ownedByRun(captured);
819
+ this.trustDeclinedNow = declined;
820
+ let outcome;
821
+ try {
822
+ outcome = await inner.invoke(request, { ...scope, policy: owned === undefined ? this.policy() : runLockPolicy(this.policy()) }, signal);
823
+ }
824
+ finally {
825
+ this.trustDeclinedNow = false;
826
+ }
827
+ if (owned !== undefined && outcome.state === "denied" && outcome.result.error !== undefined) {
828
+ const message = `${owned.path} is owned by task ${owned.task} of the background worker run ${owned.run}, which is still running: editing it now would conflict when the run integrates. Wait for the run (run_status with wait: true), relay the change to the worker (run_steer with task: ${owned.task}), or ask the user to cancel it; files no task owns can be edited now.`;
829
+ return { ...outcome, result: { ...outcome.result, error: { ...outcome.result.error, message: message.slice(0, 2000) } } };
830
+ }
831
+ if (captured !== undefined && captured.length > 0 && outcome.state === "succeeded") {
832
+ await this.checkpoint(request, captured, outcome.result.changed_paths ?? []).catch(() => undefined);
833
+ }
834
+ if (this.planOn && outcome.state === "denied" && outcome.result.error !== undefined && (WRITE_TOOLS.has(request.tool_name) || request.tool_name === "exec")) {
835
+ const message = `${outcome.result.error.message} (plan mode is on: read, discuss and propose the plan; the user leaves plan mode to carry it out)`.slice(0, 2000);
836
+ return { ...outcome, result: { ...outcome.result, error: { ...outcome.result.error, message } } };
837
+ }
838
+ return outcome;
839
+ },
840
+ };
841
+ }
842
+ /** The trust question at the first repo-code command; resolves true when the user just declined it for this call. */
843
+ async trustGate(request, signal) {
844
+ const runtime = this.runtime;
845
+ if (!this.trustAsked && runtime.permissionMode() === "auto" && !runtime.trust.recorded().trusted && runtime.sandbox.enforcement !== "full") {
846
+ // Owner revision 3: auto trusts the folder for this session without a question; one notice, nothing saved.
847
+ this.trustAsked = true;
848
+ this.renderer.render({ kind: "notice", level: "info", message: "Auto mode: trusting this folder for this session (not saved) so commands run without asking; /trust saves it." });
849
+ return false;
850
+ }
851
+ if (this.trustAsked || runtime.trust.state().trusted || runtime.sandbox.enforcement === "full")
852
+ return false;
853
+ if (this.renderer.approvals.availability !== "interactive")
854
+ return false;
855
+ const argv = strings(request.arguments.argv);
856
+ if (argv === undefined || argv.length === 0)
857
+ return false;
858
+ const policy = this.basePolicy();
859
+ const verdict = evaluateExecAllowlist({
860
+ argv,
861
+ readOnly: false,
862
+ confinement: policy.exec_confinement ?? "allowlist",
863
+ mode: policy.mode,
864
+ verificationCommands: [],
865
+ workspaceTrusted: false,
866
+ commandGrants: policy.command_grants ?? [],
867
+ });
868
+ if (verdict?.code !== WORKSPACE_UNTRUSTED_CODE)
869
+ return false;
870
+ this.trustAsked = true;
871
+ if (await promptTrustForCommand(runtime, this.renderer, argv.join(" "), signal)) {
872
+ this.policyCache = undefined;
873
+ return false;
874
+ }
875
+ return true;
876
+ }
877
+ inside(declared) {
878
+ const root = this.runtime.workspaceRoot;
879
+ const absolute = path.resolve(root, declared);
880
+ const relative = path.relative(root, absolute);
881
+ if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative))
882
+ return undefined;
883
+ return { relative: relative.split(path.sep).join("/"), absolute };
884
+ }
885
+ async capture(request) {
886
+ const args = request.arguments;
887
+ const declared = request.tool_name === "write_file" ? [typeof args.path === "string" ? args.path : ""] : patchPaths(typeof args.patch === "string" ? args.patch : "");
888
+ const files = [];
889
+ for (const entry of declared) {
890
+ const target = entry === "" ? undefined : this.inside(entry);
891
+ if (target === undefined || files.some((file) => file.relative === target.relative))
892
+ continue;
893
+ files.push({ ...target, before: await readOptional(target.absolute) });
894
+ }
895
+ return files;
896
+ }
897
+ /** K3: the first file a live task of the background run owns, if any. */
898
+ ownedByRun(files) {
899
+ const run = this.backgroundRun();
900
+ if (run === undefined)
901
+ return undefined;
902
+ for (const file of files) {
903
+ const task = run.tracker.ownerOf(file.relative, this.runtime.platform);
904
+ if (task !== undefined)
905
+ return { path: file.relative, task, run: run.id };
906
+ }
907
+ return undefined;
908
+ }
909
+ async checkpoint(request, captured, changed) {
910
+ const fold = (value) => (this.runtime.platform === "win32" || this.runtime.platform === "darwin" ? value.toLowerCase() : value);
911
+ const touched = new Set(changed.map(fold));
912
+ const files = [];
913
+ for (const file of captured) {
914
+ if (!touched.has(fold(file.relative)))
915
+ continue;
916
+ const after = await readOptional(file.absolute);
917
+ files.push({
918
+ path: file.relative,
919
+ before: file.before === undefined ? null : await this.runtime.blobs.put(new Uint8Array(file.before), "application/octet-stream"),
920
+ after: after === undefined ? null : workspaceDigest(new Uint8Array(after)),
921
+ });
922
+ }
923
+ if (files.length === 0)
924
+ return;
925
+ await this.append("checkpoint/recorded", { ...(this.turnId === undefined ? {} : { turn_id: this.turnId }), tool_call_id: request.tool_call_id, files }, "agent");
926
+ }
927
+ // ---- the loop ---------------------------------------------------------------------------------
928
+ enqueue(text, attachments = []) {
929
+ this.queued.push({ text, attachments });
930
+ }
931
+ async loop(input) {
932
+ const concurrent = this.renderer.kind === "tui";
933
+ for (;;) {
934
+ if (this.exiting || this.outer.signal.aborted)
935
+ return;
936
+ let message = this.queued.shift();
937
+ if (message === undefined && this.unreportedRuns > 0) {
938
+ // K3: a background run ended while the session was idle: a short follow-up turn reports it.
939
+ const work = this.turn("", [], true);
940
+ const left = concurrent ? await this.alongside(work, input) : await work;
941
+ if (left === true)
942
+ return;
943
+ continue;
944
+ }
945
+ if (message === undefined) {
946
+ const wake = new AbortController();
947
+ this.wakeIdle = wake;
948
+ let next;
949
+ try {
950
+ next = await input.next(AbortSignal.any([this.outer.signal, wake.signal]));
951
+ }
952
+ catch {
953
+ if (wake.signal.aborted && !this.outer.signal.aborted)
954
+ continue;
955
+ return;
956
+ }
957
+ if (next.kind === "exit") {
958
+ if (await this.confirmExit(next.command === true))
959
+ return;
960
+ continue;
961
+ }
962
+ if (!("text" in next))
963
+ continue;
964
+ const text = next.text.trim();
965
+ const attachments = next.attachments ?? [];
966
+ if (text === "" && attachments.length === 0)
967
+ continue;
968
+ message = { text, attachments };
969
+ if (next.kind === "command" && !text.startsWith("/"))
970
+ message = { text: `/${text}`, attachments };
971
+ }
972
+ const work = message.verbatim === true ? this.turn(message.text, [], false, true) : message.text.startsWith("/") ? this.command(message.text) : this.turn(message.text, message.attachments);
973
+ const left = concurrent ? await this.alongside(work, input) : await work;
974
+ if (left === true && (await this.confirmExit(true)))
975
+ return;
976
+ }
977
+ }
978
+ /**
979
+ * While work runs in the TUI: a typed message answers a pending question, steers the running
980
+ * workers (coordinator) or the running turn (next step boundary); commands marked `whileBusy` run
981
+ * at once, other commands wait for the turn to end; /exit and Ctrl+D leave.
982
+ */
983
+ async alongside(work, input) {
984
+ const stop = new AbortController();
985
+ const reader = (async () => {
986
+ for (;;) {
987
+ let next;
988
+ try {
989
+ next = await input.next(AbortSignal.any([stop.signal, this.outer.signal]));
990
+ }
991
+ catch {
992
+ return;
993
+ }
994
+ if (next.kind === "exit") {
995
+ if (await this.confirmExit(next.command === true)) {
996
+ this.hardExit();
997
+ return;
998
+ }
999
+ continue;
1000
+ }
1001
+ if (!("text" in next))
1002
+ continue;
1003
+ const text = next.text.trim();
1004
+ const attachments = next.attachments ?? [];
1005
+ if (text === "" && attachments.length === 0)
1006
+ continue;
1007
+ if (next.kind === "command" || text.startsWith("/")) {
1008
+ const command = findConversationCommand(text.split(/\s+/)[0] ?? "");
1009
+ if (command?.whileBusy === true) {
1010
+ if (await this.command(text)) {
1011
+ if (!(await this.confirmExit(true)))
1012
+ continue;
1013
+ this.hardExit();
1014
+ return;
1015
+ }
1016
+ }
1017
+ else {
1018
+ this.enqueue(text);
1019
+ this.note("info", `queued ${this.glyphs.name === "rich" ? "›" : ">"} ${text} (runs when Synorch is done)`);
1020
+ }
1021
+ continue;
1022
+ }
1023
+ if (this.desk.answer(text))
1024
+ continue;
1025
+ this.steer(text, attachments);
1026
+ }
1027
+ })();
1028
+ try {
1029
+ return await work;
1030
+ }
1031
+ finally {
1032
+ stop.abort();
1033
+ await reader;
1034
+ }
1035
+ }
1036
+ /**
1037
+ * Mid-turn steering (ADR-21 D8): to the coordinator while a foreground run holds the turn, else to
1038
+ * the running turn, else the next turn. K3: with the run in the background a message goes to the
1039
+ * agent, which relays corrections to the workers with run_steer.
1040
+ */
1041
+ steer(text, attachments) {
1042
+ const arrow = this.glyphs.name === "rich" ? "↳" : "->";
1043
+ const orchestration = this.orchestration;
1044
+ if (orchestration !== undefined && orchestration.foreground) {
1045
+ orchestration.coordinator.steer(text);
1046
+ this.note("info", `${arrow} steering the workers: ${text.trim()} (applied at the next safe point)`);
1047
+ return;
1048
+ }
1049
+ if (this.turnRunning && this.driver !== undefined && attachments.length === 0) {
1050
+ this.driver.steer(text);
1051
+ this.note("info", `${arrow} Synorch reads this at its next step`);
1052
+ return;
1053
+ }
1054
+ this.enqueue(text, attachments);
1055
+ this.note("info", `queued ${this.glyphs.name === "rich" ? "›" : ">"} ${text}`);
1056
+ }
1057
+ /** Esc: interrupts the turn; while a foreground run holds the turn, the first Esc arms and the second stops it. Background runs keep going. */
1058
+ interrupt() {
1059
+ const orchestration = this.orchestration;
1060
+ if (orchestration === undefined || !orchestration.foreground) {
1061
+ this.active?.abort();
1062
+ // K4.2: background processes survive an interrupted turn; a second Esc within the window stops them.
1063
+ const running = this.runtime.processes.running().length;
1064
+ if (running > 0) {
1065
+ if (this.processStopArmedAt !== undefined && Date.now() - this.processStopArmedAt <= ESC_ARM_MS) {
1066
+ this.processStopArmedAt = undefined;
1067
+ void this.runtime.processes.killAll().then((count) => this.note("warning", `Stopped ${count} background process${count === 1 ? "" : "es"}`));
1068
+ }
1069
+ else {
1070
+ this.processStopArmedAt = Date.now();
1071
+ this.note("info", `${running} background process${running === 1 ? " is" : "es are"} still running ${this.glyphs.sep} Esc again stops ${running === 1 ? "it" : "them"} ${this.glyphs.sep} /ps lists them`);
1072
+ }
1073
+ }
1074
+ if (orchestration !== undefined)
1075
+ this.note("info", `Workers keep running in the background (${orchestration.id}) ${this.glyphs.sep} /runs cancel stops them`);
1076
+ return;
1077
+ }
1078
+ if (!orchestration.tracker.stopArmed) {
1079
+ orchestration.tracker.stopArmed = true;
1080
+ this.renderer.views?.setBoard(orchestration.tracker.view());
1081
+ this.note("warning", `Press Esc again to stop the workers ${this.glyphs.sep} messages you type steer them instead`);
1082
+ orchestration.disarm = setTimeout(() => {
1083
+ orchestration.tracker.stopArmed = false;
1084
+ this.renderer.views?.setBoard(orchestration.tracker.view());
1085
+ }, ESC_ARM_MS);
1086
+ orchestration.disarm.unref?.();
1087
+ return;
1088
+ }
1089
+ this.note("warning", "Stopping the workers…");
1090
+ orchestration.controller.abort();
1091
+ }
1092
+ /**
1093
+ * Every question the session asks the user (ask_user, /init, /memory review, /config, /commit,
1094
+ * /model --save). The TUI opens a prompt that owns the input (a picker for choices); plain mode
1095
+ * reads a numbered answer. Either way the answer never becomes a conversation message. A numeric
1096
+ * answer resolves to its option's text.
1097
+ */
1098
+ /**
1099
+ * K5 structured question (the agent's ask_user, Claude Code's AskUserQuestion): the TUI's choice
1100
+ * modal, or in plain mode the numbered question read from the input. Undefined when dismissed.
1101
+ */
1102
+ async choose(question, signal) {
1103
+ const controls = this.renderer.controls;
1104
+ if (this.renderer.kind === "tui" && controls !== undefined)
1105
+ return controls.choose(question, signal);
1106
+ for (const line of choiceQuestionLines(question))
1107
+ this.note(line.startsWith("?") ? "warning" : "info", line);
1108
+ const input = this.renderer.input;
1109
+ for (;;) {
1110
+ let typed;
1111
+ if (this.readingDuringWorkers || input === undefined)
1112
+ typed = await this.desk.ask(signal);
1113
+ else {
1114
+ const next = await input.next(signal);
1115
+ if (!("text" in next))
1116
+ return undefined;
1117
+ typed = next.text;
1118
+ }
1119
+ if (typed.trim() === "")
1120
+ continue;
1121
+ const answer = parseTypedChoice(question, typed);
1122
+ if (answer !== undefined)
1123
+ return answer;
1124
+ this.note("info", " type one of the numbers above");
1125
+ }
1126
+ }
1127
+ /** An agent's question leaves one short line in the transcript (`? Auth method → OAuth`), not the modal. */
1128
+ async chooseForAgent(question, signal) {
1129
+ const answer = await this.choose(question, signal);
1130
+ this.note("info", askUserSummaryLine(question, answer));
1131
+ return answer;
1132
+ }
1133
+ async askUser(question, options, signal) {
1134
+ const controls = this.renderer.controls;
1135
+ if (this.renderer.kind === "tui" && controls !== undefined) {
1136
+ const answer = await controls.ask(question, options, signal);
1137
+ if (answer === undefined)
1138
+ throw new DOMException("the question was cancelled", "AbortError");
1139
+ return answer;
1140
+ }
1141
+ this.note("warning", `? ${question}`);
1142
+ if (options !== undefined && options.length > 0)
1143
+ this.note("info", ` ${options.map((option, index) => `${index + 1}. ${option}`).join(" ")}`);
1144
+ this.note("info", options !== undefined && options.length > 0 ? " type a number (or your answer) and press Enter" : " type your answer and press Enter");
1145
+ const resolve = (typed) => {
1146
+ const answer = typed.trim();
1147
+ const picked = options?.[Number(answer) - 1];
1148
+ return /^\d+$/.test(answer) && picked !== undefined ? picked : answer;
1149
+ };
1150
+ if (this.renderer.kind === "tui" || this.readingDuringWorkers)
1151
+ return resolve(await this.desk.ask(signal));
1152
+ const input = this.renderer.input;
1153
+ if (input === undefined)
1154
+ throw new DOMException("no input", "AbortError");
1155
+ for (;;) {
1156
+ const next = await input.next(signal);
1157
+ if (!("text" in next))
1158
+ throw new DOMException("the question was not answered", "AbortError");
1159
+ if (next.text.trim() === "")
1160
+ continue;
1161
+ return resolve(next.text);
1162
+ }
1163
+ }
1164
+ async currentRoute() {
1165
+ try {
1166
+ return await (this.routePromise ?? Promise.reject(new Error("no route")));
1167
+ }
1168
+ catch (error) {
1169
+ this.showFailure(failureInfo(error));
1170
+ this.routePromise = this.runtime.router.resolve({ tier: sessionRouteRule(this.runtime, this.sessionTier)?.tier ?? "orchestrator", role: "session" }, this.outer.signal);
1171
+ this.routePromise.catch(() => undefined);
1172
+ return undefined;
1173
+ }
1174
+ }
1175
+ /** One turn; `followUp` (K3) is the harness's own turn that reports a finished background run (no user text). */
1176
+ async turn(typed, attachments, followUp = false, verbatim = false) {
1177
+ this.submittedAt = followUp ? undefined : performance.now();
1178
+ let text = followUp ? "" : typed;
1179
+ if (followUp && this.unreportedRuns === 0)
1180
+ return false;
1181
+ if (!followUp && this.planOn && GO_WORDS.test(text.trim())) {
1182
+ this.setPlanMode(false);
1183
+ text = `${text}\n(The plan is approved: carry it out now.)`;
1184
+ }
1185
+ let log;
1186
+ try {
1187
+ log = await this.ensureLog(text.split(ATTACHMENTS_OPEN)[0] ?? text);
1188
+ }
1189
+ catch (error) {
1190
+ this.showFailure(failureInfo(error));
1191
+ return false;
1192
+ }
1193
+ const driver = this.ensureDriver(log);
1194
+ const route = await this.currentRoute();
1195
+ if (route === undefined)
1196
+ return false;
1197
+ if (!this.routeRecorded) {
1198
+ this.routeRecorded = true;
1199
+ await this.append("route/decided", { decision: route }, "agent").catch(() => undefined);
1200
+ }
1201
+ let body = text;
1202
+ let images = [];
1203
+ if (!verbatim && (attachments.length > 0 || /(^|\s)@\S/.test(text))) {
1204
+ const support = mayContainImage(text, attachments) ? await this.imageSupport(route) : { send: false, reason: "" };
1205
+ const blobs = this.runtime.blobs;
1206
+ const resolved = await resolveAttachments(text, attachments, {
1207
+ workspaceRoot: this.runtime.workspaceRoot,
1208
+ model: route.route.model_id,
1209
+ images: { ...support, put: (bytes, mediaType) => blobs.put(bytes, mediaType) },
1210
+ });
1211
+ for (const entry of resolved.notices)
1212
+ this.note(entry.level, entry.text);
1213
+ body = resolved.message;
1214
+ images = resolved.images;
1215
+ }
1216
+ // K7 hooks: SessionStart once, UserPromptSubmit per typed prompt (a block drops the prompt).
1217
+ const claudeNative = route.route.adapter_kind === "agent-backend" && (this.runtime.config.claudeCodeMode ?? "native") === "native";
1218
+ const hookContext = [];
1219
+ if (!this.sessionStartHooked) {
1220
+ this.sessionStartHooked = true;
1221
+ await this.reviewChangedHooks();
1222
+ const source = this.parsed.resume !== undefined || this.parsed.continue ? "resume" : this.parsed.fork !== undefined ? "fork" : "startup";
1223
+ hookContext.push(...((await this.runHook("SessionStart", { source }, [source], claudeNative, log.sessionId))?.context ?? []));
1224
+ }
1225
+ if (!followUp && !verbatim) {
1226
+ const submitted = await this.runHook("UserPromptSubmit", { prompt: typed }, [], claudeNative, log.sessionId);
1227
+ if (submitted?.blocked === true) {
1228
+ this.note("error", `Prompt blocked by a UserPromptSubmit hook: ${submitted.reason ?? "no reason given"}`);
1229
+ return false;
1230
+ }
1231
+ hookContext.push(...(submitted?.context ?? []));
1232
+ }
1233
+ if (hookContext.length > 0)
1234
+ body = `${body}\n${ATTACHMENTS_OPEN}\nContext from hooks:\n${hookContext.join("\n")}\n</synorch-attachments>`;
1235
+ const notes = this.pendingNotes.splice(0);
1236
+ // K3: completion notes ride on this turn; a follow-up turn is only needed for later ones.
1237
+ this.unreportedRuns = 0;
1238
+ for (const run of this.workerRuns)
1239
+ run.notice = undefined;
1240
+ if (followUp)
1241
+ notes.push("there is no new message from the user: tell the user in a few lines what the finished worker run did (from the result above) and the next step.");
1242
+ const message = notes.length === 0 ? body : `[Synorch note: ${notes.join(" ").replaceAll("]", ")")}]\n${body}`;
1243
+ const active = linked(this.outer.signal);
1244
+ this.active = active;
1245
+ this.turnRunning = true;
1246
+ let outcome;
1247
+ try {
1248
+ outcome = await driver.runTurn({
1249
+ sessionId: log.sessionId,
1250
+ runId: undefined,
1251
+ taskId: undefined,
1252
+ attemptId: undefined,
1253
+ role: "session",
1254
+ route: route.route,
1255
+ policy: this.policy(),
1256
+ packet: undefined,
1257
+ userMessage: message,
1258
+ ...(images.length === 0 ? {} : { userImages: images }),
1259
+ trigger: followUp ? "follow-up" : "user",
1260
+ maxSteps: MAX_STEPS,
1261
+ }, active.signal);
1262
+ }
1263
+ catch (error) {
1264
+ if (!active.signal.aborted)
1265
+ this.showFailure(failureInfo(error));
1266
+ }
1267
+ finally {
1268
+ this.turnRunning = false;
1269
+ if (this.active === active)
1270
+ this.active = undefined;
1271
+ this.submittedAt = undefined;
1272
+ }
1273
+ // A message typed while the last step settled could not be delivered in this turn: it starts the next one.
1274
+ const leftover = driver.drainSteers?.() ?? [];
1275
+ if (leftover.length > 0)
1276
+ this.queued.unshift({ text: leftover.join("\n"), attachments: [] });
1277
+ // K7 Stop hooks: a block continues the conversation with the hook's reason (at most 3 times in a row).
1278
+ if (outcome?.outcome === "completed" && leftover.length === 0 && !active.signal.aborted) {
1279
+ const stopped = await this.runHook("Stop", { stop_hook_active: this.stopContinuations > 0 }, [], claudeNative, log.sessionId);
1280
+ if (stopped?.blocked === true && this.stopContinuations < MAX_STOP_CONTINUATIONS) {
1281
+ this.stopContinuations += 1;
1282
+ this.queued.unshift({ text: `[Synorch note: a Stop hook asked you to continue: ${(stopped.reason ?? "keep going").replaceAll("]", ")")}]`, attachments: [], verbatim: true });
1283
+ }
1284
+ else
1285
+ this.stopContinuations = 0;
1286
+ }
1287
+ else
1288
+ this.stopContinuations = 0;
1289
+ if (this.planOn && outcome?.outcome === "completed" && leftover.length === 0) {
1290
+ this.note("info", `${this.glyphs.bullet} Plan mode ${this.glyphs.sep} /go carries it out here ${this.glyphs.sep} /go workers runs it with workers ${this.glyphs.sep} or keep refining`);
1291
+ }
1292
+ await this.memoryNudge();
1293
+ return false;
1294
+ }
1295
+ // ---- K2 memory: start summary, proposal nudge, the desk --------------------------------------
1296
+ memoryNudged = 0;
1297
+ /** `memory: 3 decisions, 1 open assumption` at session start (nothing for an empty vault). */
1298
+ async memoryStartLine() {
1299
+ const store = this.runtime.memory;
1300
+ const summary = await readLedger(store, this.runtime.projectId, this.runtime.gitBranch).then(ledgerSummary, () => undefined);
1301
+ if (summary !== undefined)
1302
+ this.startupLine({ level: "info", text: `${this.glyphs.bullet} ${summary}`, context: true });
1303
+ }
1304
+ startupLine(notice) {
1305
+ if (this.notices === undefined)
1306
+ this.note(notice.level, notice.text);
1307
+ else
1308
+ this.notices.add(notice);
1309
+ }
1310
+ /** After a turn: `📌 2 memory proposals · /memory review` when the agent proposed something new this session. */
1311
+ async memoryNudge() {
1312
+ const since = new Date(this.startedAt).toISOString();
1313
+ const fresh = await this.runtime.memory.pending().then((pending) => pending.filter((proposal) => proposal.state === "pending" && proposal.created_at >= since).length, () => 0);
1314
+ if (fresh <= this.memoryNudged)
1315
+ return;
1316
+ this.memoryNudged = fresh;
1317
+ this.note("info", `${this.glyphs.name === "rich" ? "📌" : "*"} ${fresh} memory proposal${fresh === 1 ? "" : "s"} ${this.glyphs.sep} /memory review`);
1318
+ }
1319
+ memoryDesk() {
1320
+ const views = this.renderer.views;
1321
+ const host = {
1322
+ store: this.runtime.memory,
1323
+ projectId: this.runtime.projectId,
1324
+ branch: this.runtime.gitBranch,
1325
+ obsidian: createSystemObsidianLauncher(this.io.env, this.io.platform),
1326
+ print: (lines) => this.print(lines),
1327
+ show: (view) => {
1328
+ if (views === undefined)
1329
+ return false;
1330
+ views.showView(view);
1331
+ return true;
1332
+ },
1333
+ ask: (question, options) => this.askUser(question, options, this.outer.signal),
1334
+ append: async (type, data) => {
1335
+ await this.append(type, data, "user");
1336
+ },
1337
+ bootstrap: () => runMemoryInit(this.runtime),
1338
+ };
1339
+ views?.connectMemory?.(memorySeam(host));
1340
+ return host;
1341
+ }
1342
+ /**
1343
+ * Whether this route sends images: the direct OpenAI Responses and Anthropic Messages adapters do
1344
+ * unless the model's capability says `unsupported`; other routes (the Claude Code bridge, Codex
1345
+ * app-server, scripted) get a notice instead.
1346
+ */
1347
+ async imageSupport(route) {
1348
+ const adapterId = route.route.adapter_id;
1349
+ if (!IMAGE_ADAPTERS.has(adapterId)) {
1350
+ return { send: false, reason: `the ${adapterId} route does not take image input` };
1351
+ }
1352
+ let level = "unknown";
1353
+ try {
1354
+ const adapter = this.runtime.router.adapterFor(route.route);
1355
+ const capabilities = await adapter.discoverCapabilities(this.outer.signal);
1356
+ level = capabilities.models.find((model) => model.id === route.route.model_id)?.image_input ?? "unknown";
1357
+ }
1358
+ catch {
1359
+ // Capability discovery failing does not block the image; the provider will refuse it if needed.
1360
+ }
1361
+ return level === "unsupported" ? { send: false, reason: `${route.route.model_id} does not take image input` } : { send: true, reason: "" };
1362
+ }
1363
+ showFailure(error) {
1364
+ const workspace = error.workspace_effect === "none" ? "unchanged" : error.workspace_effect === "unknown" ? "may have changed · check git status" : "partly changed";
1365
+ this.note("error", `${this.glyphs.fail} ${error.message}`);
1366
+ this.note("info", ` Workspace ${workspace}`);
1367
+ if (error.next_command !== undefined)
1368
+ this.note("info", ` Next ${error.next_command}`);
1369
+ }
1370
+ // ---- orchestration (the `orchestrate` tool, ADR-21 D4/D5) -----------------------------------
1371
+ observeOrchestration(orchestration, event) {
1372
+ const tracker = orchestration.tracker;
1373
+ const before = tracker.sessionId;
1374
+ if (!tracker.observe(event))
1375
+ return;
1376
+ const g = this.glyphs;
1377
+ if (event.type === "plan/proposed" && event.session_id === tracker.sessionId) {
1378
+ const plan = event.data.plan;
1379
+ const stop = orchestration.foreground ? ` ${g.sep} esc to stop` : "";
1380
+ this.note("info", `${g.bullet} Plan ${g.sep} ${plan.tasks.length} task${plan.tasks.length === 1 ? "" : "s"} ${g.sep} risk ${plan.risk}${this.runtime.policyMode === "autonomous" ? ` ${g.sep} starting now${stop}` : ""}`);
1381
+ // Task goals are what the user checks the plan by: shown in full (the renderer wraps them).
1382
+ for (const [index, task] of plan.tasks.entries())
1383
+ this.note("info", ` ${index + 1}. ${task.key} (${task.role}): ${task.objective.trim()}`);
1384
+ }
1385
+ if (before === undefined && tracker.sessionId !== undefined)
1386
+ this.orchestratedSessions.push(tracker.sessionId);
1387
+ const views = this.renderer.views;
1388
+ if (views !== undefined) {
1389
+ // The TUI board updates in place (and ticks at 1 Hz); the plain board prints only on task changes.
1390
+ if (this.renderer.kind === "tui" || event.type === "task/state_changed" || event.type === "task/created")
1391
+ views.setBoard(tracker.view());
1392
+ return;
1393
+ }
1394
+ if (event.type === "task/state_changed") {
1395
+ const line = tracker.line(event.data.task_id);
1396
+ if (line !== undefined)
1397
+ this.note("info", line);
1398
+ }
1399
+ }
1400
+ async runOrchestration(input, context) {
1401
+ if (this.planOn)
1402
+ return toolError("policy_denied", "plan mode is on: present the plan; the user starts workers with /go workers");
1403
+ const current = this.orchestration;
1404
+ if (current !== undefined) {
1405
+ return toolError("execution_failed", `workers are already running in this conversation (${current.id}): check them with run_status, relay changes with run_steer, or wait for the end with run_status {wait: true}`);
1406
+ }
1407
+ const runtime = this.runtime;
1408
+ if (!this.trustAsked && !runtime.trust.state().trusted && runtime.sandbox.enforcement !== "full" && this.renderer.approvals.availability === "interactive") {
1409
+ this.trustAsked = true;
1410
+ if (await promptTrustForCommand(runtime, this.renderer, "the workers' checks", context.signal))
1411
+ this.policyCache = undefined;
1412
+ }
1413
+ // K3: an interactive session runs workers in the background unless the agent asks to wait; headless always waits.
1414
+ const background = input.wait !== true && this.canRunInBackground();
1415
+ const run = this.startRun(input, !background, context.signal);
1416
+ if (!background)
1417
+ return run.done;
1418
+ // The tool row's summary is the one on-screen line for this ("Workers running in background (run-1) · keep chatting · /runs").
1419
+ const text = [
1420
+ `Started worker run ${run.id} in the background. The user sees the plan and a live board; the conversation is not blocked.`,
1421
+ `Goal: ${snippet(input.goal, 400)}`,
1422
+ "End your turn now with one short line to the user (or keep helping them with something else). Do not wait or poll.",
1423
+ "When the run ends you receive a Synorch note with its result block; report from it then.",
1424
+ "run_status shows progress (wait: true only if you cannot continue without the result), run_steer relays a correction to the orchestrator or one task, run_cancel stops it.",
1425
+ "Until it ends, files its tasks own are refused to your edits (run_status lists them); other small edits are fine.",
1426
+ ].join("\n");
1427
+ return { status: "ok", text, truncated: false, redactions: 0 };
1428
+ }
1429
+ /** K3: a human is attached and input is read between turns, so a run can go to the background. */
1430
+ canRunInBackground() {
1431
+ return this.io.stdinIsTTY && this.renderer.input !== undefined && this.renderer.approvals.availability === "interactive";
1432
+ }
1433
+ /** The active run when it runs in the background (undefined for none or a foreground run). */
1434
+ backgroundRun() {
1435
+ const run = this.orchestration;
1436
+ return run !== undefined && !run.foreground ? run : undefined;
1437
+ }
1438
+ startRun(input, foreground, turnSignal) {
1439
+ const runtime = this.runtime;
1440
+ this.coordinator ??= runtime.createCoordinator(runtime.brokerFor(this.gatedApprovals()));
1441
+ const coordinator = this.coordinator;
1442
+ this.connectWorkers(coordinator);
1443
+ const goal = input.brief === undefined ? input.goal : `${input.goal}\n\nContext from the conversation:\n${input.brief}`;
1444
+ const tracker = new OrchestrationTracker(goal, input.reason);
1445
+ this.lastTracker = tracker;
1446
+ // A background run belongs to the session (it ends with it); a foreground run also to the turn (interrupting the turn stops it).
1447
+ const controller = linked(this.outer.signal);
1448
+ if (foreground) {
1449
+ if (turnSignal.aborted)
1450
+ controller.abort(turnSignal.reason);
1451
+ else
1452
+ turnSignal.addEventListener("abort", () => controller.abort(turnSignal.reason), { once: true });
1453
+ }
1454
+ const run = {
1455
+ id: `run-${this.workerRuns.length + 1}`,
1456
+ goal: input.goal,
1457
+ tracker,
1458
+ coordinator,
1459
+ controller,
1460
+ disarm: undefined,
1461
+ foreground,
1462
+ status: "running",
1463
+ done: Promise.resolve({ status: "ok", text: "", truncated: false, redactions: 0 }),
1464
+ result: undefined,
1465
+ notice: undefined,
1466
+ };
1467
+ this.orchestration = run;
1468
+ this.workerRuns.push(run);
1469
+ const views = this.renderer.views;
1470
+ // The rationale is audit, not transcript: it is in the session log and in `/runs <id>` (Why:).
1471
+ // ADR-07/19: without git, writing workers edit in place one at a time (scoped-dir with a revertable snapshot).
1472
+ if (this.workerRuns.length === 1 && !insideGitRepository(runtime.workspaceRoot)) {
1473
+ this.note("info", `No git repository here: workers edit in place one at a time ${this.glyphs.sep} git init and a first commit give each worker its own worktree`);
1474
+ }
1475
+ if (this.renderer.kind === "tui")
1476
+ views?.setBoard(tracker.view());
1477
+ run.done = this.driveRun(run, goal);
1478
+ return run;
1479
+ }
1480
+ /** Runs the coordinator to the end; the result block (or error) is the run's tool result. Never rejects. */
1481
+ async driveRun(run, goal) {
1482
+ const g = this.glyphs;
1483
+ const runtime = this.runtime;
1484
+ const { tracker, controller } = run;
1485
+ const views = this.renderer.views;
1486
+ const ticker = views === undefined || this.renderer.kind !== "tui" ? undefined : setInterval(() => views.setBoard(tracker.view()), 1000);
1487
+ ticker?.unref?.();
1488
+ const stopReading = run.foreground ? this.readWhileWorkersRun() : async () => undefined;
1489
+ let result;
1490
+ try {
1491
+ const outcome = await run.coordinator.run({
1492
+ goal,
1493
+ workspaceRoot: runtime.workspaceRoot,
1494
+ policyMode: runtime.policyMode,
1495
+ headless: this.renderer.approvals.availability === "headless",
1496
+ resumeSessionId: undefined,
1497
+ budget: runtime.config.budget,
1498
+ }, controller.signal);
1499
+ if (!this.orchestratedSessions.includes(outcome.sessionId))
1500
+ this.orchestratedSessions.push(outcome.sessionId);
1501
+ tracker.finish(outcome.status);
1502
+ run.status = outcome.status;
1503
+ const block = tracker.resultBlock({ status: outcome.status, summary: outcome.summary, runId: outcome.runId, sessionId: outcome.sessionId });
1504
+ if (ticker !== undefined)
1505
+ clearInterval(ticker);
1506
+ const view = tracker.view();
1507
+ // A done board collapses to its summary, pinned once (U3); the plain renderer prints it.
1508
+ views?.setBoard(view);
1509
+ if (views === undefined) {
1510
+ const seconds = Math.round(((view.endedAtMs ?? Date.now()) - (view.startedAtMs ?? Date.now())) / 1000);
1511
+ const status = outcome.status === "succeeded" ? "done" : outcome.status;
1512
+ this.note(outcome.status === "succeeded" ? "info" : "warning", `${g.bullet} Workers ${g.sep} ${view.tasks.length} task${view.tasks.length === 1 ? "" : "s"} ${g.sep} ${status} in ${seconds}s`);
1513
+ for (const task of view.tasks) {
1514
+ const glyph = task.state === "completed" ? g.ok : task.state === "failed" || task.state === "cancelled" || task.state === "blocked" ? g.fail : g.bullet;
1515
+ this.note("info", ` ${glyph} ${task.key.padEnd(18)} ${task.role.padEnd(12)} ${task.summary ?? task.reason ?? task.state.replaceAll("_", " ")}`);
1516
+ }
1517
+ }
1518
+ for (const line of tracker.resultLines())
1519
+ this.note("info", ` ${line}`);
1520
+ result =
1521
+ outcome.status === "succeeded"
1522
+ ? { status: "ok", text: block, truncated: false, redactions: 0 }
1523
+ : toolError(outcome.status === "cancelled" ? "cancelled" : "execution_failed", `the orchestration ${outcome.status}: ${snippet(outcome.summary, 400)}`, block);
1524
+ }
1525
+ catch (caught) {
1526
+ const cancelled = controller.signal.aborted;
1527
+ tracker.finish(cancelled ? "cancelled" : "failed");
1528
+ run.status = cancelled ? "cancelled" : "failed";
1529
+ views?.setBoard(tracker.view());
1530
+ result = toolError(cancelled ? "cancelled" : "execution_failed", failureInfo(caught).message);
1531
+ }
1532
+ finally {
1533
+ if (ticker !== undefined)
1534
+ clearInterval(ticker);
1535
+ if (run.disarm !== undefined)
1536
+ clearTimeout(run.disarm);
1537
+ if (this.orchestration === run)
1538
+ this.orchestration = undefined;
1539
+ await stopReading();
1540
+ }
1541
+ run.result = result;
1542
+ if (!run.foreground)
1543
+ this.completedInBackground(run, result);
1544
+ return result;
1545
+ }
1546
+ /**
1547
+ * K3 completion notice: a line for the user, and a note for the agent that rides on the next turn;
1548
+ * when the session is idle the loop wakes and a short follow-up turn reports the result.
1549
+ */
1550
+ completedInBackground(run, result) {
1551
+ const g = this.glyphs;
1552
+ const ok = run.status === "succeeded";
1553
+ this.note(ok ? "info" : "warning", `${ok ? g.ok : g.warn} Background workers ${run.id} ${ok ? "finished" : run.status}${this.exiting ? "" : ` ${g.sep} Synorch reports the result`}`);
1554
+ if (this.exiting)
1555
+ return;
1556
+ const block = result.text !== "" ? result.text : "(no result block)";
1557
+ run.notice = `background worker run ${run.id} ended: ${run.status}.${result.error === undefined ? "" : ` ${result.error.message}`} Result block: ${block}`;
1558
+ this.pendingNotes.push(run.notice);
1559
+ this.unreportedRuns += 1;
1560
+ this.wakeIdle.abort();
1561
+ }
1562
+ /** The agent read a finished run's result itself (run_status): its completion note is dropped. */
1563
+ consumeNotice(run) {
1564
+ const notice = run.notice;
1565
+ if (notice === undefined)
1566
+ return;
1567
+ run.notice = undefined;
1568
+ const index = this.pendingNotes.indexOf(notice);
1569
+ if (index !== -1)
1570
+ this.pendingNotes.splice(index, 1);
1571
+ this.unreportedRuns = Math.max(0, this.unreportedRuns - 1);
1572
+ }
1573
+ findRun(ref) {
1574
+ if (ref === undefined || ref.trim() === "")
1575
+ return this.orchestration ?? this.workerRuns.at(-1);
1576
+ const wanted = ref.trim();
1577
+ return this.workerRuns.find((run) => run.id === wanted || run.tracker.run === wanted);
1578
+ }
1579
+ unknownRun(ref) {
1580
+ if (this.workerRuns.length === 0)
1581
+ return "no worker run in this conversation yet";
1582
+ return `no worker run ${ref ?? ""}; known: ${this.workerRuns.map((run) => run.id).join(", ")}`;
1583
+ }
1584
+ runStatusText(run) {
1585
+ const lines = [
1586
+ `Run ${run.id}${run.tracker.run === undefined ? "" : ` (${run.tracker.run})`}: ${run.status === "running" ? (run.foreground ? "running" : "running in the background") : run.status}`,
1587
+ `Goal: ${snippet(run.goal, 300)}`,
1588
+ ...(run.tracker.reason === undefined || run.tracker.reason.trim() === "" ? [] : [`Why: ${snippet(run.tracker.reason, 400)}`]),
1589
+ ...run.tracker.statusLines(),
1590
+ ];
1591
+ if (run.result !== undefined)
1592
+ lines.push("Result:", run.result.text !== "" ? run.result.text : (run.result.error?.message ?? ""));
1593
+ return lines.join("\n").slice(0, 16 * 1024);
1594
+ }
1595
+ /** K3 `run_status`: progress, or with `wait` the end of the run (bounded by timeout_seconds). */
1596
+ async runStatus(input, signal) {
1597
+ const run = this.findRun(input.run);
1598
+ if (run === undefined)
1599
+ return toolError("execution_failed", this.unknownRun(input.run));
1600
+ if (input.wait === true && run.status === "running") {
1601
+ const stop = new AbortController();
1602
+ const timeout = (input.timeout_seconds ?? 600) * 1000;
1603
+ await Promise.race([run.done, delay(timeout, undefined, { signal: AbortSignal.any([stop.signal, signal]) }).catch(() => undefined)]);
1604
+ stop.abort();
1605
+ }
1606
+ if (run.status !== "running")
1607
+ this.consumeNotice(run);
1608
+ return { status: "ok", text: this.runStatusText(run), truncated: false, redactions: 0 };
1609
+ }
1610
+ /** K3 `run_steer`: to the orchestrator (next safe point) or, with `task`, to that worker (next step). */
1611
+ async runSteer(input) {
1612
+ const run = this.findRun(input.run);
1613
+ if (run === undefined)
1614
+ return toolError("execution_failed", this.unknownRun(input.run));
1615
+ if (run.status !== "running")
1616
+ return toolError("execution_failed", `${run.id} already ended (${run.status}); nothing was sent`);
1617
+ if (run.kind === "review")
1618
+ return toolError("execution_failed", `${run.id} is an independent review; it cannot be steered (run_cancel stops it)`);
1619
+ const arrow = this.glyphs.name === "rich" ? "↳" : "->";
1620
+ if (input.task !== undefined) {
1621
+ const result = await run.coordinator.workers.message(input.task, input.message);
1622
+ if (!result.ok)
1623
+ return toolError("execution_failed", result.message);
1624
+ this.note("info", `${arrow} Synorch told ${input.task}: ${input.message.trim()}`);
1625
+ return { status: "ok", text: result.message, truncated: false, redactions: 0 };
1626
+ }
1627
+ run.coordinator.steer(input.message);
1628
+ this.note("info", `${arrow} Synorch steered the workers: ${input.message.trim()} (applied at the next safe point)`);
1629
+ return { status: "ok", text: `Queued for the orchestrator of ${run.id}; it applies the message at the next safe point (it may re-plan).`, truncated: false, redactions: 0 };
1630
+ }
1631
+ /** K3 `run_cancel`: one task, or the whole run (its completion note follows). */
1632
+ async runCancel(input) {
1633
+ const run = this.findRun(input.run);
1634
+ if (run === undefined)
1635
+ return toolError("execution_failed", this.unknownRun(input.run));
1636
+ if (run.status !== "running")
1637
+ return toolError("execution_failed", `${run.id} already ended (${run.status})`);
1638
+ if (input.task !== undefined) {
1639
+ const result = await run.coordinator.workers.cancel(input.task);
1640
+ return result.ok ? { status: "ok", text: result.message, truncated: false, redactions: 0 } : toolError("execution_failed", result.message);
1641
+ }
1642
+ this.note("warning", `Stopping the workers (${run.id})…`);
1643
+ run.controller.abort();
1644
+ return { status: "ok", text: `Cancelling ${run.id}; work already integrated stays. A note follows when it has stopped.`, truncated: false, redactions: 0 };
1645
+ }
1646
+ /** Worker prompts go through the typing-pause gate (K3). */
1647
+ gatedApprovals() {
1648
+ const inner = this.renderer.approvals;
1649
+ return { availability: inner.availability, request: (request, signal) => this.gated(signal, () => inner.request(request, signal)) };
1650
+ }
1651
+ /**
1652
+ * K3: while workers run in the background, prompts (worker approvals, questions) open one at a time
1653
+ * and only once the user pauses typing, so a modal never swallows keystrokes meant for the editor.
1654
+ */
1655
+ async gated(signal, work) {
1656
+ if (this.backgroundRun() === undefined || this.renderer.kind !== "tui")
1657
+ return work();
1658
+ const previous = this.promptQueue;
1659
+ let release = () => undefined;
1660
+ this.promptQueue = new Promise((resolve) => {
1661
+ release = resolve;
1662
+ });
1663
+ try {
1664
+ await previous;
1665
+ await this.typingPause(signal);
1666
+ return await work();
1667
+ }
1668
+ finally {
1669
+ release();
1670
+ }
1671
+ }
1672
+ async typingPause(signal) {
1673
+ const controls = this.renderer.controls;
1674
+ const typing = () => {
1675
+ const activity = controls?.inputActivity?.();
1676
+ return activity !== undefined && Date.now() - activity.lastKeyAtMs < TYPING_PAUSE_MS;
1677
+ };
1678
+ if (!typing())
1679
+ return;
1680
+ this.note("warning", `${this.glyphs.warn} Synorch needs your answer ${this.glyphs.sep} the prompt opens when you pause typing (your draft is kept)`);
1681
+ while (typing()) {
1682
+ if (signal?.aborted === true)
1683
+ throw new DOMException("aborted", "AbortError");
1684
+ await delay(150);
1685
+ }
1686
+ }
1687
+ hardExit() {
1688
+ this.exiting = true;
1689
+ this.active?.abort();
1690
+ this.orchestration?.controller.abort();
1691
+ this.outer.abort();
1692
+ }
1693
+ /**
1694
+ * K3 exit with workers in the background: /exit and /quit stop them cleanly; Ctrl+C / Ctrl+D ask
1695
+ * (stop and exit, or keep working). Without a terminal the run is awaited, never orphaned.
1696
+ * Resolves true when the session should end.
1697
+ */
1698
+ async confirmExit(explicit) {
1699
+ const run = this.backgroundRun();
1700
+ if (run === undefined)
1701
+ return true;
1702
+ const g = this.glyphs;
1703
+ if (!this.io.stdinIsTTY) {
1704
+ this.note("info", `Waiting for the background workers (${run.id}) to finish before exiting…`);
1705
+ await run.done;
1706
+ return false;
1707
+ }
1708
+ if (!explicit) {
1709
+ const stop = "Stop the workers and exit";
1710
+ let answer;
1711
+ try {
1712
+ answer = await this.askUser(`Workers are still running in the background (${run.id}). Stop them and exit?`, ["Keep working", stop], this.outer.signal);
1713
+ }
1714
+ catch {
1715
+ answer = undefined;
1716
+ }
1717
+ if (answer !== stop) {
1718
+ this.note("info", `Still here ${g.sep} the workers keep running ${g.sep} /runs cancel stops them`);
1719
+ return false;
1720
+ }
1721
+ }
1722
+ this.note("warning", `Stopping the workers (${run.id})…`);
1723
+ run.controller.abort();
1724
+ await Promise.race([run.done, delay(SETTLE_MS, undefined, { ref: false })]);
1725
+ return true;
1726
+ }
1727
+ /** At session end: an active run is cancelled and given a moment to clean up its worktrees. */
1728
+ async settleRuns() {
1729
+ const run = this.orchestration;
1730
+ if (run === undefined)
1731
+ return;
1732
+ run.controller.abort();
1733
+ await Promise.race([run.done, delay(SETTLE_MS, undefined, { ref: false })]);
1734
+ }
1735
+ /** K3 `/runs [id | cancel [id]]`. */
1736
+ async runs(argument) {
1737
+ const g = this.glyphs;
1738
+ const [verb = "", target] = argument.trim().split(/\s+/).filter((part) => part !== "");
1739
+ if (verb === "cancel" || verb === "stop") {
1740
+ const run = this.findRun(target);
1741
+ if (run === undefined || run.status !== "running") {
1742
+ this.print([run === undefined ? this.unknownRun(target) : `${run.id} already ended (${run.status})`]);
1743
+ return;
1744
+ }
1745
+ this.note("warning", `Stopping the workers (${run.id})…`);
1746
+ run.controller.abort();
1747
+ return;
1748
+ }
1749
+ if (this.workerRuns.length === 0) {
1750
+ this.print([`No worker runs in this conversation yet ${g.sep} /workers <goal> starts one`]);
1751
+ return;
1752
+ }
1753
+ if (verb !== "") {
1754
+ const run = this.findRun(verb);
1755
+ this.print(run === undefined ? [this.unknownRun(verb)] : this.runStatusText(run).split("\n"));
1756
+ return;
1757
+ }
1758
+ const runEntries = () => this.workerRuns.map((run) => ({
1759
+ id: run.id,
1760
+ goal: run.goal,
1761
+ status: run.status === "running" ? (run.foreground ? "running" : "background") : run.status,
1762
+ running: run.status === "running",
1763
+ view: () => run.tracker.view(),
1764
+ report: () => this.runStatusText(run),
1765
+ cancel: () => run.controller.abort(),
1766
+ }));
1767
+ if (await this.showPanel(() => runsPanel(runEntries, g.sep)))
1768
+ return;
1769
+ this.print([
1770
+ `${g.bullet} Worker runs ${g.sep} ${this.workerRuns.filter((run) => run.status === "running").length} running`,
1771
+ ...this.workerRuns.map((run) => {
1772
+ const view = run.tracker.view();
1773
+ const done = view.tasks.filter((task) => task.state === "completed").length;
1774
+ const seconds = Math.round(((view.endedAtMs ?? Date.now()) - (view.startedAtMs ?? Date.now())) / 1000);
1775
+ const status = run.status === "running" ? (run.foreground ? "running" : "background") : run.status;
1776
+ return ` ${run.id.padEnd(8)}${status.padEnd(12)}${`${done}/${view.tasks.length} tasks`.padEnd(12)}${`${seconds}s`.padEnd(8)}${snippet(run.goal, 60)}`;
1777
+ }),
1778
+ ` /runs <id> shows one ${g.sep} /runs cancel [id] stops one ${g.sep} Ctrl+G board/graph ${g.sep} /worker <key> enters a worker`,
1779
+ ]);
1780
+ }
1781
+ // ---- K1.7: entering workers ---------------------------------------------------------------
1782
+ /** Connects the worker seam (live streams + control, keyed by board key) to the renderer when an orchestration starts. */
1783
+ connectWorkers(coordinator) {
1784
+ const directory = coordinator.workers;
1785
+ this.workerDirectory = directory;
1786
+ this.hub.assignments = (taskKey) => directory.assignment(taskKey);
1787
+ const rejecting = (run) => async (taskKey, text = "") => {
1788
+ const result = await run(taskKey, text);
1789
+ if (!result.ok)
1790
+ throw new Error(result.message);
1791
+ };
1792
+ const control = {
1793
+ message: rejecting((taskKey, text) => directory.message(taskKey, text)),
1794
+ pause: rejecting((taskKey) => directory.pause(taskKey)),
1795
+ resume: rejecting((taskKey) => directory.resume(taskKey)),
1796
+ cancel: rejecting((taskKey) => directory.cancel(taskKey)),
1797
+ };
1798
+ this.workerSeam = { stream: this.hub, control };
1799
+ this.renderer.views?.connectWorkers?.(this.workerSeam);
1800
+ }
1801
+ /**
1802
+ * Main-chat lines: a delegation card when the orchestrator dispatches work (every renderer), and in
1803
+ * plain mode the user's messages to workers and pause/resume/cancel (the TUI draws its own).
1804
+ */
1805
+ workerLine(recorded) {
1806
+ const g = this.glyphs;
1807
+ const arrow = g.name === "rich" ? "→" : "->";
1808
+ if (recorded.type === "task/delegated") {
1809
+ const data = recorded.data;
1810
+ const views = this.renderer.views;
1811
+ if (views !== undefined) {
1812
+ const assignment = this.workerDirectory?.assignment(data.key);
1813
+ views.showView({ kind: "delegation", taskKey: data.key, role: data.role, model: data.model_id, objective: data.objective, ...(assignment === undefined ? {} : { assignment }) });
1814
+ }
1815
+ else
1816
+ this.note("info", `${arrow} ${data.key} (${data.role}, ${data.model_id}): ${data.objective.trim()}`);
1817
+ return;
1818
+ }
1819
+ if (this.renderer.kind === "tui")
1820
+ return;
1821
+ const key = (taskId) => this.workerDirectory?.list().find((worker) => worker.taskId === taskId)?.key ?? taskId;
1822
+ if (recorded.type === "task/user_message") {
1823
+ this.note("info", `${g.name === "rich" ? "↳" : "->"} you ${arrow} ${key(recorded.data.task_id)}: ${recorded.data.text.trim()}`);
1824
+ }
1825
+ else if (recorded.type === "attempt/user_control") {
1826
+ const verb = recorded.data.action === "pause" ? "paused" : recorded.data.action === "resume" ? "resumed" : "cancelled";
1827
+ this.note(recorded.data.action === "cancel" ? "warning" : "info", `${g.bullet} ${key(recorded.data.task_id)} ${verb} by you`);
1828
+ }
1829
+ }
1830
+ /**
1831
+ * Plain interactive mode reads no input while a turn runs; while workers run it does, so a typed
1832
+ * line steers the workers, answers the orchestrator, or runs a `whileBusy` command (`/worker`).
1833
+ * The TUI reads concurrently anyway (`alongside`). Resolves the stop function.
1834
+ */
1835
+ readWhileWorkersRun() {
1836
+ const input = this.renderer.input;
1837
+ if (this.renderer.kind === "tui" || !this.io.stdinIsTTY || input === undefined || this.readingDuringWorkers)
1838
+ return async () => undefined;
1839
+ const stop = new AbortController();
1840
+ this.readingDuringWorkers = true;
1841
+ const reader = (async () => {
1842
+ for (;;) {
1843
+ let next;
1844
+ try {
1845
+ next = await input.next(AbortSignal.any([stop.signal, this.outer.signal]));
1846
+ }
1847
+ catch {
1848
+ return;
1849
+ }
1850
+ if (next.kind === "exit") {
1851
+ this.exiting = true;
1852
+ this.orchestration?.controller.abort();
1853
+ this.active?.abort();
1854
+ this.outer.abort();
1855
+ return;
1856
+ }
1857
+ if (!("text" in next))
1858
+ continue;
1859
+ const text = next.text.trim();
1860
+ if (text === "")
1861
+ continue;
1862
+ if (next.kind === "command" || text.startsWith("/")) {
1863
+ const command = findConversationCommand(text.split(/\s+/)[0] ?? "");
1864
+ if (command?.whileBusy === true)
1865
+ await this.command(text);
1866
+ else {
1867
+ this.enqueue(text);
1868
+ this.note("info", `queued > ${text} (runs when Synorch is done)`);
1869
+ }
1870
+ continue;
1871
+ }
1872
+ if (this.desk.answer(text))
1873
+ continue;
1874
+ this.steer(text, []);
1875
+ }
1876
+ })();
1877
+ return async () => {
1878
+ stop.abort();
1879
+ await reader;
1880
+ this.readingDuringWorkers = false;
1881
+ };
1882
+ }
1883
+ /** `/worker [key] [message | --pause | --resume | --cancel]`. */
1884
+ async worker(argument) {
1885
+ const directory = this.workerDirectory;
1886
+ const [key = "", ...rest] = argument.split(/\s+/).filter((part) => part !== "");
1887
+ if (directory === undefined || directory.list().length === 0) {
1888
+ this.print([`No workers yet in this conversation ${this.glyphs.sep} /workers <goal> runs a goal with workers`]);
1889
+ return;
1890
+ }
1891
+ if (key === "") {
1892
+ this.print(this.workerList(directory));
1893
+ return;
1894
+ }
1895
+ const worker = directory.list().find((candidate) => candidate.key === key || candidate.taskId === key);
1896
+ if (worker === undefined) {
1897
+ this.print([`No worker ${key} ${this.glyphs.sep} known: ${directory.list().map((candidate) => candidate.key).join(", ")}`]);
1898
+ return;
1899
+ }
1900
+ const message = rest.join(" ").trim();
1901
+ const flag = /^--(pause|resume|cancel)$/i.exec(message)?.[1]?.toLowerCase();
1902
+ if (flag !== undefined || message !== "") {
1903
+ const result = flag === "pause" ? await directory.pause(worker.key) : flag === "resume" ? await directory.resume(worker.key) : flag === "cancel" ? await directory.cancel(worker.key) : await directory.message(worker.key, message);
1904
+ this.note(result.ok ? "info" : "warning", result.message);
1905
+ return;
1906
+ }
1907
+ const views = this.renderer.views;
1908
+ if (this.renderer.kind === "tui" && views?.openWorkerView?.(worker.key) === true)
1909
+ return;
1910
+ if (views === undefined) {
1911
+ this.print(this.workerDetail(directory, worker.key));
1912
+ return;
1913
+ }
1914
+ const task = this.lastTracker?.view().tasks.find((candidate) => candidate.key === worker.key) ?? { key: worker.key, role: worker.role, model: worker.model, state: worker.state };
1915
+ // The hub's assignment carries delivery state (a steer the worker already took is no longer "queued").
1916
+ const snapshot = this.hub.snapshot(worker.key);
1917
+ const latest = snapshot.filter((event) => event.kind === "assignment").at(-1);
1918
+ const assignment = latest?.kind === "assignment" ? latest.assignment : directory.assignment(worker.key);
1919
+ const events = snapshot.filter((event) => event.kind !== "assignment");
1920
+ views.showView({ kind: "worker", task: worker.paused ? { ...task, paused: true } : task, ...(assignment === undefined ? {} : { assignment }), events });
1921
+ }
1922
+ workerList(directory) {
1923
+ const g = this.glyphs;
1924
+ const rows = directory.list();
1925
+ const width = Math.max(4, ...rows.map((row) => row.key.length));
1926
+ return [
1927
+ `${g.bullet} Workers ${g.sep} ${rows.filter((row) => row.live).length} running`,
1928
+ ...rows.map((row) => {
1929
+ const status = row.live ? (row.paused ? "paused" : "running") : row.state.replaceAll("_", " ");
1930
+ return ` ${row.key.padEnd(width + 2)}${row.role.padEnd(12)}${status.padEnd(18)}${row.model ?? ""}${row.attempt > 1 ? ` ${g.sep} attempt ${row.attempt}` : ""}`;
1931
+ }),
1932
+ ` /worker <key> shows one ${g.sep} /worker <key> <message> messages it ${g.sep} --pause / --resume / --cancel`,
1933
+ ];
1934
+ }
1935
+ /** Text fallback of `/worker <key>` when the renderer hosts no views. */
1936
+ workerDetail(directory, key) {
1937
+ const g = this.glyphs;
1938
+ const assignment = directory.assignment(key);
1939
+ const lines = [];
1940
+ if (assignment !== undefined) {
1941
+ lines.push(`${g.bullet} ${assignment.taskKey} ${g.sep} ${assignment.objective}`);
1942
+ lines.push(` Owns ${assignment.owned_paths.join(", ") || "nothing (read-only)"}`);
1943
+ for (const [index, criterion] of assignment.acceptance_criteria.entries())
1944
+ lines.push(` ${index === 0 ? "Criteria " : " "}${criterion}`);
1945
+ lines.push(` Checks ${assignment.verification_commands.join("; ") || "none"}`);
1946
+ for (const note of assignment.steering)
1947
+ lines.push(` ${note.from === "user" ? "You said " : "Orchestrator"} ${snippet(note.text, 160)}`);
1948
+ }
1949
+ const activity = this.hub
1950
+ .recent(key, 200)
1951
+ .filter((event) => event.type !== "session/opened" && event.type !== "tool/execution_started")
1952
+ .map((event) => describeEvent(event))
1953
+ .filter((line) => line !== undefined)
1954
+ .slice(-12);
1955
+ lines.push(activity.length === 0 ? " (no activity recorded yet)" : " Recent activity:");
1956
+ for (const line of activity)
1957
+ lines.push(` ${snippet(line.text, 160)}`);
1958
+ return lines;
1959
+ }
1960
+ // ---- slash commands ---------------------------------------------------------------------------
1961
+ /** Resolves true when the user asked to leave. */
1962
+ async command(text) {
1963
+ const [name = ""] = text.split(/\s+/);
1964
+ const argument = text.slice(name.length).trim();
1965
+ const command = findConversationCommand(name);
1966
+ if (command === undefined) {
1967
+ // K7: a skill or markdown command runs as the next message (its body plus the arguments).
1968
+ const entry = name.startsWith("/") ? this.runtime.extensions.find(name.slice(1)) : undefined;
1969
+ const expanded = entry === undefined ? undefined : await this.runtime.extensions.invocationText(entry, argument).catch(() => undefined);
1970
+ if (expanded !== undefined) {
1971
+ this.queued.unshift({ text: expanded, attachments: [], verbatim: true });
1972
+ return false;
1973
+ }
1974
+ this.print([unknownConversationCommand(name, this.glyphs.sep)]);
1975
+ return false;
1976
+ }
1977
+ try {
1978
+ return (await command.run(this, argument)) === true;
1979
+ }
1980
+ catch (error) {
1981
+ if (!this.outer.signal.aborted)
1982
+ this.showFailure(failureInfo(error));
1983
+ return false;
1984
+ }
1985
+ }
1986
+ /** `/cancel`: the running turn first; a background run only when no turn runs (K3). */
1987
+ cancel() {
1988
+ const run = this.orchestration;
1989
+ if (run !== undefined && (run.foreground || this.active === undefined)) {
1990
+ if (!run.foreground)
1991
+ this.note("warning", `Stopping the workers (${run.id})…`);
1992
+ run.controller.abort();
1993
+ }
1994
+ else if (this.active === undefined)
1995
+ this.print(["Nothing is running."]);
1996
+ else
1997
+ this.active.abort();
1998
+ }
1999
+ /**
2000
+ * The permission mode from Shift+Tab, `/permissions <mode>`, `/plan`, `/go` or a go-word
2001
+ * (ADR-08 revision 2026-09-24); idempotent. The policy is recomputed at once, the footer follows,
2002
+ * and the agent is told at its next message.
2003
+ */
2004
+ setMode(mode) {
2005
+ const runtime = this.runtime;
2006
+ const previous = runtime.permissionMode();
2007
+ if (previous === mode)
2008
+ return;
2009
+ if (mode === "plan")
2010
+ this.modeBeforePlan = previous;
2011
+ runtime.setPermissionMode(mode);
2012
+ this.policyCache = undefined;
2013
+ const g = this.glyphs;
2014
+ if (mode !== undefined)
2015
+ this.renderer.controls?.setPermissionMode(mode);
2016
+ if (mode === "plan") {
2017
+ this.note("info", `${g.bullet} Plan mode on ${g.sep} Synorch reads and plans but does not edit or run commands ${g.sep} Shift+Tab or /plan leaves`);
2018
+ this.pendingNotes.push("plan mode is ON: you may read, search and discuss, but not edit files or run commands. Produce a concrete plan (steps, files, risks, how it will be verified); say whether it is small enough to do directly or big enough for workers.");
2019
+ return;
2020
+ }
2021
+ if (previous === "plan") {
2022
+ this.pendingNotes.push("plan mode is OFF: you may edit files and run commands again.");
2023
+ if (mode !== "full") {
2024
+ this.note("info", `${g.bullet} Plan mode off ${g.sep} Synorch may edit and run commands again${mode === undefined ? "" : ` ${g.sep} ${mode} mode`}`);
2025
+ return;
2026
+ }
2027
+ }
2028
+ if (mode === "full") {
2029
+ this.note("error", this.fullAccessNotice());
2030
+ this.pendingNotes.push("the user switched to full access: edits and commands in this folder run without asking (hard rails still apply).");
2031
+ }
2032
+ else if (mode === "ask") {
2033
+ this.note("info", `${g.bullet} Ask mode ${g.sep} Synorch asks before every edit and command`);
2034
+ this.pendingNotes.push("the user switched to ask mode: each edit and command is confirmed by the user first.");
2035
+ }
2036
+ else if (mode === "auto") {
2037
+ this.note("info", `${g.bullet} Auto mode ${g.sep} edits and commands in this workspace run (workers too); pushes, publishing and destructive commands ask you first`);
2038
+ if (previous === "ask" || previous === "full")
2039
+ this.pendingNotes.push("the user switched to auto mode: edits and any command inside the workspace run without asking (workers too); only outward actions (push, publish, deploy) and destructive commands are asked for.");
2040
+ }
2041
+ else
2042
+ this.note("info", `${g.bullet} Default-deny ${g.sep} commands outside the allowlist are refused`);
2043
+ }
2044
+ /** Plan mode on/off from `/plan`, `/go` or a go-word; leaving returns to the mode before it. */
2045
+ setPlanMode(on) {
2046
+ if (this.planOn === on)
2047
+ return;
2048
+ if (on)
2049
+ this.setMode("plan");
2050
+ else
2051
+ this.setMode(this.modeBeforePlan);
2052
+ }
2053
+ async planMode(goal) {
2054
+ if (goal === "") {
2055
+ this.setPlanMode(!this.planOn);
2056
+ return;
2057
+ }
2058
+ this.setPlanMode(true);
2059
+ this.enqueue(goal);
2060
+ }
2061
+ async go(mode) {
2062
+ const workers = /^(workers?|w|parallel)$/i.test(mode.trim());
2063
+ this.setPlanMode(false);
2064
+ this.enqueue(workers
2065
+ ? "The plan is approved. Carry it out with workers now: call the orchestrate tool with the goal, a one-sentence reason and the plan as the brief."
2066
+ : "The plan is approved. Carry it out here directly now.");
2067
+ }
2068
+ async workers(goal) {
2069
+ if (goal === "") {
2070
+ if (this.workerDirectory !== undefined && this.workerDirectory.list().length > 0) {
2071
+ this.print(this.workerList(this.workerDirectory));
2072
+ return;
2073
+ }
2074
+ this.print(["Usage: /workers <goal> · plans the goal, runs parallel workers in their own worktrees and has an independent reviewer check the result"]);
2075
+ return;
2076
+ }
2077
+ this.setPlanMode(false);
2078
+ this.enqueue(`Use workers for this (call the orchestrate tool): ${goal}`);
2079
+ if (this.turnRunning || this.orchestration?.foreground === true)
2080
+ this.note("info", `queued > /workers ${snippet(goal, 80)} (runs when Synorch is done)`);
2081
+ }
2082
+ async undo() {
2083
+ const g = this.glyphs;
2084
+ const events = await this.readEvents();
2085
+ const restoredSeqs = new Set(events.flatMap((event) => (event.type === "checkpoint/restored" ? [event.data.checkpoint_seq] : [])));
2086
+ const target = [...events].reverse().find((event) => event.type === "checkpoint/recorded" && !restoredSeqs.has(event.seq));
2087
+ if (target === undefined) {
2088
+ this.note("info", "Nothing to undo: Synorch has not edited a file in this conversation (or every edit was already undone).");
2089
+ return;
2090
+ }
2091
+ const restored = [];
2092
+ const skipped = [];
2093
+ for (const file of target.data.files) {
2094
+ const place = this.inside(file.path);
2095
+ if (place === undefined) {
2096
+ skipped.push({ path: file.path, reason: "outside the workspace" });
2097
+ continue;
2098
+ }
2099
+ const current = await readOptional(place.absolute);
2100
+ const digest = current === undefined ? null : workspaceDigest(new Uint8Array(current));
2101
+ if (digest !== file.after) {
2102
+ skipped.push({ path: file.path, reason: "it changed after the edit, so it was left as is" });
2103
+ continue;
2104
+ }
2105
+ if (file.before === null)
2106
+ await unlink(place.absolute);
2107
+ else {
2108
+ await mkdir(path.dirname(place.absolute), { recursive: true });
2109
+ await writeFile(place.absolute, await this.runtime.blobs.get(file.before.digest));
2110
+ }
2111
+ restored.push(file.path);
2112
+ }
2113
+ await this.append("checkpoint/restored", { checkpoint_seq: target.seq, restored, skipped }, "user");
2114
+ if (restored.length > 0) {
2115
+ this.note("info", `${g.resume} Reverted ${restored.join(", ")} ${g.sep} command side effects are not undone`);
2116
+ this.pendingNotes.push(`the user ran /undo; ${restored.join(", ")} ${restored.length === 1 ? "is" : "are"} back to the content before your last edit (re-read before editing again).`);
2117
+ }
2118
+ for (const entry of skipped)
2119
+ this.note("warning", `${g.warn} Kept ${entry.path}: ${entry.reason}`);
2120
+ }
2121
+ async allow(argument) {
2122
+ const grants = this.grants;
2123
+ if (grants === undefined)
2124
+ return;
2125
+ if (argument === "") {
2126
+ const list = await grants.list();
2127
+ this.note("info", list.length === 0 ? "No commands allowed yet. /allow <command prefix> adds one, for example /allow node check.mjs" : `Allowed here: ${list.join(" · ")}`);
2128
+ return;
2129
+ }
2130
+ const removing = /^(-r|--remove)\s+/.exec(argument);
2131
+ const normalized = normalizeGrant(removing === null ? argument : argument.slice(removing[0].length));
2132
+ if ("error" in normalized) {
2133
+ this.note("warning", `${this.glyphs.warn} ${normalized.error}`);
2134
+ return;
2135
+ }
2136
+ if (removing !== null) {
2137
+ this.grantList = await grants.remove(normalized.prefix);
2138
+ this.policyCache = undefined;
2139
+ this.note("info", `${this.glyphs.ok} Removed: ${normalized.prefix}`);
2140
+ return;
2141
+ }
2142
+ await this.addGrant(normalized.prefix, "command");
2143
+ }
2144
+ /** Persists a command prefix for this folder (user scope), audits it and applies it to the policy at once. */
2145
+ async addGrant(raw, source) {
2146
+ const grants = this.grants;
2147
+ if (grants === undefined)
2148
+ return;
2149
+ const normalized = normalizeGrant(raw);
2150
+ if ("error" in normalized) {
2151
+ this.note("warning", `${this.glyphs.warn} Not saved as a rule: ${normalized.error}`);
2152
+ return;
2153
+ }
2154
+ this.grantList = await grants.add(normalized.prefix);
2155
+ this.policyCache = undefined;
2156
+ // A prompt answer arrives inside a turn whose log is already open; /allow may be the first thing typed.
2157
+ if (source === "command")
2158
+ await this.ensureLog(`/allow ${normalized.prefix}`).catch(() => undefined);
2159
+ await this.append("command/allowed", { workspace_root: this.runtime.trust.state().root, prefix: normalized.prefix }, "user").catch(() => undefined);
2160
+ const trustNote = this.runtime.sandbox.enforcement !== "full" && !this.runtime.trust.state().trusted ? " (it runs repository code, so this folder must be trusted too; Synorch asks when it first runs)" : "";
2161
+ this.note("info", `${this.glyphs.ok} Always allowed: commands starting with "${normalized.prefix}" in this folder${trustNote} ${this.glyphs.sep} /permissions lists the rules`);
2162
+ this.pendingNotes.push(`the user allowed commands starting with "${normalized.prefix}"; you may run them now.`);
2163
+ }
2164
+ /**
2165
+ * `/permissions`: the mode, the persisted allow rules and the trust state; `/permissions <mode>`
2166
+ * switches the mode, `allow <prefix>` / `remove <prefix>` edit the rules (like /allow).
2167
+ */
2168
+ async permissions(argument) {
2169
+ const [verb = "", ...rest] = argument.split(/\s+/).filter((part) => part !== "");
2170
+ const tail = rest.join(" ");
2171
+ const wanted = PERMISSION_MODE_WORDS.includes(verb.toLowerCase()) ? verb.toLowerCase() : verb.toLowerCase() === "mode" ? rest[0]?.toLowerCase() : undefined;
2172
+ if (wanted !== undefined) {
2173
+ if (!PERMISSION_MODE_WORDS.includes(wanted)) {
2174
+ this.print([`Unknown mode "${wanted}". Modes: ${PERMISSION_MODE_WORDS.join(", ")}`]);
2175
+ return;
2176
+ }
2177
+ if (this.renderer.approvals.availability !== "interactive" && wanted !== "full" && wanted !== "plan") {
2178
+ this.note("warning", `${this.glyphs.warn} No terminal can answer prompts here: ${wanted} mode questions become refusals`);
2179
+ }
2180
+ this.setMode(wanted);
2181
+ return;
2182
+ }
2183
+ if (verb === "allow" || verb === "add") {
2184
+ await this.allow(tail);
2185
+ return;
2186
+ }
2187
+ if (verb === "remove" || verb === "rm" || verb === "-r") {
2188
+ await this.allow(tail === "" ? "" : `-r ${tail}`);
2189
+ return;
2190
+ }
2191
+ if (verb === "web") {
2192
+ await this.webDomains(rest);
2193
+ return;
2194
+ }
2195
+ if (verb !== "") {
2196
+ this.print(["Usage: /permissions [ask|auto|full|plan] · /permissions allow <prefix> · /permissions remove <prefix> · /permissions web [allow|remove <domain>]"]);
2197
+ return;
2198
+ }
2199
+ this.print(await this.permissionLines());
2200
+ }
2201
+ /** `/permissions web [allow|remove <domain>]`: the web domains web_fetch reads without asking (K4.1, global user scope). */
2202
+ async webDomains(args) {
2203
+ const web = this.runtime.web;
2204
+ const [action = "", domain = ""] = args;
2205
+ if (action === "allow" || action === "add") {
2206
+ const added = await web.allowDomain(domain);
2207
+ if (added)
2208
+ await this.append("network/host_allowed", { host: domain.trim().toLowerCase(), scope: "global" }, "user").catch(() => undefined);
2209
+ this.print([added ? `${this.glyphs.ok} Always allowed: ${domain} (every project)` : `${domain} is already allowed or is not a domain name`]);
2210
+ return;
2211
+ }
2212
+ if (action === "remove" || action === "rm") {
2213
+ this.print([(await web.removeDomain(domain)) ? `${this.glyphs.ok} Removed: ${domain}` : `${domain} was not among your allowed domains`]);
2214
+ return;
2215
+ }
2216
+ const granted = web.grantedDomains();
2217
+ this.print([
2218
+ `Web domains ${granted.length === 0 ? "none of your own yet (answer \"Always allow\" at a fetch prompt, or /permissions web allow <domain>)" : granted.join(` ${this.glyphs.sep} `)}`,
2219
+ `Built in ${DEFAULT_WEB_DOMAINS.length} documentation and registry sites (github.com, docs.python.org, …)`,
2220
+ ]);
2221
+ }
2222
+ async permissionLines() {
2223
+ const runtime = this.runtime;
2224
+ const g = this.glyphs;
2225
+ const mode = runtime.permissionMode();
2226
+ const recorded = runtime.trust.recorded();
2227
+ const state = runtime.trust.state();
2228
+ const rules = (await this.grants?.list()) ?? this.grantList;
2229
+ const trust = runtime.sandbox.enforcement === "full"
2230
+ ? "not needed (full sandbox)"
2231
+ : recorded.trusted
2232
+ ? recorded.source === "store" || recorded.source === undefined ? "trusted (saved; syn trust --revoke removes it)" : "trusted for this session"
2233
+ : state.trusted
2234
+ ? "trusted for this session by full access (not saved)"
2235
+ : "not trusted (Synorch asks at the first build or test command; /trust asks now)";
2236
+ return [
2237
+ `Mode ${mode === undefined ? `default-deny (no prompts; headless)` : `${mode} ${g.sep} ${MODE_MEANINGS[mode]}`}`,
2238
+ ` Shift+Tab cycles ask ${g.name === "rich" ? "→" : "->"} auto ${g.name === "rich" ? "→" : "->"} full ${g.name === "rich" ? "→" : "->"} plan; /permissions <mode> switches`,
2239
+ `Trust ${trust}`,
2240
+ `Sandbox ${runtime.sandbox.backend} (${runtime.sandbox.enforcement})${runtime.sandbox.enforcement === "full" ? "" : ` ${g.sep} allowed commands can write outside this folder`}`,
2241
+ `Always allowed ${rules.length === 0 ? "none yet (answer \"Always allow\" in a prompt, or /permissions allow <prefix>)" : rules.join(` ${g.sep} `)}`,
2242
+ ...(rules.length === 0 ? [] : [" /permissions remove <prefix> deletes a rule"]),
2243
+ `Web search free except in ask mode; fetch asks at a new domain (auto/plan) ${g.sep} your domains: ${runtime.web.grantedDomains().length === 0 ? "none" : runtime.web.grantedDomains().join(", ")} ${g.sep} /permissions web`,
2244
+ `Always asks destructive commands (force push, publish, recursive delete, reset --hard…), in every mode`,
2245
+ `Never ${HARD_RAILS.filter((rail) => rail !== "destructive-command").join(", ")} (hard rails, every mode); git history changes stay with you`,
2246
+ ];
2247
+ }
2248
+ /** `/status`: mode, sandbox, MCP servers and every notice of this session's start. */
2249
+ async status() {
2250
+ const lines = this.statusLines();
2251
+ if (await this.showPanel(() => statusPanel(lines, this.mcpPanelHost(), this.glyphs.sep)))
2252
+ return;
2253
+ this.print(lines);
2254
+ }
2255
+ statusLines() {
2256
+ const runtime = this.runtime;
2257
+ const g = this.glyphs;
2258
+ const mode = runtime.permissionMode();
2259
+ const mcp = runtime.mcp.status();
2260
+ const count = (state) => mcp.filter((entry) => entry.state === state).length;
2261
+ const mcpParts = [
2262
+ count("connected") > 0 ? `${count("connected")} connected` : undefined,
2263
+ count("idle") > 0 ? `${count("idle")} ready` : undefined,
2264
+ count("starting") > 0 ? `${count("starting")} starting` : undefined,
2265
+ count("needs-auth") > 0 ? `${count("needs-auth")} need sign-in` : undefined,
2266
+ count("needs-approval") > 0 ? `${count("needs-approval")} need approval` : undefined,
2267
+ count("failed") > 0 ? `${count("failed")} failed` : undefined,
2268
+ count("disabled") > 0 ? `${count("disabled")} off` : undefined,
2269
+ ].filter((part) => part !== undefined);
2270
+ const lines = [
2271
+ `Model ${this.currentModel ?? "none"}`,
2272
+ `Mode ${mode === undefined ? "default-deny (no prompts; headless)" : `${mode} ${g.sep} ${MODE_MEANINGS[mode]}`}`,
2273
+ `Sandbox ${runtime.sandbox.backend} (${runtime.sandbox.enforcement})${runtime.sandbox.enforcement === "full" ? "" : ` ${g.sep} allowed commands can write outside this folder`}`,
2274
+ `MCP ${mcp.length === 0 ? "no servers" : mcpParts.join(` ${g.sep} `)}${mcp.length === 0 ? "" : ` ${g.sep} /mcp`}`,
2275
+ ];
2276
+ for (const entry of mcp) {
2277
+ if (entry.state === "needs-auth")
2278
+ lines.push(` ${entry.name}: needs sign-in ${g.sep} /mcp login ${entry.name}`);
2279
+ else if (entry.state === "needs-approval")
2280
+ lines.push(` ${entry.name}: declared by this repo ${g.sep} /mcp approve ${entry.name}`);
2281
+ else if (entry.state === "failed")
2282
+ lines.push(` ${entry.name}: ${entry.error ?? "did not start"} ${g.sep} /mcp ${entry.name}`);
2283
+ }
2284
+ const notices = this.notices?.all ?? [];
2285
+ lines.push(`Notices ${notices.length === 0 ? "none" : notices.length}`);
2286
+ for (const notice of notices)
2287
+ lines.push(` ${notice.text}`);
2288
+ return lines;
2289
+ }
2290
+ async trust() {
2291
+ this.trustAsked = true;
2292
+ if (this.runtime.trust.state().trusted || this.runtime.sandbox.enforcement === "full")
2293
+ this.print(["This folder is already trusted."]);
2294
+ else if (await promptTrustForCommand(this.runtime, this.renderer, "build and test commands", this.outer.signal))
2295
+ this.policyCache = undefined;
2296
+ }
2297
+ /** Tiers whose route the conversation may use (a rule without a role, or one narrowed to `session`). */
2298
+ sessionRules() {
2299
+ return this.runtime.routeRules().filter((rule) => rule.role === undefined || rule.role === "session");
2300
+ }
2301
+ authLabel(rule) {
2302
+ const method = this.runtime.adapters.find((adapter) => adapter.adapterId === rule.route.adapter_id)?.authMethod;
2303
+ return method === "oauth-subscription" ? "oauth" : method === "cli-bridge" ? "cli" : method === "api-key" ? "api-key" : "unknown";
2304
+ }
2305
+ async model(argument) {
2306
+ // K1.5: every logged-in provider's models, per-tier session routes and --save (cli/model-picker.ts).
2307
+ await runModelCommand({
2308
+ runtime: this.runtime,
2309
+ controls: this.renderer.controls,
2310
+ signal: this.outer.signal,
2311
+ ok: this.glyphs.ok,
2312
+ sep: this.glyphs.sep,
2313
+ conversationTier: () => this.sessionTier,
2314
+ print: (lines) => this.print(lines),
2315
+ ask: (question, options) => this.askUser(question, options, this.outer.signal),
2316
+ switchConversation: (tier, save) => this.switchModel(tier, save),
2317
+ showFailure: (error) => this.showFailure(failureInfo(error)),
2318
+ pickEffort: async (tier) => {
2319
+ const host = this.effortHost();
2320
+ const target = effortTarget(host, tier === "session" || tier === this.sessionTier ? undefined : tier);
2321
+ if (!("error" in target))
2322
+ await pickEffort(host, target);
2323
+ },
2324
+ }, argument);
2325
+ }
2326
+ async effort(argument) {
2327
+ await runEffortCommand(this.effortHost(), argument);
2328
+ }
2329
+ /** K3 `/mcp [list | tools [name] | reconnect | enable | disable | approve | revoke <name>]`. */
2330
+ async mcp(argument) {
2331
+ if (argument.trim() === "" && (await this.showPanel(() => mcpPanel(this.mcpPanelHost()))))
2332
+ return;
2333
+ await runMcpSlash(this.mcpPanelHost(), argument);
2334
+ }
2335
+ mcpPanelHost() {
2336
+ return {
2337
+ manager: this.runtime.mcp,
2338
+ home: this.runtime.home,
2339
+ sep: this.glyphs.sep,
2340
+ env: this.io.env,
2341
+ print: (lines) => this.print(lines),
2342
+ openBrowser: (url) => openBrowser(url, { platform: process.platform, env: this.io.env }),
2343
+ signal: this.outer.signal,
2344
+ };
2345
+ }
2346
+ /** K7 `/skills [show | enable | disable <name>]`. */
2347
+ async skills(argument) {
2348
+ if (argument.trim() === "" && (await this.showPanel(() => skillsPanel(this.extensionHost()))))
2349
+ return;
2350
+ await runSkillsSlash(this.extensionHost(), argument);
2351
+ }
2352
+ /** K7 `/plugins [install <spec> | remove | enable | disable <name>]`. */
2353
+ async plugins(argument) {
2354
+ if (argument.trim() === "" && (await this.showPanel(() => pluginsPanel(this.extensionHost()))))
2355
+ return;
2356
+ await runPluginsSlash(this.extensionHost(), argument);
2357
+ }
2358
+ /** K7: runs one hook event (notes for hook messages); undefined when no active hook listens. */
2359
+ async runHook(event, payload, match, claudeNative, sessionId) {
2360
+ const hooks = this.runtime.extensions.hooks;
2361
+ if (!hooks.has(event, { claudeNative }))
2362
+ return undefined;
2363
+ const outcome = await hooks
2364
+ .run(event, payload, match, { sessionId, cwd: this.runtime.workspaceRoot, permissionMode: this.runtime.permissionMode(), claudeNative, signal: this.outer.signal })
2365
+ .catch(() => undefined);
2366
+ for (const message of outcome?.messages ?? [])
2367
+ this.note("warning", message);
2368
+ return outcome;
2369
+ }
2370
+ /** K7: a Synorch plugin whose approved hooks changed asks again (interactive sessions only); Claude plugins wait for /plugins hooks approve. */
2371
+ async reviewChangedHooks() {
2372
+ const changed = this.runtime.extensions.hooks.sources().filter((source) => source.state === "changed");
2373
+ if (changed.length === 0)
2374
+ return;
2375
+ if (this.renderer.approvals.availability !== "interactive") {
2376
+ this.note("warning", `Hooks changed since you approved them and do not run: ${changed.map((source) => source.key).join(", ")} ${this.glyphs.sep} /plugins hooks approve <plugin>`);
2377
+ return;
2378
+ }
2379
+ for (const source of changed)
2380
+ await askHookApproval(this.extensionHost(), source).catch(() => false);
2381
+ }
2382
+ extensionHost() {
2383
+ return {
2384
+ extensions: this.runtime.extensions,
2385
+ home: this.runtime.home,
2386
+ workspaceRoot: this.runtime.workspaceRoot,
2387
+ env: this.runtime.extensions.env,
2388
+ sep: this.glyphs.sep,
2389
+ print: (lines) => this.print(lines),
2390
+ choose: (question) => this.choose(question, this.outer.signal).catch(() => undefined),
2391
+ changed: async () => {
2392
+ this.refreshPalette();
2393
+ // Plugin MCP servers follow the plugin set: re-resolve the definitions (a new server starts lazily).
2394
+ const mcp = this.runtime.mcp;
2395
+ const before = new Set(mcp.definitions().map((definition) => definition.name));
2396
+ await mcp.load().catch(() => undefined);
2397
+ for (const definition of mcp.definitions())
2398
+ if (!before.has(definition.name) && definition.enabled)
2399
+ await mcp.reconnect(definition.name).catch(() => undefined);
2400
+ },
2401
+ };
2402
+ }
2403
+ /** The command palette: built-in commands, then every active skill and markdown command (K7). */
2404
+ refreshPalette() {
2405
+ const extensions = this.runtime.extensions.invocable(reservedCommandNames()).map((entry) => ({
2406
+ name: entry.name,
2407
+ description: `${entry.kind === "skill" ? "skill" : "command"} (${entry.source}) ${entry.meta.description.replace(/\s+/g, " ").slice(0, 90)}`,
2408
+ argsHint: entry.meta.argumentHint ?? "[args]",
2409
+ }));
2410
+ this.renderer.controls?.setCommands([...conversationPaletteEntries(), ...extensions]);
2411
+ }
2412
+ /** K3: session start of the MCP servers; approvals the repository needs and start failures become notes. */
2413
+ async startMcp() {
2414
+ const mcp = this.runtime.mcp;
2415
+ const sep = this.glyphs.sep;
2416
+ if (mcp.problems.length > 0)
2417
+ this.startupLine({ level: "warning", text: `MCP configuration: ${mcp.problems.length === 1 ? "1 problem" : `${mcp.problems.length} problems`} ${sep} /mcp` });
2418
+ const approval = approvalNotice(mcp.pendingApprovals().map((definition) => definition.name), sep);
2419
+ if (approval !== undefined)
2420
+ this.startupLine(approval);
2421
+ await mcp.startSession().catch(() => undefined);
2422
+ if (this.exiting)
2423
+ return;
2424
+ for (const notice of mcpStartNotices(mcp.status(), sep))
2425
+ this.startupLine(notice);
2426
+ }
2427
+ effortHost() {
2428
+ return {
2429
+ runtime: this.runtime,
2430
+ controls: this.renderer.controls,
2431
+ signal: this.outer.signal,
2432
+ ok: this.glyphs.ok,
2433
+ sep: this.glyphs.sep,
2434
+ conversationRule: () => sessionRouteRule(this.runtime, this.sessionTier),
2435
+ print: (lines) => this.print(lines),
2436
+ showFailure: (error) => this.showFailure(failureInfo(error)),
2437
+ refreshStatus: () => this.refreshStatus(),
2438
+ };
2439
+ }
2440
+ /** K3: the footer's model and effort from live state (after start, /model, /effort). */
2441
+ refreshStatus() {
2442
+ const controls = this.renderer.controls;
2443
+ const rule = sessionRouteRule(this.runtime, this.sessionTier);
2444
+ if (controls?.setSessionStatus === undefined || rule === undefined)
2445
+ return;
2446
+ const resolution = this.runtime.effortFor(rule.tier, "session", rule.route);
2447
+ const effort = effortLabel(resolution, rule.route.adapter_id);
2448
+ controls.setSessionStatus({ model: rule.route.model_id, effort: effort === "default" || effort === "n/a" ? undefined : effort });
2449
+ }
2450
+ /** Switches the conversation's route for this session; `save` persists it as the default only after the human confirms. */
2451
+ async switchModel(tier, save) {
2452
+ const g = this.glyphs;
2453
+ const runtime = this.runtime;
2454
+ const rule = this.sessionRules().find((candidate) => candidate.tier === tier);
2455
+ if (rule === undefined)
2456
+ return;
2457
+ const decision = runtime.router.resolve({ tier, role: "session" }, this.outer.signal);
2458
+ try {
2459
+ await decision;
2460
+ }
2461
+ catch (error) {
2462
+ this.showFailure(failureInfo(error));
2463
+ return;
2464
+ }
2465
+ this.routePromise = decision;
2466
+ this.routeRecorded = false;
2467
+ this.sessionTier = tier;
2468
+ this.currentModel = rule.route.model_id;
2469
+ this.refreshStatus();
2470
+ this.print([`${g.ok} Conversation model: ${rule.route.model_id} (${tier}) for this conversation${save ? "" : ` ${g.sep} /model ${tier} --save makes it the default`}`]);
2471
+ if (!save)
2472
+ return;
2473
+ const answer = await this.askUser(`Make ${rule.route.model_id} (${tier}) the default conversation model for new conversations?`, ["Yes", "No"], this.outer.signal).catch(() => "No");
2474
+ if (!/^(1|y|yes|evet|e)$/i.test(answer.trim())) {
2475
+ this.print(["Not saved; the default is unchanged."]);
2476
+ return;
2477
+ }
2478
+ await mkdir(runtime.home, { recursive: true });
2479
+ await writeFile(path.join(runtime.home, SESSION_MODEL_FILE), `${JSON.stringify({ tier }, null, 2)}\n`, "utf8");
2480
+ this.print([`${g.ok} Saved: new conversations use the ${tier} route (${path.join(runtime.home, SESSION_MODEL_FILE)})`]);
2481
+ }
2482
+ /**
2483
+ * `/review [--staged | <commit> | <from>..<to> | run-<n> | fix] [focus]` (ADR-09): pins the target's
2484
+ * diff (digest of the full content, generated files left out), resolves the reviewer route
2485
+ * (cross-provider when possible) and runs a read-only reviewer attempt through the worker manager
2486
+ * as its own background run (`/runs`); the verdict card, `/evidence` and "Fix these?" follow.
2487
+ */
2488
+ async review(argument) {
2489
+ const g = this.glyphs;
2490
+ const runtime = this.runtime;
2491
+ const root = runtime.workspaceRoot;
2492
+ const parsed = parseReviewArgument(argument);
2493
+ if (parsed.fix) {
2494
+ if (this.lastReview === undefined)
2495
+ this.print(["No review in this conversation yet; /review runs one."]);
2496
+ else
2497
+ await this.offerReviewFixes(this.lastReview, true);
2498
+ return;
2499
+ }
2500
+ let target;
2501
+ let runLabel;
2502
+ if (parsed.target.kind === "run") {
2503
+ const run = this.findRun(parsed.target.ref);
2504
+ if (run === undefined || run.kind === "review") {
2505
+ this.print([run === undefined ? this.unknownRun(parsed.target.ref) : `${run.id} is a review run; review a worker run instead`]);
2506
+ return;
2507
+ }
2508
+ const paths = run.tracker.integratedPaths();
2509
+ if (paths.length === 0) {
2510
+ this.print([`${run.id} integrated no files${run.status === "running" ? " yet" : ""}; nothing to review.`]);
2511
+ return;
2512
+ }
2513
+ target = { kind: "workspace", paths };
2514
+ runLabel = `files integrated by ${run.id}`;
2515
+ }
2516
+ else
2517
+ target = parsed.target;
2518
+ let diff = await reviewDiff(root, target, isGeneratedPath, this.outer.signal);
2519
+ if (typeof diff === "string") {
2520
+ this.print([`/review: ${diff === "not a git repository" ? "needs a git repository (the diff is what gets reviewed)" : diff}`]);
2521
+ return;
2522
+ }
2523
+ if (diff.text.trim() === "" && parsed.target.kind === "workspace") {
2524
+ // Nothing uncommitted (e.g. the last run was committed): review the last commit instead.
2525
+ const last = await reviewDiff(root, { kind: "commit", rev: "HEAD" }, isGeneratedPath, this.outer.signal);
2526
+ if (typeof last !== "string" && last.text.trim() !== "")
2527
+ diff = { ...last, label: `last ${last.label}` };
2528
+ }
2529
+ if (diff.text.trim() === "") {
2530
+ this.print([diff.excluded.length > 0 ? `Only generated files changed (${diff.excluded.slice(0, 3).join(", ")}); nothing to review.` : `Nothing to review in ${diff.label}.`]);
2531
+ return;
2532
+ }
2533
+ const label = runLabel ?? diff.label;
2534
+ // ADR-09 reviewer routing, as for workers: routes.<tier>.reviewer wins, otherwise review.cross_provider
2535
+ // prefers a provider other than the one that wrote the changes (the conversation's route).
2536
+ const rules = runtime.routeRules();
2537
+ const explicit = rules.find((candidate) => candidate.role === "reviewer");
2538
+ const tier = explicit?.tier ?? (rules.some((candidate) => candidate.tier === "complex_worker") ? "complex_worker" : (sessionRouteRule(runtime, this.sessionTier)?.tier ?? "complex_worker"));
2539
+ const implementer = await this.routePromise?.then((decision) => decision.route).catch(() => undefined);
2540
+ let route;
2541
+ try {
2542
+ route = await runtime.router.resolve({ tier, role: "reviewer", ...(implementer === undefined ? {} : { implementer }) }, this.outer.signal);
2543
+ }
2544
+ catch (error) {
2545
+ this.showFailure(failureInfo(error));
2546
+ return;
2547
+ }
2548
+ const digest = digestOf({ diff: diff.text });
2549
+ const truncated = Buffer.byteLength(diff.text, "utf8") > REVIEW_DIFF_LIMIT;
2550
+ const shown = truncated ? Buffer.from(diff.text, "utf8").subarray(0, REVIEW_DIFF_LIMIT).toString("utf8") : diff.text;
2551
+ const resolved = diff.resolved;
2552
+ const artifact = {
2553
+ digest,
2554
+ brief: renderArtifactReviewBrief({ label, digest, diff: shown, truncated, changedPaths: diff.files, excludedPaths: diff.excluded, focus: parsed.focus }),
2555
+ repin: async (signal) => {
2556
+ const again = await reviewDiff(root, resolved, isGeneratedPath, signal);
2557
+ return typeof again === "string" ? undefined : digestOf({ diff: again.text });
2558
+ },
2559
+ };
2560
+ const request = {
2561
+ workspaceRoot: root,
2562
+ policyMode: runtime.policyMode,
2563
+ headless: this.renderer.approvals.availability === "headless",
2564
+ target: parsed.target,
2565
+ label,
2566
+ focus: parsed.focus,
2567
+ tier: tier === "session" ? "complex_worker" : tier,
2568
+ route,
2569
+ implementer,
2570
+ changedPaths: diff.files,
2571
+ excludedPaths: diff.excluded,
2572
+ artifact,
2573
+ };
2574
+ this.coordinator ??= runtime.createCoordinator(runtime.brokerFor(this.gatedApprovals()));
2575
+ const goal = artifactReviewGoal(label, parsed.focus);
2576
+ const background = this.canRunInBackground();
2577
+ const run = {
2578
+ id: `run-${this.workerRuns.length + 1}`,
2579
+ goal,
2580
+ tracker: new OrchestrationTracker(goal, undefined),
2581
+ coordinator: this.coordinator,
2582
+ controller: linked(this.outer.signal),
2583
+ disarm: undefined,
2584
+ foreground: !background,
2585
+ status: "running",
2586
+ done: Promise.resolve({ status: "ok", text: "", truncated: false, redactions: 0 }),
2587
+ result: undefined,
2588
+ notice: undefined,
2589
+ kind: "review",
2590
+ };
2591
+ this.workerRuns.push(run);
2592
+ const reviewer = `${route.route.provider_id}/${route.route.model_id}`;
2593
+ const crossProvider = implementer !== undefined && implementer.provider_id !== route.route.provider_id;
2594
+ this.note("info", `${g.bullet} Reviewing ${diff.files.length} file${diff.files.length === 1 ? "" : "s"} (${label}) with ${reviewer}${crossProvider ? " (another provider)" : ""} ${g.sep} fresh context, read-only, artifact ${digest.slice(0, 19)}… pinned ${g.sep} ${background ? `background ${run.id} · /runs` : "waiting"}`);
2595
+ run.done = this.driveReview(run, request);
2596
+ if (!background)
2597
+ await run.done;
2598
+ }
2599
+ /** Runs one `/review` to its card; never rejects. */
2600
+ async driveReview(run, request) {
2601
+ const g = this.glyphs;
2602
+ const result = await run.coordinator.review(request, run.controller.signal).catch((error) => ({
2603
+ kind: "artifact-review",
2604
+ runId: "",
2605
+ sessionId: "",
2606
+ reviewerAttemptId: undefined,
2607
+ reviewerSessionId: undefined,
2608
+ target: request.label,
2609
+ targetKind: request.target.kind,
2610
+ artifactDigest: request.artifact.digest,
2611
+ changedPaths: request.changedPaths,
2612
+ excludedPaths: request.excludedPaths,
2613
+ reviewer: { provider_id: request.route.route.provider_id, model_id: request.route.route.model_id },
2614
+ sameProvider: undefined,
2615
+ status: "failed",
2616
+ verdict: undefined,
2617
+ criteria: [],
2618
+ findings: [],
2619
+ evidence: [],
2620
+ problems: [failureInfo(error).message],
2621
+ }));
2622
+ run.status = result.status === "reviewed" || result.status === "artifact_changed" ? "succeeded" : result.status === "cancelled" ? "cancelled" : "failed";
2623
+ run.tracker.finish(run.status);
2624
+ if (result.sessionId !== "" && !this.orchestratedSessions.includes(result.sessionId))
2625
+ this.orchestratedSessions.push(result.sessionId);
2626
+ this.lastReview = result;
2627
+ const card = renderReviewCard(result, g);
2628
+ const level = result.verdict === "approve" ? "info" : "warning";
2629
+ for (const [index, line] of card.entries())
2630
+ this.note(index === 0 ? level : "info", line);
2631
+ const text = JSON.stringify(result);
2632
+ run.result = { status: "ok", text: text.slice(0, 16 * 1024), truncated: text.length > 16 * 1024, redactions: 0 };
2633
+ if (!this.exiting && result.verdict !== undefined) {
2634
+ const findings = result.findings.map((finding) => `${finding.severity} ${finding.path ?? ""}${finding.line === undefined ? "" : `:${finding.line}`} ${finding.summary}`.replace(/\s+/g, " ").trim());
2635
+ this.pendingNotes.push(snippet(`the user ran /review (${result.target}); the independent reviewer (${result.reviewer.provider_id}/${result.reviewer.model_id}, fresh context) verdict: ${result.verdict.replaceAll("_", " ")}${findings.length === 0 ? ", no findings" : `; findings: ${findings.join("; ")}`}`, 1500));
2636
+ }
2637
+ if (!this.exiting && actionableFindings(result).length > 0)
2638
+ await this.offerReviewFixes(result, false);
2639
+ return run.result;
2640
+ }
2641
+ /**
2642
+ * "Fix these?" after a review: the TUI's choice modal (every finding checked); the accepted ones
2643
+ * reach the session agent as a follow-up message. Plain mode sends them on `/review fix`.
2644
+ */
2645
+ async offerReviewFixes(result, explicit) {
2646
+ const g = this.glyphs;
2647
+ const findings = actionableFindings(result);
2648
+ if (findings.length === 0) {
2649
+ if (explicit)
2650
+ this.print(["The last review has no findings to fix."]);
2651
+ return;
2652
+ }
2653
+ let chosen = findings;
2654
+ if (this.renderer.kind === "tui" && this.renderer.controls !== undefined) {
2655
+ const answer = await this.typingPause(this.outer.signal)
2656
+ .then(() => this.choose({
2657
+ question: `Fix these? ${findings.length} finding${findings.length === 1 ? "" : "s"} from the review of ${result.target}`,
2658
+ header: "Review",
2659
+ subtitle: "checked findings go to Synorch as a follow-up message",
2660
+ options: findings.map((finding) => ({
2661
+ label: snippet(`${finding.severity} ${finding.path === undefined ? "" : `${finding.path}${finding.line === undefined ? "" : `:${finding.line}`} `}${finding.summary}`, 140),
2662
+ ...(finding.recommendation === undefined ? {} : { description: snippet(finding.recommendation, 160) }),
2663
+ })),
2664
+ multiSelect: true,
2665
+ initialChecked: findings.map((_, index) => index),
2666
+ allowOther: false,
2667
+ escapeLabel: "not now",
2668
+ tone: "neutral",
2669
+ }, this.outer.signal))
2670
+ .catch(() => undefined);
2671
+ if (answer?.kind !== "selected" || answer.indices.length === 0) {
2672
+ this.note("info", `Not sent ${g.sep} /review fix sends the findings later`);
2673
+ return;
2674
+ }
2675
+ chosen = answer.indices.flatMap((index) => (findings[index] === undefined ? [] : [findings[index]]));
2676
+ }
2677
+ else if (!explicit) {
2678
+ this.note("info", `/review fix sends ${findings.length === 1 ? "this finding" : `these ${findings.length} findings`} to Synorch`);
2679
+ return;
2680
+ }
2681
+ this.queued.push({ text: fixFollowUpMessage(result, chosen), attachments: [], verbatim: true });
2682
+ this.note("info", `${g.name === "rich" ? "→" : "->"} ${chosen.length} finding${chosen.length === 1 ? "" : "s"} sent to Synorch${this.turnRunning ? " (after the current turn)" : ""}`);
2683
+ this.wakeIdle.abort();
2684
+ }
2685
+ async commit(argument) {
2686
+ const g = this.glyphs;
2687
+ const root = this.runtime.workspaceRoot;
2688
+ const changes = await uncommittedChanges(root, this.outer.signal);
2689
+ if (changes === undefined) {
2690
+ this.print(["/commit needs a git repository."]);
2691
+ return;
2692
+ }
2693
+ if (changes.files.length === 0) {
2694
+ this.print(["Nothing to commit: the working tree is clean."]);
2695
+ return;
2696
+ }
2697
+ const tui = this.renderer.kind === "tui" && this.renderer.controls !== undefined;
2698
+ let selected = changes.files;
2699
+ if (tui && changes.files.length > 1) {
2700
+ // Selective staging: every changed file is checked; Space unchecks what stays out of this commit.
2701
+ const picked = await this.pickCommitFiles(changes.files);
2702
+ if (picked === undefined) {
2703
+ this.print(["Not committed."]);
2704
+ return;
2705
+ }
2706
+ selected = picked;
2707
+ }
2708
+ const all = () => selected.length === changes.files.length;
2709
+ const statFor = () => (all() ? changes.stat : changes.stat.split(/\r?\n/).filter((line) => selected.some((file) => changePathspecs(file).some((spec) => line.includes(spec)))).join("\n"));
2710
+ const statTail = all() ? (changes.stat.split(/\r?\n/).at(-1)?.trim() ?? "") : "";
2711
+ let message = argument.trim() === "" ? await this.proposeCommitMessage(selected.map((file) => file.path), statFor()) : argument.trim();
2712
+ const countOf = (files) => `${files.length} file${files.length === 1 ? "" : "s"}`;
2713
+ const views = this.renderer.views;
2714
+ if (views !== undefined) {
2715
+ // UX-03 action card: what, why, consequence, reversibility — before the human decides.
2716
+ views.showView({
2717
+ kind: "action",
2718
+ title: `Commit ${countOf(selected)}?`,
2719
+ what: `git commit -m "${snippet(message.split(/\r?\n/)[0] ?? message, 90)}"`,
2720
+ why: "you ran /commit",
2721
+ consequence: all()
2722
+ ? `stages every uncommitted change (git add -A) and records one commit${statTail === "" ? "" : ` · ${statTail}`}`
2723
+ : `stages and commits only the ${countOf(selected)} you selected; the other ${changes.files.length - selected.length} stay uncommitted`,
2724
+ effect: "local",
2725
+ reversible: true,
2726
+ paths: selected.map((file) => file.path),
2727
+ scope: "this commit once; nothing is pushed",
2728
+ });
2729
+ this.print(["Message:", ...message.split(/\r?\n/).map((line) => ` ${line}`)]);
2730
+ }
2731
+ else {
2732
+ const lines = [`${g.bullet} Commit ${g.sep} ${countOf(selected)}`];
2733
+ for (const file of selected.slice(0, 15))
2734
+ lines.push(` ${file.status.padEnd(3)}${file.path}`);
2735
+ if (selected.length > 15)
2736
+ lines.push(` … ${selected.length - 15} more`);
2737
+ if (statTail !== "")
2738
+ lines.push(` ${statTail}`);
2739
+ this.print([...lines, "Proposed message:", ...message.split(/\r?\n/).map((line) => ` ${line}`)]);
2740
+ }
2741
+ const choices = ["Commit", "Edit the message", ...(!tui && changes.files.length > 1 ? ["Choose files"] : []), "Cancel"];
2742
+ const answer = (await this.askUser(`Commit ${countOf(selected)} with this message?`, choices, this.outer.signal).catch(() => "Cancel")).trim();
2743
+ if (/^(edit|e$)/i.test(answer)) {
2744
+ message = (await this.askUser("Type the commit message", undefined, this.outer.signal).catch(() => "")).trim();
2745
+ if (message === "") {
2746
+ this.print(["Not committed."]);
2747
+ return;
2748
+ }
2749
+ }
2750
+ else if (/^choose/i.test(answer)) {
2751
+ const picked = await this.pickCommitFiles(changes.files);
2752
+ if (picked === undefined) {
2753
+ this.print(["Not committed."]);
2754
+ return;
2755
+ }
2756
+ selected = picked;
2757
+ }
2758
+ else if (!/^(y|yes|commit|evet)/i.test(answer)) {
2759
+ this.print(["Not committed."]);
2760
+ return;
2761
+ }
2762
+ const result = all() ? await commitAll(root, message, this.outer.signal) : await commitSelected(root, message, selected, this.outer.signal);
2763
+ if (!result.ok) {
2764
+ this.note("error", `${g.fail} git commit failed: ${snippet(result.stderr || result.stdout, 300)}`);
2765
+ return;
2766
+ }
2767
+ const subject = message.split(/\r?\n/)[0] ?? message;
2768
+ const left = changes.files.length - selected.length;
2769
+ this.print([`${g.ok} Committed ${countOf(selected)}: ${subject}${left === 0 ? "" : ` ${g.sep} ${left} left uncommitted`}`]);
2770
+ this.pendingNotes.push(`the user committed ${all() ? "the working tree" : `${selected.map((file) => file.path).join(", ")}`} (/commit) with the message "${snippet(subject, 120)}".`);
2771
+ }
2772
+ /** The changed files as a multi-select, all checked; undefined when dismissed or nothing is left checked. */
2773
+ async pickCommitFiles(files) {
2774
+ const answer = await this.choose({
2775
+ question: "Files to commit",
2776
+ subtitle: "Space toggles · Enter continues with the checked files · Esc cancels",
2777
+ options: files.map((file) => ({ label: `${file.status.padEnd(2)} ${file.path}` })),
2778
+ multiSelect: true,
2779
+ initialChecked: files.map((_, index) => index),
2780
+ allowOther: false,
2781
+ escapeLabel: "cancel",
2782
+ tone: "neutral",
2783
+ }, this.outer.signal).catch(() => undefined);
2784
+ if (answer?.kind !== "selected")
2785
+ return undefined;
2786
+ const picked = answer.indices.flatMap((index) => (files[index] === undefined ? [] : [files[index]]));
2787
+ return picked.length === 0 ? undefined : picked;
2788
+ }
2789
+ /** A one-shot request to the conversation model for a commit message; a plain summary when that is not possible. */
2790
+ async proposeCommitMessage(files, stat) {
2791
+ const fallback = `chore: update ${files.slice(0, 3).join(", ")}${files.length > 3 ? ` and ${files.length - 3} more` : ""}`;
2792
+ const route = await this.routePromise?.catch(() => undefined);
2793
+ if (route === undefined)
2794
+ return fallback;
2795
+ const adapter = this.runtime.router.adapterFor(route.route);
2796
+ if (adapter.kind !== "model")
2797
+ return fallback;
2798
+ const goals = (await this.readEvents())
2799
+ .flatMap((event) => (event.type === "message/recorded" && event.data.role === "user" && event.data.message !== undefined ? [event.data.message.content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join(" ")] : []))
2800
+ .map((text) => snippet(text.replace(/^\[Synorch note:[^\]]*\]\s*/u, "").split("<synorch-attachments>")[0] ?? "", 160))
2801
+ .slice(-5);
2802
+ const request = {
2803
+ request_id: createId("request"),
2804
+ route: route.route,
2805
+ system: [],
2806
+ messages: [
2807
+ {
2808
+ role: "user",
2809
+ content: [
2810
+ {
2811
+ type: "text",
2812
+ text: `Write a git commit message for these changes: a conventional-commit subject line of at most 72 characters, then optionally a blank line and a short body. Reply with the message only.\n\nWhat the user asked for:\n${goals.map((goal) => `- ${goal}`).join("\n") || "- (no conversation yet)"}\n\nDiff stat:\n${stat.slice(0, 4000) || files.join("\n")}`,
2813
+ },
2814
+ ],
2815
+ },
2816
+ ],
2817
+ tools: [],
2818
+ max_output_tokens: 300,
2819
+ };
2820
+ try {
2821
+ const signal = AbortSignal.any([this.outer.signal, AbortSignal.timeout(30_000)]);
2822
+ const credential = await this.runtime.credentials(route.route, signal);
2823
+ let text = "";
2824
+ for await (const event of adapter.stream(request, credential, signal)) {
2825
+ if (event.type === "text_delta")
2826
+ text += event.text;
2827
+ if (event.type === "done" && text === "")
2828
+ text = event.message.content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("");
2829
+ if (event.type === "error")
2830
+ return fallback;
2831
+ }
2832
+ const cleaned = text.trim().replace(/^```\w*\n?|```$/g, "").trim();
2833
+ return cleaned === "" ? fallback : cleaned.slice(0, 2000);
2834
+ }
2835
+ catch {
2836
+ return fallback;
2837
+ }
2838
+ }
2839
+ async usage() {
2840
+ const ledger = this.usageLedger;
2841
+ if (ledger === undefined)
2842
+ return;
2843
+ const usagePage = async () => cardPanel("Usage", await ledger.view(Date.now() - this.startedAt), [], usagePage);
2844
+ if (await this.showPanel(usagePage))
2845
+ return;
2846
+ const views = this.renderer.views;
2847
+ if (views !== undefined)
2848
+ views.showView(await ledger.view(Date.now() - this.startedAt, await this.quotaPlans(), quotaProviderLabel));
2849
+ else
2850
+ this.print(await ledger.report());
2851
+ }
2852
+ /** Provider id → plan label (`ChatGPT Plus`, `Claude Code`) for `/usage`, subscription routes first. No network request. */
2853
+ async quotaPlans() {
2854
+ const runtime = this.runtime;
2855
+ const plans = new Map();
2856
+ const method = (adapterId) => runtime.adapters.find((adapter) => adapter.adapterId === adapterId)?.authMethod;
2857
+ const rules = [...runtime.routeRules()].sort((left, right) => Number(method(left.route.adapter_id) === "api-key") - Number(method(right.route.adapter_id) === "api-key"));
2858
+ for (const rule of rules) {
2859
+ if (plans.has(rule.route.provider_id))
2860
+ continue;
2861
+ const plan = (await subscriptionPlan(runtime, rule.route, this.outer.signal).catch(() => undefined)) ?? planLabel(runtime, rule.route);
2862
+ if (plan !== undefined)
2863
+ plans.set(rule.route.provider_id, plan);
2864
+ }
2865
+ return plans;
2866
+ }
2867
+ async cost() {
2868
+ this.print(this.usageLedger?.cost() ?? ["No usage recorded yet."]);
2869
+ }
2870
+ /** `/ps [kill <handle|all>]`: background processes of this session (conversation and workers). */
2871
+ async ps(argument) {
2872
+ const processes = this.runtime.processes;
2873
+ const [verb, target] = argument.trim().split(/\s+/);
2874
+ if (verb === "kill" || verb === "stop") {
2875
+ if (target === undefined || target === "all") {
2876
+ const count = await processes.killAll();
2877
+ this.print([count === 0 ? "No background process is running." : `Stopped ${count} background process${count === 1 ? "" : "es"}.`]);
2878
+ return;
2879
+ }
2880
+ const stopped = await processes.kill(target);
2881
+ this.print([stopped === undefined ? `No background process ${target}; /ps lists them.` : `Stopped ${describeProcess(stopped)}`]);
2882
+ return;
2883
+ }
2884
+ const all = processes.list();
2885
+ if (all.length === 0) {
2886
+ this.print(["No background processes. The agent starts one with exec {background: true} (dev servers, watchers, long test runs)."]);
2887
+ return;
2888
+ }
2889
+ const g = this.glyphs;
2890
+ this.print([
2891
+ ...all.map((info) => `${info.state === "running" ? (g.name === "rich" ? "◌" : "o") : info.state === "exited" && info.exitCode === 0 ? g.ok : g.fail} ${describeProcess(info)}${info.owner === "session" ? "" : ` ${g.sep} worker`}`),
2892
+ `/ps kill <handle|all> stops them ${g.sep} all are stopped when the session ends`,
2893
+ ]);
2894
+ }
2895
+ /** Background process lifecycle in the view: live row status and a note when one ends by itself. */
2896
+ watchProcesses() {
2897
+ const processes = this.runtime.processes;
2898
+ const unbindStatus = bindBackgroundStatus((handle) => {
2899
+ const info = processes.get(handle);
2900
+ if (info === undefined)
2901
+ return undefined;
2902
+ const ended = info.state === "running" ? undefined : info.state === "exited" ? `exited ${info.exitCode ?? info.signal ?? "?"}` : info.state === "killed" ? "stopped" : info.state === "timeout" ? "timed out" : "failed to start";
2903
+ return { running: info.state === "running", startedAt: info.startedAt, endedAt: info.endedAt, ended };
2904
+ });
2905
+ const unsubscribe = processes.onChange((info, change) => {
2906
+ if (change !== "ended" || info.state === "killed" || this.exiting)
2907
+ return;
2908
+ const failed = info.state !== "exited" || info.exitCode !== 0;
2909
+ this.note(failed ? "warning" : "info", `${failed ? this.glyphs.warn : this.glyphs.ok} background ${describeProcess(info)}`);
2910
+ });
2911
+ const onExit = () => {
2912
+ processes.killAllSync();
2913
+ this.runtime.mcp.killAllSync();
2914
+ };
2915
+ process.once("exit", onExit);
2916
+ return () => {
2917
+ unbindStatus();
2918
+ unsubscribe();
2919
+ process.removeListener("exit", onExit);
2920
+ };
2921
+ }
2922
+ /** Events of this conversation's worker runs (this process, or recorded in orchestrate results when resumed). */
2923
+ async orchestrationEvents() {
2924
+ const ids = new Set(this.orchestratedSessions);
2925
+ for (const event of await this.readEvents()) {
2926
+ if (event.type !== "message/recorded" || event.data.role !== "tool" || event.data.message === undefined)
2927
+ continue;
2928
+ for (const part of event.data.message.content) {
2929
+ if (part.type !== "tool_result")
2930
+ continue;
2931
+ for (const match of part.text.matchAll(ORCHESTRATION_SESSION))
2932
+ if (match[1] !== undefined)
2933
+ ids.add(match[1]);
2934
+ }
2935
+ }
2936
+ const events = [];
2937
+ for (const id of ids) {
2938
+ try {
2939
+ const reader = await this.runtime.sessions.openForRead(id);
2940
+ for await (const item of reader.read())
2941
+ if (item.status === "ok")
2942
+ events.push(item.event);
2943
+ }
2944
+ catch {
2945
+ continue;
2946
+ }
2947
+ }
2948
+ return events;
2949
+ }
2950
+ /** The conversation's worker runs (this process, or recorded in orchestrate results when resumed), oldest first. */
2951
+ async orchestrationRuns() {
2952
+ const ids = new Set(this.orchestratedSessions);
2953
+ for (const event of await this.readEvents()) {
2954
+ if (event.type !== "message/recorded" || event.data.role !== "tool" || event.data.message === undefined)
2955
+ continue;
2956
+ for (const part of event.data.message.content) {
2957
+ if (part.type !== "tool_result")
2958
+ continue;
2959
+ for (const match of part.text.matchAll(ORCHESTRATION_SESSION))
2960
+ if (match[1] !== undefined)
2961
+ ids.add(match[1]);
2962
+ }
2963
+ }
2964
+ const runs = [];
2965
+ for (const id of ids) {
2966
+ try {
2967
+ const reader = await this.runtime.sessions.openForRead(id);
2968
+ const events = [];
2969
+ for await (const item of reader.read())
2970
+ if (item.status === "ok")
2971
+ events.push(item.event);
2972
+ runs.push({ sessionId: id, events });
2973
+ }
2974
+ catch {
2975
+ continue;
2976
+ }
2977
+ }
2978
+ return runs.sort((left, right) => (left.events[0]?.timestamp ?? "").localeCompare(right.events[0]?.timestamp ?? ""));
2979
+ }
2980
+ /** K2 `/evidence [turn | <n>]` (UX-04): the last turn or worker run, criterion by criterion, from the log only. */
2981
+ async evidence(argument = "") {
2982
+ const view = await buildEvidence({ blobs: this.runtime.blobs, conversation: await this.readEvents(), runs: await this.orchestrationRuns(), diff: await this.diffView() }, argument);
2983
+ const views = this.renderer.views;
2984
+ if (views !== undefined) {
2985
+ views.showView(view);
2986
+ return;
2987
+ }
2988
+ if (view.criteria.length === 0) {
2989
+ this.print([view.next ?? "No evidence recorded yet."]);
2990
+ return;
2991
+ }
2992
+ this.print(view.criteria.map((criterion) => `${criterion.status}: ${criterion.text}`));
2993
+ }
2994
+ /** K2 `/why [last | <n> | <tool> | model]` (X6): mode, layer, rule, who answered, and what would change it. */
2995
+ async why(argument) {
2996
+ const runtime = this.runtime;
2997
+ const mode = runtime.permissionMode();
2998
+ const recorded = runtime.trust.recorded();
2999
+ const trusted = runtime.sandbox.enforcement === "full" ? "not needed (full sandbox)" : recorded.trusted ? "trusted" : runtime.trust.state().trusted ? "trusted for this session" : "not trusted (build/test commands ask first; /trust)";
3000
+ const result = buildWhy(await this.readEvents(), argument, {
3001
+ mode: mode === undefined ? "default-deny (no prompts; headless)" : `${mode}: ${MODE_MEANINGS[mode]}`,
3002
+ trust: trusted,
3003
+ sandbox: `${runtime.sandbox.backend} (${runtime.sandbox.enforcement})`,
3004
+ grants: this.grantList,
3005
+ planOn: this.planOn,
3006
+ noPermissionMode: mode === undefined,
3007
+ });
3008
+ if (typeof result === "string") {
3009
+ this.print([result]);
3010
+ return;
3011
+ }
3012
+ const views = this.renderer.views;
3013
+ if (views !== undefined) {
3014
+ views.showView(result);
3015
+ return;
3016
+ }
3017
+ const lines = [`${result.subject} ${this.glyphs.sep} ${result.decision}`];
3018
+ for (const reason of result.reasons)
3019
+ lines.push(` ${reason.layer}: ${reason.message} (${reason.code})`);
3020
+ for (const fact of result.facts ?? [])
3021
+ lines.push(` ${fact.label}: ${fact.text}`);
3022
+ for (const entry of result.howToChange)
3023
+ lines.push(` ${this.glyphs.name === "rich" ? "→" : "->"} ${entry.command}: ${entry.effect}`);
3024
+ this.print(lines);
3025
+ }
3026
+ async compact(focus) {
3027
+ const log = this.log;
3028
+ if (log === undefined) {
3029
+ this.print(["Nothing to compact yet."]);
3030
+ return;
3031
+ }
3032
+ const events = await this.readEvents();
3033
+ const history = await reconstructHistory(events, this.runtime.blobs, { role: "session", taskId: undefined, attemptId: undefined });
3034
+ const tokensBefore = history.messages.reduce((sum, entry) => sum + messageTokens(entry.message), 0);
3035
+ const compactor = createCompactor({
3036
+ blobs: this.runtime.blobs,
3037
+ writerFor: (sessionId) => (sessionId === log.sessionId ? log : undefined),
3038
+ keepRecentTokens: 6_000,
3039
+ summarize: async (input, signal) => {
3040
+ const content = await extractiveSummarizer(input, signal);
3041
+ return focus === "" ? content : { ...content, summary: `Focus requested by the user: ${focus}\n${content.summary}` };
3042
+ },
3043
+ });
3044
+ const outcome = await compactor.compact({ sessionId: log.sessionId, events, messages: history.messages, trigger: "manual", tokensBefore }, this.outer.signal);
3045
+ if (outcome.status === "compacted") {
3046
+ const data = outcome.event.type === "context/compacted" ? outcome.event.data : undefined;
3047
+ this.print([`${this.glyphs.ok} Compacted: ~${data?.tokens_before ?? tokensBefore} → ~${data?.tokens_after ?? "?"} tokens ${this.glyphs.sep} older messages are summarized (the log keeps everything)`]);
3048
+ }
3049
+ else if (outcome.status === "nothing-to-compact")
3050
+ this.print(["Nothing to compact: the conversation is still short."]);
3051
+ else if (outcome.status === "thrash")
3052
+ this.print(["Just compacted; try again after a few more steps."]);
3053
+ else
3054
+ this.print(["Compaction is unavailable for this conversation."]);
3055
+ }
3056
+ async init(argument) {
3057
+ await runInitCommand({ runtime: this.runtime, print: (lines) => this.print(lines), ask: (question, options) => this.askUser(question, options, this.outer.signal) }, argument);
3058
+ }
3059
+ async report(name, argument) {
3060
+ switch (name) {
3061
+ case "context": {
3062
+ // K2 "Why this context?" (UX-07): provenance and token estimate of every block of the last request.
3063
+ const events = await this.readEvents();
3064
+ const view = await buildContextView(events, this.runtime.blobs, DEFAULT_CONTEXT_WINDOW).catch(() => undefined);
3065
+ if (view !== undefined && (await this.showPanel(() => cardPanel("Context", view))))
3066
+ return;
3067
+ const views = this.renderer.views;
3068
+ if (view !== undefined && views !== undefined)
3069
+ views.showView(view);
3070
+ else if (view !== undefined)
3071
+ this.print(view.groups.flatMap((group) => [`${group.title}:`, ...group.items.map((item) => ` ${item.label} ~${item.tokens}${item.detail === undefined ? "" : ` (${item.detail})`}`)]));
3072
+ else {
3073
+ const profile = this.runtime.profile.current();
3074
+ this.print([...contextReport(events), ...(profile === undefined ? [] : ["Project profile (sent with every request):", ...profileSummaryLines(profile).map((line) => ` ${line}`)])]);
3075
+ }
3076
+ return;
3077
+ }
3078
+ case "permissions":
3079
+ await this.permissions(argument);
3080
+ return;
3081
+ case "tasks": {
3082
+ const events = await this.orchestrationEvents();
3083
+ this.print(events.length === 0 ? ["No worker runs in this conversation yet."] : tasksReport(events));
3084
+ return;
3085
+ }
3086
+ case "memory":
3087
+ if (argument.trim() === "" && (await this.showPanel(() => memoryPanel(this.memoryDesk()))))
3088
+ return;
3089
+ await runMemoryDesk(this.memoryDesk(), argument);
3090
+ return;
3091
+ case "diff": {
3092
+ const view = await this.diffView();
3093
+ const views = this.renderer.views;
3094
+ if (views !== undefined)
3095
+ views.showView(view);
3096
+ else
3097
+ this.print(diffLines(view));
3098
+ return;
3099
+ }
3100
+ case "log": {
3101
+ const count = Number(argument.split(/\s+/)[0] || "20");
3102
+ const events = await this.readEvents();
3103
+ const shown = events.slice(-Math.max(1, Number.isFinite(count) ? count : 20));
3104
+ this.print(shown.length === 0
3105
+ ? ["Nothing recorded yet."]
3106
+ : shown.map((event) => {
3107
+ const line = describeEvent(event);
3108
+ return `[event] #${event.seq} ${event.type}${line === undefined ? "" : ` · ${line.text}`}`;
3109
+ }));
3110
+ }
3111
+ }
3112
+ }
3113
+ /**
3114
+ * `/diff`: every file Synorch changed in this conversation (undone edits excluded), diffed from
3115
+ * the content before Synorch's first edit to the content on disk now, plus files workers integrated.
3116
+ */
3117
+ async diffView() {
3118
+ const events = await this.readEvents();
3119
+ const restored = new Set(events.flatMap((event) => (event.type === "checkpoint/restored" ? [event.data.checkpoint_seq] : [])));
3120
+ const firstBefore = new Map();
3121
+ for (const event of events) {
3122
+ if (event.type !== "checkpoint/recorded" || restored.has(event.seq))
3123
+ continue;
3124
+ for (const file of event.data.files)
3125
+ if (!firstBefore.has(file.path))
3126
+ firstBefore.set(file.path, file.before);
3127
+ }
3128
+ const files = [];
3129
+ for (const [file, before] of firstBefore) {
3130
+ const place = this.inside(file);
3131
+ const current = place === undefined ? undefined : await readOptional(place.absolute).catch(() => undefined);
3132
+ const original = before === null ? undefined : await this.runtime.blobs.get(before.digest).then((bytes) => Buffer.from(bytes)).catch(() => undefined);
3133
+ const change = before === null ? "added" : current === undefined ? "deleted" : "modified";
3134
+ if ((original !== undefined && original.includes(0)) || (current !== undefined && current.includes(0))) {
3135
+ files.push({ path: file, change, added: 0, removed: 0, lines: [], note: "binary file" });
3136
+ continue;
3137
+ }
3138
+ if (before !== null && original === undefined) {
3139
+ files.push({ path: file, change, added: 0, removed: 0, lines: [], note: "the content before Synorch's edit is no longer available" });
3140
+ continue;
3141
+ }
3142
+ const diff = lineDiff(original?.toString("utf8") ?? "", current?.toString("utf8") ?? "");
3143
+ if (diff.added === 0 && diff.removed === 0)
3144
+ continue;
3145
+ files.push({ path: file, change, added: diff.added, removed: diff.removed, lines: diff.lines, ...(diff.tooLarge ? { note: "large change: shown as removed and added lines" } : {}) });
3146
+ }
3147
+ const runEvents = await this.orchestrationEvents();
3148
+ const reviewed = new Set(runEvents.flatMap((event) => (event.type === "review/recorded" && event.data.decision === "accept" ? [event.data.task_id] : [])));
3149
+ const integrated = new Map();
3150
+ for (const event of runEvents)
3151
+ if (event.type === "task/integrated")
3152
+ for (const file of event.data.paths)
3153
+ integrated.set(file, (integrated.get(file) ?? false) || reviewed.has(event.data.task_id));
3154
+ return { kind: "diff", files, ...(integrated.size === 0 ? {} : { integrated: [...integrated].map(([file, accepted]) => ({ path: file, reviewed: accepted })) }) };
3155
+ }
3156
+ async clear() {
3157
+ if (this.turnRunning || this.orchestration !== undefined) {
3158
+ this.print(["Synorch is working; /cancel first."]);
3159
+ return;
3160
+ }
3161
+ const previous = this.sessionId;
3162
+ await this.log?.close().catch(() => undefined);
3163
+ this.log = undefined;
3164
+ this.sessionId = undefined;
3165
+ this.driver = undefined;
3166
+ this.turnId = undefined;
3167
+ this.routeRecorded = false;
3168
+ this.orchestratedSessions.length = 0;
3169
+ this.pendingNotes.length = 0;
3170
+ this.unreportedRuns = 0;
3171
+ this.requests.clear();
3172
+ this.lastTracker = undefined;
3173
+ this.print([`${this.glyphs.ok} New conversation${previous === undefined ? "" : ` ${this.glyphs.sep} the previous one is saved (/resume)`}`]);
3174
+ }
3175
+ async resume(argument) {
3176
+ const list = (await this.conversations()).filter((entry) => entry.sessionId !== this.sessionId).slice(0, 20);
3177
+ const g = this.glyphs;
3178
+ const forkLabel = (entry) => (entry.forked ? ` ${g.sep} fork of ${entry.parentTitle === undefined ? "an earlier conversation" : `"${snippet(entry.parentTitle, 40)}"`}` : "");
3179
+ let chosen;
3180
+ if (argument === "") {
3181
+ if (list.length === 0) {
3182
+ this.print(["No other conversations in this folder."]);
3183
+ return;
3184
+ }
3185
+ const resumeEntries = list.map((entry) => ({ sessionId: entry.sessionId, title: snippet(entry.title, 90), when: relativeTime(Date.parse(entry.at)), forkOf: entry.forked ? (entry.parentTitle === undefined ? "an earlier conversation" : `"${snippet(entry.parentTitle, 40)}"`) : undefined }));
3186
+ if (await this.showPanel(() => resumePanel(resumeEntries, g.sep)))
3187
+ return;
3188
+ if (this.renderer.kind !== "tui" || this.renderer.controls === undefined) {
3189
+ this.print(["Recent conversations (/resume <n>):", ...list.map((entry, index) => ` ${String(index + 1).padStart(2)}. ${snippet(entry.title, 70)} ${g.sep} ${relativeTime(Date.parse(entry.at))}${forkLabel(entry)}`)]);
3190
+ return;
3191
+ }
3192
+ if (this.turnRunning || this.orchestration !== undefined) {
3193
+ this.print(["Synorch is working; /cancel first."]);
3194
+ return;
3195
+ }
3196
+ const answer = await this.choose({
3197
+ question: "Resume a conversation",
3198
+ subtitle: "Enter switches · Esc cancels · this one stays saved",
3199
+ options: list.map((entry) => ({ label: `${entry.forked ? (g.name === "rich" ? "↳ " : "-> ") : ""}${snippet(entry.title, 70)}`, description: `${relativeTime(Date.parse(entry.at))}${forkLabel(entry)}` })),
3200
+ allowOther: false,
3201
+ escapeLabel: "cancel",
3202
+ tone: "neutral",
3203
+ }, this.outer.signal).catch(() => undefined);
3204
+ if (answer?.kind !== "selected")
3205
+ return;
3206
+ chosen = list[answer.indices[0] ?? -1]?.sessionId;
3207
+ if (chosen === undefined)
3208
+ return;
3209
+ }
3210
+ if (this.turnRunning || this.orchestration !== undefined) {
3211
+ this.print(["Synorch is working; /cancel first."]);
3212
+ return;
3213
+ }
3214
+ if (chosen === undefined) {
3215
+ const index = Number(argument);
3216
+ chosen = Number.isInteger(index) && index >= 1 ? list[index - 1]?.sessionId : list.find((entry) => entry.sessionId === argument)?.sessionId;
3217
+ }
3218
+ if (chosen === undefined) {
3219
+ this.print([`No conversation "${argument}"; /resume lists them.`]);
3220
+ return;
3221
+ }
3222
+ await this.leaveConversation();
3223
+ await this.showResumed(await this.openSession(chosen));
3224
+ }
3225
+ /** Closes the current conversation's log (it stays resumable) and resets per-conversation state. */
3226
+ async leaveConversation() {
3227
+ await this.log?.close().catch(() => undefined);
3228
+ this.log = undefined;
3229
+ this.driver = undefined;
3230
+ this.turnId = undefined;
3231
+ this.orchestratedSessions.length = 0;
3232
+ this.pendingNotes.length = 0;
3233
+ this.requests.clear();
3234
+ this.lastTracker = undefined;
3235
+ }
3236
+ // ---- K3 time travel: /fork and /rewind ------------------------------------------------------------
3237
+ /**
3238
+ * Continues in a new conversation whose history is this one's events up to `upToSeq` (the store
3239
+ * reads the parent prefix through the fork pointer; nothing is copied). The original is closed and
3240
+ * stays resumable. `upToSeq` 0 (before the first event) starts an empty conversation instead.
3241
+ */
3242
+ async forkAt(upToSeq, title) {
3243
+ const source = this.sessionId;
3244
+ if (source === undefined || this.log === undefined)
3245
+ return undefined;
3246
+ if (upToSeq < 1) {
3247
+ await this.leaveConversation();
3248
+ this.sessionId = undefined;
3249
+ this.routeRecorded = false;
3250
+ return [];
3251
+ }
3252
+ const forked = await this.runtime.sessions.fork(source, upToSeq, title === undefined ? {} : { title });
3253
+ const forkedId = forked.sessionId;
3254
+ await forked.close();
3255
+ await this.leaveConversation();
3256
+ return this.openSession(forkedId);
3257
+ }
3258
+ /** The last few exchanges of a conversation, as the resume card shows them. */
3259
+ replayTail(events) {
3260
+ const users = events.flatMap((event, index) => (event.type === "message/recorded" && event.data.role === "user" ? [index] : []));
3261
+ const firstShown = users.length > REPLAY_EXCHANGES ? (users[users.length - REPLAY_EXCHANGES] ?? 0) : 0;
3262
+ if (users.length > 0)
3263
+ this.renderer.replay?.(events.slice(firstShown));
3264
+ }
3265
+ /** `/fork [name]`: branch the conversation here; the original stays resumable. */
3266
+ async fork(argument) {
3267
+ if (this.turnRunning || this.orchestration !== undefined) {
3268
+ this.print(["Synorch is working; /cancel first."]);
3269
+ return;
3270
+ }
3271
+ const log = this.log;
3272
+ if (log === undefined || this.sessionId === undefined) {
3273
+ this.print(["Nothing to fork yet: send a message first."]);
3274
+ return;
3275
+ }
3276
+ const name = argument.replace(/\s+/g, " ").trim();
3277
+ const previous = this.sessionId;
3278
+ await this.forkAt(log.lastSeq, name === "" ? undefined : `${CONVERSATION_TITLE}${name.slice(0, 120)}`);
3279
+ this.print([`${this.glyphs.ok} Forked${name === "" ? "" : ` "${snippet(name, 60)}"`} ${this.glyphs.sep} you are in the new branch ${this.glyphs.sep} the original (${previous}) stays saved: /resume`]);
3280
+ }
3281
+ restoreIO() {
3282
+ return {
3283
+ resolve: (relative) => this.inside(relative)?.absolute,
3284
+ read: async (absolute) => {
3285
+ const bytes = await readOptional(absolute);
3286
+ return bytes === undefined ? undefined : new Uint8Array(bytes);
3287
+ },
3288
+ write: async (absolute, bytes) => {
3289
+ await mkdir(path.dirname(absolute), { recursive: true });
3290
+ await writeFile(absolute, bytes);
3291
+ },
3292
+ remove: (absolute) => unlink(absolute),
3293
+ blob: (ref) => this.runtime.blobs.get(ref.digest),
3294
+ };
3295
+ }
3296
+ /**
3297
+ * `/rewind` (Esc Esc on an empty editor): pick an earlier message; the conversation forks from just
3298
+ * before it and the message goes back into the editor. Optionally the files Synorch changed after
3299
+ * that point are restored from its `/undo` checkpoints (files the user changed since are kept).
3300
+ */
3301
+ async rewind() {
3302
+ const g = this.glyphs;
3303
+ if (this.turnRunning || this.orchestration !== undefined) {
3304
+ this.print(["Synorch is working; /cancel first."]);
3305
+ return;
3306
+ }
3307
+ const events = await this.readEvents();
3308
+ const points = rewindPoints(events).reverse().slice(0, 50);
3309
+ if (points.length === 0) {
3310
+ this.print(["Nothing to rewind yet: send a message first."]);
3311
+ return;
3312
+ }
3313
+ const picked = await this.choose({
3314
+ question: "Rewind to before which message?",
3315
+ subtitle: "the conversation forks from just before it · the original stays saved (/resume) · Esc cancels",
3316
+ options: points.map((point) => ({ label: snippet(point.text === "" ? "(attachment only)" : point.text, 72), description: relativeTime(Date.parse(point.at)) })),
3317
+ allowOther: false,
3318
+ escapeLabel: "cancel",
3319
+ tone: "neutral",
3320
+ }, this.outer.signal).catch(() => undefined);
3321
+ const point = picked?.kind === "selected" ? points[picked.indices[0] ?? -1] : undefined;
3322
+ if (point === undefined)
3323
+ return;
3324
+ const plan = planRestore(events, point.forkSeq);
3325
+ const io = this.restoreIO();
3326
+ const conflicts = plan.files.length === 0 ? [] : await restoreConflicts(plan, io);
3327
+ const restorable = plan.files.length - conflicts.length;
3328
+ const count = (n) => `${n} file${n === 1 ? "" : "s"}`;
3329
+ const options = [
3330
+ { label: "Fork conversation from here", description: "new conversation up to just before this message; your message returns to the editor; files stay as they are" },
3331
+ ...(plan.files.length === 0
3332
+ ? []
3333
+ : [
3334
+ {
3335
+ label: "Fork and restore files to that point",
3336
+ description: `${count(restorable)} Synorch changed after it go back${conflicts.length === 0 ? "" : ` ${g.sep} ${count(conflicts.length)} you changed since are kept: ${conflicts.slice(0, 3).join(", ")}${conflicts.length > 3 ? ", …" : ""}`} ${g.sep} command side effects are not undone`,
3337
+ ...(restorable === 0 ? { disabled: "every file Synorch changed was changed again since" } : {}),
3338
+ },
3339
+ ]),
3340
+ ];
3341
+ const action = await this.choose({ question: `Rewind to "${snippet(point.text, 50)}"`, options, allowOther: false, escapeLabel: "cancel", tone: "neutral" }, this.outer.signal).catch(() => undefined);
3342
+ if (action?.kind !== "selected")
3343
+ return;
3344
+ const restore = action.indices[0] === 1;
3345
+ let restoredFiles = [];
3346
+ if (restore) {
3347
+ const result = await applyRestore(plan, io);
3348
+ restoredFiles = result.restored;
3349
+ // The original conversation records what was undone, so its /diff and /undo stay truthful.
3350
+ for (const checkpoint of plan.checkpoints) {
3351
+ const mine = (file) => checkpoint.data.files.some((entry) => entry.path === file);
3352
+ await this.append("checkpoint/restored", { checkpoint_seq: checkpoint.seq, restored: result.restored.filter(mine), skipped: result.skipped.filter((entry) => mine(entry.path)) }, "user").catch(() => undefined);
3353
+ }
3354
+ for (const entry of result.skipped)
3355
+ this.note("warning", `${g.warn} Kept ${entry.path}: ${entry.reason}`);
3356
+ }
3357
+ const previous = this.sessionId;
3358
+ const forked = await this.forkAt(point.forkSeq, undefined);
3359
+ if (forked === undefined)
3360
+ return;
3361
+ this.replayTail(forked);
3362
+ const kept = plan.files.map((file) => file.path).filter((file) => !restoredFiles.includes(file));
3363
+ if (kept.length > 0)
3364
+ this.pendingNotes.push(`the user rewound the conversation to this point; files you changed after it were kept as they are now: ${kept.join(", ")} (re-read before editing).`);
3365
+ this.print([
3366
+ `${g.resume} Rewound ${g.sep} new branch from before "${snippet(point.text, 50)}"${restoredFiles.length === 0 ? "" : ` ${g.sep} restored ${restoredFiles.join(", ")}`} ${g.sep} the original${previous === undefined ? "" : ` (${previous})`} stays saved: /resume`,
3367
+ ]);
3368
+ const controls = this.renderer.controls;
3369
+ if (point.text !== "") {
3370
+ if (controls?.setEditorText !== undefined)
3371
+ controls.setEditorText(point.text);
3372
+ else
3373
+ this.print(["Your message, to edit and send again:", ...point.text.split(/\r?\n/).map((line) => ` ${line}`)]);
3374
+ }
3375
+ }
3376
+ /** `/mouse [on|off]` when it reaches the session (the interactive renderer normally handles it itself). */
3377
+ async mouse(argument) {
3378
+ const controls = this.renderer.controls;
3379
+ if (controls === undefined) {
3380
+ this.print(["Mouse mode needs the interactive terminal view."]);
3381
+ return;
3382
+ }
3383
+ const wanted = /^on$/i.test(argument) ? true : /^off$/i.test(argument) ? false : !controls.mouseMode;
3384
+ controls.setMouseMode(wanted);
3385
+ this.print([`Mouse mode ${wanted ? "on: wheel scrolls, click expands; /select for native text selection" : "off: the terminal selects text"}`]);
3386
+ }
3387
+ /**
3388
+ * `/config` (K1.5-3): the settings screen built on the picker (Enter edits, booleans toggle, enums
3389
+ * and routes offer choices, Esc closes); `/config <key>` shows one value, `/config <key> <value>`
3390
+ * sets it. Only the user configuration is written; the permission mode and mouse apply at once.
3391
+ */
3392
+ async config(argument) {
3393
+ const [key, ...rest] = argument.split(/\s+/).filter((part) => part !== "");
3394
+ const listing = async () => listSettings(this.runtime.home, this.runtime.workspaceRoot).catch((error) => {
3395
+ this.print([`${this.glyphs.warn} ${failureInfo(error).message}`, "Fix it with syn config edit"]);
3396
+ return undefined;
3397
+ });
3398
+ if (key !== undefined) {
3399
+ if (rest.length > 0) {
3400
+ await this.applySetting(key, rest.join(" "));
3401
+ return;
3402
+ }
3403
+ try {
3404
+ const definition = settingFor(key);
3405
+ const row = (await listing())?.rows.find((candidate) => candidate.key === definition.key);
3406
+ this.print([`${definition.key} = ${row?.value ?? "(not set)"} (${row?.source ?? "default"}) ${this.glyphs.sep} ${definition.description}`]);
3407
+ }
3408
+ catch (error) {
3409
+ this.print([failureInfo(error).message]);
3410
+ }
3411
+ return;
3412
+ }
3413
+ const controls = this.renderer.controls;
3414
+ if (controls === undefined) {
3415
+ const current = await listing();
3416
+ if (current !== undefined)
3417
+ this.print([...formatListing(current).slice(0, -1), "Change: /config <key> <value> (or syn config set)"]);
3418
+ return;
3419
+ }
3420
+ const panelShown = await this.showPanel(() => configPanel({
3421
+ sep: this.glyphs.sep,
3422
+ rows: listing,
3423
+ edit: async (row) => {
3424
+ const value = await this.pickSettingValue(row, controls);
3425
+ return value === undefined ? undefined : this.applySetting(row.key, value);
3426
+ },
3427
+ }));
3428
+ if (panelShown)
3429
+ return;
3430
+ for (;;) {
3431
+ const current = await listing();
3432
+ if (current === undefined)
3433
+ return;
3434
+ const entries = current.rows.map((row, index) => ({
3435
+ id: String(index),
3436
+ tier: row.key,
3437
+ provider: "",
3438
+ model: "",
3439
+ label: row.value ?? "(not set)",
3440
+ auth: row.source === "workspace" || row.source === "project" ? `${row.source} (narrowed)` : row.source,
3441
+ current: false,
3442
+ description: row.description,
3443
+ }));
3444
+ const chosen = await controls
3445
+ .openModelPicker(entries, this.outer.signal, { title: "Settings", hint: `Enter changes · Esc closes · saved to ${current.userFile}` })
3446
+ .catch(() => undefined);
3447
+ const row = chosen === undefined ? undefined : current.rows[Number(chosen.id)];
3448
+ if (row === undefined)
3449
+ return;
3450
+ const value = await this.pickSettingValue(row, controls);
3451
+ if (value !== undefined)
3452
+ await this.applySetting(row.key, value);
3453
+ }
3454
+ }
3455
+ async pickSettingValue(row, controls) {
3456
+ if (row.kind === "boolean")
3457
+ return row.value === "true" ? "false" : "true";
3458
+ const unset = "(unset: use the default)";
3459
+ let choices = [];
3460
+ const badges = new Map();
3461
+ if (row.kind === "enum")
3462
+ choices = [...(row.choices ?? [])];
3463
+ if (row.kind === "route") {
3464
+ // Connected models from the catalog (K1.5-A), then the configured routes; the adapter is spelled out when it is not the provider default.
3465
+ const catalog = await this.runtime.modelCatalog(this.outer.signal).catch(() => []);
3466
+ for (const model of catalog.filter((candidate) => candidate.connected)) {
3467
+ const value = `${model.provider}/${model.model}${model.adapterId === DEFAULT_ADAPTER_FOR_PROVIDER[model.provider] ? "" : `@${model.adapterId}`}`;
3468
+ badges.set(value, model.badge);
3469
+ }
3470
+ for (const rule of this.runtime.config.router.rules) {
3471
+ const value = `${rule.route.provider_id}/${rule.route.model_id}${rule.route.adapter_id === DEFAULT_ADAPTER_FOR_PROVIDER[rule.route.provider_id] ? "" : `@${rule.route.adapter_id}`}`;
3472
+ if (!badges.has(value))
3473
+ badges.set(value, "configured");
3474
+ }
3475
+ choices = [...badges.keys()];
3476
+ }
3477
+ if (row.key === "ui.theme")
3478
+ choices = (controls.appearance?.themes ?? BUILTIN_THEMES).map((theme) => theme.name);
3479
+ const typeIt = "Type a value…";
3480
+ if (row.kind === "route" || row.kind === "int" || row.kind === "number" || row.kind === "string" || row.kind === "list")
3481
+ choices.push(typeIt);
3482
+ choices.push(unset);
3483
+ const entries = choices.map((choice, index) => ({ id: String(index), tier: "", provider: "", model: "", label: choice, auth: badges.get(choice) ?? "", current: choice === row.value }));
3484
+ const picked = choices.length === 2 && choices[0] === typeIt ? { id: "0" } : await controls.openModelPicker(entries, this.outer.signal, { title: row.key, hint: `${row.description} — Enter selects, Esc cancels` }).catch(() => undefined);
3485
+ const choice = picked === undefined ? undefined : choices[Number(picked.id)];
3486
+ if (choice === undefined)
3487
+ return undefined;
3488
+ if (choice === unset)
3489
+ return UNSET_SETTING;
3490
+ if (choice !== typeIt)
3491
+ return choice;
3492
+ const hint = row.kind === "route" ? "provider/model[@adapter], e.g. openai/gpt-6-sol" : row.kind === "string" ? "text" : row.kind === "list" ? `comma-separated: ${(row.choices ?? []).join(",")}` : "a positive number";
3493
+ const answer = await this.askUser(`New value for ${row.key} (${hint}; empty cancels)`, undefined, this.outer.signal).catch(() => "");
3494
+ return answer.trim() === "" ? undefined : answer.trim();
3495
+ }
3496
+ async applySetting(key, value) {
3497
+ const g = this.glyphs;
3498
+ try {
3499
+ const change = value === UNSET_SETTING ? await unsetUserSetting(this.runtime.home, key) : await setUserSetting(this.runtime.home, key, value);
3500
+ const shown = change.value ?? "default";
3501
+ let effect = "new conversations use it";
3502
+ if (change.key === "ui.permission_mode") {
3503
+ this.setMode((change.value ?? "auto"));
3504
+ effect = "applied now";
3505
+ }
3506
+ else if (change.key === "ui.mouse" && this.renderer.controls !== undefined) {
3507
+ this.renderer.controls.setMouseMode(change.value === "true");
3508
+ effect = "applied now";
3509
+ }
3510
+ else if (change.key === "ui.theme") {
3511
+ // K8: a theme applies at once in the interactive view (a file created after start: next conversation).
3512
+ effect = this.renderer.controls?.appearance?.setTheme(change.value ?? defaultThemeName(this.io.env)) === true ? "applied now" : "new conversations use it";
3513
+ }
3514
+ else if (change.key.startsWith("ui.welcome.")) {
3515
+ const appearance = this.renderer.controls?.appearance;
3516
+ if (appearance !== undefined) {
3517
+ appearance.setWelcome(welcomeWith(appearance.welcome, change.key, change.value));
3518
+ effect = "applied now";
3519
+ }
3520
+ }
3521
+ else if (change.key === "ui.glyphs")
3522
+ effect = "applies from the next session";
3523
+ else if (change.key.startsWith("routes."))
3524
+ effect = "new conversations use it; /model switches this one";
3525
+ else if (change.key.startsWith("effort.")) {
3526
+ // K6: set or unset, a tier or role level applies to this session at once: the live configured level
3527
+ // changes, and a session override of that tier (an earlier /effort) gives way to it.
3528
+ const slot = EFFORT_SLOTS.find((candidate) => `effort.${candidate}` === change.key);
3529
+ if (slot !== undefined) {
3530
+ this.runtime.setConfiguredEffort(slot, change.value === undefined || !isReasoningEffort(change.value) ? undefined : change.value);
3531
+ const tier = MODEL_TIERS.find((candidate) => candidate === slot);
3532
+ if (tier !== undefined)
3533
+ this.runtime.setSessionEffort(tier, undefined);
3534
+ this.refreshStatus();
3535
+ effect = "applied from the next request";
3536
+ }
3537
+ }
3538
+ const line = `${g.ok} ${change.key} = ${shown}${change.previous !== undefined && change.previous !== change.value ? ` (was ${change.previous})` : ""} ${g.sep} ${effect}`;
3539
+ this.print([line]);
3540
+ return line;
3541
+ }
3542
+ catch (error) {
3543
+ const line = `${g.warn} ${error instanceof Error ? error.message : String(error)}`;
3544
+ this.print([line]);
3545
+ return line;
3546
+ }
3547
+ }
3548
+ // ---- K8 appearance -------------------------------------------------------------------------
3549
+ /** The welcome's hint: first run, a conversation from the last hours to resume, else a rotating tip. */
3550
+ async welcomeHint(resumed) {
3551
+ try {
3552
+ const list = resumed ? [] : await this.conversations();
3553
+ const recent = list[0];
3554
+ const recentAt = recent === undefined ? Number.NaN : Date.parse(recent.at);
3555
+ let skills;
3556
+ try {
3557
+ skills = this.runtime.extensions.invocable(reservedCommandNames()).length;
3558
+ }
3559
+ catch {
3560
+ skills = undefined;
3561
+ }
3562
+ return pickHint({
3563
+ firstRun: !resumed && list.length === 0,
3564
+ resumable: Number.isFinite(recentAt) && Date.now() - recentAt < RESUME_HINT_MS ? relativeTime(recentAt) : undefined,
3565
+ skills,
3566
+ seed: Math.floor(Date.now() / 1000),
3567
+ sep: this.glyphs.sep,
3568
+ });
3569
+ }
3570
+ catch {
3571
+ return undefined;
3572
+ }
3573
+ }
3574
+ /** The first-run setup shows once, in an interactive terminal, unless CI or SYN_NO_SETUP says otherwise. */
3575
+ shouldOnboard() {
3576
+ const on = (name) => {
3577
+ const value = this.io.env[name];
3578
+ return value !== undefined && value !== "" && value !== "0" && value.toLowerCase() !== "false";
3579
+ };
3580
+ return this.renderer.controls?.appearance !== undefined && this.io.stdinIsTTY && this.runtime.config.onboarded !== true && !on("CI") && !on("SYN_NO_SETUP");
3581
+ }
3582
+ async saveQuietly(key, value) {
3583
+ try {
3584
+ await setUserSetting(this.runtime.home, key, value);
3585
+ }
3586
+ catch (error) {
3587
+ this.note("warning", `${this.glyphs.warn} ${key} not saved: ${error instanceof Error ? error.message : String(error)}`);
3588
+ }
3589
+ }
3590
+ /** `/setup` (and the first run): theme, welcome style and symbols, each with a live preview; Esc skips the rest. */
3591
+ async setup(first = false) {
3592
+ const g = this.glyphs;
3593
+ const appearance = this.renderer.controls?.appearance;
3594
+ if (appearance === undefined) {
3595
+ this.print([`Setup runs in the interactive terminal view ${g.sep} here: /config ui.theme <name>, ui.welcome.style, ui.glyphs (or syn config set)`]);
3596
+ return;
3597
+ }
3598
+ if (first)
3599
+ this.note("info", `${g.bullet} Welcome to Synorch ${g.sep} three quick choices ${g.sep} Esc skips (/setup runs it again)`);
3600
+ const signal = this.outer.signal;
3601
+ const done = [];
3602
+ const finish = async (skipped) => {
3603
+ if (first || !skipped)
3604
+ await this.saveQuietly("ui.onboarded", "true");
3605
+ if (done.length === 0)
3606
+ this.print([first ? `Setup skipped ${g.sep} /setup runs it any time` : "Setup cancelled"]);
3607
+ else
3608
+ this.print([`${g.ok} Saved: ${done.join(", ")}${skipped ? " (rest skipped)" : ""} ${g.sep} /setup to change`]);
3609
+ };
3610
+ const theme = await appearance.pickTheme(signal, "1/3");
3611
+ if (theme === undefined)
3612
+ return finish(true);
3613
+ await this.saveQuietly("ui.theme", theme);
3614
+ done.push(`${theme} theme`);
3615
+ const style = await appearance.pickWelcomeStyle(signal, "2/3");
3616
+ if (style === undefined)
3617
+ return finish(true);
3618
+ await this.saveQuietly("ui.welcome.style", style);
3619
+ done.push(`${style} welcome`);
3620
+ const glyphs = await appearance.pickGlyphs(signal, "3/3");
3621
+ if (glyphs === undefined)
3622
+ return finish(true);
3623
+ await this.saveQuietly("ui.glyphs", glyphs);
3624
+ done.push(`${glyphs} symbols${glyphs === this.glyphs.name ? "" : " (next session)"}`);
3625
+ return finish(false);
3626
+ }
3627
+ /** `/theme [name]`: the picker with a live preview, or switch directly; saved to ui.theme. */
3628
+ async theme(argument) {
3629
+ const name = argument.trim().toLowerCase();
3630
+ if (name !== "") {
3631
+ await this.applySetting("ui.theme", name);
3632
+ return;
3633
+ }
3634
+ const appearance = this.renderer.controls?.appearance;
3635
+ if (appearance === undefined) {
3636
+ this.print([`Themes: ${BUILTIN_THEMES.map((theme) => theme.name).join(", ")} ${this.glyphs.sep} /theme <name> saves one (the interactive view previews them)`]);
3637
+ return;
3638
+ }
3639
+ const picked = await appearance.pickTheme(this.outer.signal);
3640
+ if (picked !== undefined)
3641
+ await this.applySetting("ui.theme", picked);
3642
+ }
3643
+ /** `/welcome [full|compact|minimal|off]`: the header's style directly, or the whole customization with a live preview. */
3644
+ async welcome(argument) {
3645
+ const g = this.glyphs;
3646
+ const word = argument.trim().toLowerCase();
3647
+ if (word !== "") {
3648
+ if (["full", "compact", "minimal", "off"].some((style) => style === word))
3649
+ await this.applySetting("ui.welcome.style", word);
3650
+ else
3651
+ this.print([`/welcome [full | compact | minimal | off] ${g.sep} alone: customize it with a live preview`]);
3652
+ return;
3653
+ }
3654
+ const appearance = this.renderer.controls?.appearance;
3655
+ if (appearance === undefined) {
3656
+ this.print([`The welcome header belongs to the interactive view ${g.sep} /config ui.welcome.style | ui.welcome.logo | ui.welcome.fields | ui.welcome.tips`]);
3657
+ return;
3658
+ }
3659
+ const before = appearance.welcome;
3660
+ const chosen = await appearance.pickWelcome(this.outer.signal);
3661
+ if (chosen === undefined)
3662
+ return;
3663
+ if (chosen.style !== before.style)
3664
+ await this.saveQuietly("ui.welcome.style", chosen.style);
3665
+ if (chosen.logo !== before.logo)
3666
+ await this.saveQuietly("ui.welcome.logo", chosen.logo);
3667
+ if (chosen.fields.join(",") !== before.fields.join(","))
3668
+ await this.saveQuietly("ui.welcome.fields", chosen.fields.length === 0 ? "none" : chosen.fields.join(","));
3669
+ if (chosen.tips !== before.tips)
3670
+ await this.saveQuietly("ui.welcome.tips", String(chosen.tips));
3671
+ this.print([`${g.ok} Welcome: ${chosen.style} ${g.sep} logo ${chosen.logo} ${g.sep} ${chosen.fields.length} fact${chosen.fields.length === 1 ? "" : "s"} ${g.sep} tips ${chosen.tips ? "on" : "off"} ${g.sep} saved`]);
3672
+ }
3673
+ /** `/graph`: the plan graph of the running or last worker run of this conversation. */
3674
+ async graph() {
3675
+ const tracker = this.orchestration?.tracker ?? this.lastTracker;
3676
+ if (tracker === undefined || tracker.view().tasks.length === 0) {
3677
+ this.print(["No worker run in this conversation yet: the graph shows a run's tasks and their dependencies."]);
3678
+ return;
3679
+ }
3680
+ const views = this.renderer.views;
3681
+ if (views !== undefined)
3682
+ views.showGraph(tracker.view());
3683
+ else
3684
+ this.print(tracker.view().tasks.map((task) => `${task.key} (${task.role}) ${task.state.replaceAll("_", " ")}${task.dependsOn === undefined || task.dependsOn.length === 0 ? "" : ` after ${task.dependsOn.join(", ")}`}`));
3685
+ }
3686
+ }
3687
+ const PERMISSION_MODE_WORDS = ["ask", "auto", "full", "plan"];
3688
+ /** K8: the welcome hint offers /resume for a conversation newer than this. */
3689
+ const RESUME_HINT_MS = 12 * 60 * 60 * 1000;
3690
+ /** `ui.welcome.<key>` set (or unset: the default) applied to the live welcome preferences. */
3691
+ function welcomeWith(current, key, value) {
3692
+ const field = key.slice("ui.welcome.".length);
3693
+ if (field === "style")
3694
+ return { ...current, style: ["full", "compact", "minimal", "off"].find((style) => style === value) ?? "full" };
3695
+ if (field === "logo")
3696
+ return { ...current, logo: ["on", "art", "glyph", "off", "custom"].find((logo) => logo === value) ?? "on" };
3697
+ if (field === "tips")
3698
+ return { ...current, tips: value === undefined ? true : value === "true" };
3699
+ if (field === "fields") {
3700
+ const all = ["version", "model", "plan", "workers", "folder", "mode"];
3701
+ const wanted = value?.split(",").map((part) => part.trim());
3702
+ return { ...current, fields: wanted === undefined ? [...all] : all.filter((name) => wanted.includes(name)) };
3703
+ }
3704
+ return current;
3705
+ }
3706
+ /** `/config` picker choice that removes the key from the user configuration. */
3707
+ const UNSET_SETTING = "(unset)";
3708
+ const MODE_MEANINGS = {
3709
+ ask: "asks before every edit and command",
3710
+ auto: "edits and commands in this workspace run, workers included; pushes, publishing and destructive commands ask you first",
3711
+ full: "no prompts except destructive commands (force push, publish, recursive delete…); hard rails stay",
3712
+ plan: "read-only: reads and plans, no edits or commands",
3713
+ };
3714
+ /**
3715
+ * The last todo list the agent wrote with a todo tool (any tool whose name contains `todo`, with a
3716
+ * `todos`/`items` array of `{ content | text | title, status }`), for the resume card (UX-06).
3717
+ */
3718
+ function lastTodo(events) {
3719
+ for (let index = events.length - 1; index >= 0; index -= 1) {
3720
+ const event = events[index];
3721
+ if (event?.type !== "message/recorded" || event.data.role !== "assistant" || event.data.message === undefined)
3722
+ continue;
3723
+ for (const part of [...event.data.message.content].reverse()) {
3724
+ if (part.type !== "tool_call" || !/todo/i.test(part.name))
3725
+ continue;
3726
+ const list = Array.isArray(part.arguments.todos) ? part.arguments.todos : Array.isArray(part.arguments.items) ? part.arguments.items : undefined;
3727
+ if (list === undefined)
3728
+ continue;
3729
+ const items = list.flatMap((item) => {
3730
+ if (typeof item !== "object" || item === null)
3731
+ return [];
3732
+ const record = item;
3733
+ const text = [record.content, record.text, record.title].find((value) => typeof value === "string");
3734
+ return text === undefined ? [] : [{ text, done: /^(done|completed|complete)$/i.test(String(record.status ?? "")) }];
3735
+ });
3736
+ if (items.length === 0)
3737
+ continue;
3738
+ return { done: items.filter((item) => item.done).length, total: items.length, next: items.find((item) => !item.done)?.text };
3739
+ }
3740
+ }
3741
+ return undefined;
3742
+ }
3743
+ function relativeTime(then) {
3744
+ if (!Number.isFinite(then))
3745
+ return "earlier";
3746
+ const seconds = Math.max(0, Math.round((Date.now() - then) / 1000));
3747
+ if (seconds < 60)
3748
+ return "just now";
3749
+ if (seconds < 3600)
3750
+ return `${Math.round(seconds / 60)}m ago`;
3751
+ if (seconds < 86_400)
3752
+ return `${Math.round(seconds / 3600)}h ago`;
3753
+ return `${Math.round(seconds / 86_400)}d ago`;
3754
+ }
3755
+ //# sourceMappingURL=conversation.js.map