@namzu/cli 29.0.0 → 29.1.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 (390) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/README.md +20 -0
  3. package/dist/browser/commands.d.ts +29 -0
  4. package/dist/browser/commands.d.ts.map +1 -0
  5. package/dist/browser/commands.js +361 -0
  6. package/dist/browser/commands.js.map +1 -0
  7. package/dist/browser/control.d.ts +79 -0
  8. package/dist/browser/control.d.ts.map +1 -0
  9. package/dist/browser/control.js +140 -0
  10. package/dist/browser/control.js.map +1 -0
  11. package/dist/browser/describe.d.ts +12 -0
  12. package/dist/browser/describe.d.ts.map +1 -0
  13. package/dist/browser/describe.js +27 -0
  14. package/dist/browser/describe.js.map +1 -0
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +29 -2
  17. package/dist/cli.js.map +1 -1
  18. package/dist/commands/acp.d.ts.map +1 -1
  19. package/dist/commands/acp.js +1 -0
  20. package/dist/commands/acp.js.map +1 -1
  21. package/dist/commands/browser.d.ts +7 -0
  22. package/dist/commands/browser.d.ts.map +1 -0
  23. package/dist/commands/browser.js +16 -0
  24. package/dist/commands/browser.js.map +1 -0
  25. package/dist/commands/drain.d.ts +2 -1
  26. package/dist/commands/drain.d.ts.map +1 -1
  27. package/dist/commands/drain.js +6 -3
  28. package/dist/commands/drain.js.map +1 -1
  29. package/dist/commands/exec-flags.d.ts +2 -1
  30. package/dist/commands/exec-flags.d.ts.map +1 -1
  31. package/dist/commands/exec-flags.js +6 -3
  32. package/dist/commands/exec-flags.js.map +1 -1
  33. package/dist/commands/exec-json.d.ts.map +1 -1
  34. package/dist/commands/exec-json.js +2 -1
  35. package/dist/commands/exec-json.js.map +1 -1
  36. package/dist/commands/exec.d.ts.map +1 -1
  37. package/dist/commands/exec.js +2 -1
  38. package/dist/commands/exec.js.map +1 -1
  39. package/dist/commands/host-queries.d.ts.map +1 -1
  40. package/dist/commands/host-queries.js +10 -3
  41. package/dist/commands/host-queries.js.map +1 -1
  42. package/dist/commands/schedule.d.ts +9 -0
  43. package/dist/commands/schedule.d.ts.map +1 -0
  44. package/dist/commands/schedule.js +126 -0
  45. package/dist/commands/schedule.js.map +1 -0
  46. package/dist/commands/serve.d.ts +3 -1
  47. package/dist/commands/serve.d.ts.map +1 -1
  48. package/dist/commands/serve.js +5 -3
  49. package/dist/commands/serve.js.map +1 -1
  50. package/dist/commands/skills.d.ts +10 -0
  51. package/dist/commands/skills.d.ts.map +1 -1
  52. package/dist/commands/skills.js +23 -7
  53. package/dist/commands/skills.js.map +1 -1
  54. package/dist/commands/upgrade.d.ts +5 -0
  55. package/dist/commands/upgrade.d.ts.map +1 -1
  56. package/dist/commands/upgrade.js +11 -0
  57. package/dist/commands/upgrade.js.map +1 -1
  58. package/dist/config/load.d.ts +39 -0
  59. package/dist/config/load.d.ts.map +1 -1
  60. package/dist/config/load.js +232 -1
  61. package/dist/config/load.js.map +1 -1
  62. package/dist/config/schema.d.ts +78 -0
  63. package/dist/config/schema.d.ts.map +1 -1
  64. package/dist/config/schema.js.map +1 -1
  65. package/dist/config/user-config.d.ts +23 -0
  66. package/dist/config/user-config.d.ts.map +1 -0
  67. package/dist/config/user-config.js +48 -0
  68. package/dist/config/user-config.js.map +1 -0
  69. package/dist/context/capabilities.d.ts +1 -1
  70. package/dist/context/capabilities.d.ts.map +1 -1
  71. package/dist/context/capabilities.js +1 -0
  72. package/dist/context/capabilities.js.map +1 -1
  73. package/dist/doctor/checks/browser.d.ts +15 -0
  74. package/dist/doctor/checks/browser.d.ts.map +1 -0
  75. package/dist/doctor/checks/browser.js +63 -0
  76. package/dist/doctor/checks/browser.js.map +1 -0
  77. package/dist/doctor/checks/index.d.ts +4 -1
  78. package/dist/doctor/checks/index.d.ts.map +1 -1
  79. package/dist/doctor/checks/index.js +8 -1
  80. package/dist/doctor/checks/index.js.map +1 -1
  81. package/dist/doctor/checks/scheduler.d.ts +10 -0
  82. package/dist/doctor/checks/scheduler.d.ts.map +1 -0
  83. package/dist/doctor/checks/scheduler.js +74 -0
  84. package/dist/doctor/checks/scheduler.js.map +1 -0
  85. package/dist/index.d.ts +1 -1
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +1 -1
  88. package/dist/index.js.map +1 -1
  89. package/dist/integrations/notifications/desktop/freedesktop.d.ts +10 -0
  90. package/dist/integrations/notifications/desktop/freedesktop.d.ts.map +1 -0
  91. package/dist/integrations/notifications/desktop/freedesktop.js +33 -0
  92. package/dist/integrations/notifications/desktop/freedesktop.js.map +1 -0
  93. package/dist/integrations/notifications/desktop/macos.d.ts +6 -0
  94. package/dist/integrations/notifications/desktop/macos.d.ts.map +1 -0
  95. package/dist/integrations/notifications/desktop/macos.js +17 -0
  96. package/dist/integrations/notifications/desktop/macos.js.map +1 -0
  97. package/dist/integrations/notifications/desktop/sanitize.d.ts +16 -0
  98. package/dist/integrations/notifications/desktop/sanitize.d.ts.map +1 -0
  99. package/dist/integrations/notifications/desktop/sanitize.js +49 -0
  100. package/dist/integrations/notifications/desktop/sanitize.js.map +1 -0
  101. package/dist/integrations/notifications/desktop/throttle.d.ts +10 -0
  102. package/dist/integrations/notifications/desktop/throttle.d.ts.map +1 -0
  103. package/dist/integrations/notifications/desktop/throttle.js +39 -0
  104. package/dist/integrations/notifications/desktop/throttle.js.map +1 -0
  105. package/dist/integrations/notifications/desktop/windows-toast.d.ts +23 -0
  106. package/dist/integrations/notifications/desktop/windows-toast.d.ts.map +1 -0
  107. package/dist/integrations/notifications/desktop/windows-toast.js +56 -0
  108. package/dist/integrations/notifications/desktop/windows-toast.js.map +1 -0
  109. package/dist/integrations/notifications/desktop.d.ts +82 -0
  110. package/dist/integrations/notifications/desktop.d.ts.map +1 -0
  111. package/dist/integrations/notifications/desktop.js +198 -0
  112. package/dist/integrations/notifications/desktop.js.map +1 -0
  113. package/dist/integrations/resident/session-step.d.ts.map +1 -1
  114. package/dist/integrations/resident/session-step.js +1 -0
  115. package/dist/integrations/resident/session-step.js.map +1 -1
  116. package/dist/integrations/subagents/runtime.d.ts +6 -0
  117. package/dist/integrations/subagents/runtime.d.ts.map +1 -1
  118. package/dist/integrations/subagents/runtime.js +14 -5
  119. package/dist/integrations/subagents/runtime.js.map +1 -1
  120. package/dist/permissions/browser-sites.d.ts +95 -0
  121. package/dist/permissions/browser-sites.d.ts.map +1 -0
  122. package/dist/permissions/browser-sites.js +270 -0
  123. package/dist/permissions/browser-sites.js.map +1 -0
  124. package/dist/permissions/checks.d.ts.map +1 -1
  125. package/dist/permissions/checks.js +7 -1
  126. package/dist/permissions/checks.js.map +1 -1
  127. package/dist/permissions/rules.d.ts.map +1 -1
  128. package/dist/permissions/rules.js +4 -8
  129. package/dist/permissions/rules.js.map +1 -1
  130. package/dist/schedule/build.d.ts +79 -0
  131. package/dist/schedule/build.d.ts.map +1 -0
  132. package/dist/schedule/build.js +246 -0
  133. package/dist/schedule/build.js.map +1 -0
  134. package/dist/schedule/changes.d.ts +22 -0
  135. package/dist/schedule/changes.d.ts.map +1 -0
  136. package/dist/schedule/changes.js +55 -0
  137. package/dist/schedule/changes.js.map +1 -0
  138. package/dist/schedule/commands/add.d.ts +24 -0
  139. package/dist/schedule/commands/add.d.ts.map +1 -0
  140. package/dist/schedule/commands/add.js +459 -0
  141. package/dist/schedule/commands/add.js.map +1 -0
  142. package/dist/schedule/commands/args.d.ts +22 -0
  143. package/dist/schedule/commands/args.d.ts.map +1 -0
  144. package/dist/schedule/commands/args.js +80 -0
  145. package/dist/schedule/commands/args.js.map +1 -0
  146. package/dist/schedule/commands/confirm-prompt.d.ts +6 -0
  147. package/dist/schedule/commands/confirm-prompt.d.ts.map +1 -0
  148. package/dist/schedule/commands/confirm-prompt.js +21 -0
  149. package/dist/schedule/commands/confirm-prompt.js.map +1 -0
  150. package/dist/schedule/commands/lifecycle.d.ts +39 -0
  151. package/dist/schedule/commands/lifecycle.d.ts.map +1 -0
  152. package/dist/schedule/commands/lifecycle.js +585 -0
  153. package/dist/schedule/commands/lifecycle.js.map +1 -0
  154. package/dist/schedule/commands/list.d.ts +33 -0
  155. package/dist/schedule/commands/list.d.ts.map +1 -0
  156. package/dist/schedule/commands/list.js +202 -0
  157. package/dist/schedule/commands/list.js.map +1 -0
  158. package/dist/schedule/commands/logs.d.ts +7 -0
  159. package/dist/schedule/commands/logs.d.ts.map +1 -0
  160. package/dist/schedule/commands/logs.js +106 -0
  161. package/dist/schedule/commands/logs.js.map +1 -0
  162. package/dist/schedule/commands/service.d.ts +16 -0
  163. package/dist/schedule/commands/service.d.ts.map +1 -0
  164. package/dist/schedule/commands/service.js +458 -0
  165. package/dist/schedule/commands/service.js.map +1 -0
  166. package/dist/schedule/daemon/daemon.d.ts +156 -0
  167. package/dist/schedule/daemon/daemon.d.ts.map +1 -0
  168. package/dist/schedule/daemon/daemon.js +1248 -0
  169. package/dist/schedule/daemon/daemon.js.map +1 -0
  170. package/dist/schedule/daemon/endpoint.d.ts +38 -0
  171. package/dist/schedule/daemon/endpoint.d.ts.map +1 -0
  172. package/dist/schedule/daemon/endpoint.js +158 -0
  173. package/dist/schedule/daemon/endpoint.js.map +1 -0
  174. package/dist/schedule/daemon/exit.d.ts +8 -0
  175. package/dist/schedule/daemon/exit.d.ts.map +1 -0
  176. package/dist/schedule/daemon/exit.js +8 -0
  177. package/dist/schedule/daemon/exit.js.map +1 -0
  178. package/dist/schedule/daemon/log.d.ts +12 -0
  179. package/dist/schedule/daemon/log.d.ts.map +1 -0
  180. package/dist/schedule/daemon/log.js +53 -0
  181. package/dist/schedule/daemon/log.js.map +1 -0
  182. package/dist/schedule/daemon/notify.d.ts +31 -0
  183. package/dist/schedule/daemon/notify.d.ts.map +1 -0
  184. package/dist/schedule/daemon/notify.js +89 -0
  185. package/dist/schedule/daemon/notify.js.map +1 -0
  186. package/dist/schedule/daemon/retention.d.ts +16 -0
  187. package/dist/schedule/daemon/retention.d.ts.map +1 -0
  188. package/dist/schedule/daemon/retention.js +46 -0
  189. package/dist/schedule/daemon/retention.js.map +1 -0
  190. package/dist/schedule/daemon/sessions.d.ts +23 -0
  191. package/dist/schedule/daemon/sessions.d.ts.map +1 -0
  192. package/dist/schedule/daemon/sessions.js +52 -0
  193. package/dist/schedule/daemon/sessions.js.map +1 -0
  194. package/dist/schedule/env.d.ts +28 -0
  195. package/dist/schedule/env.d.ts.map +1 -0
  196. package/dist/schedule/env.js +99 -0
  197. package/dist/schedule/env.js.map +1 -0
  198. package/dist/schedule/fire/browser-preflight.d.ts +41 -0
  199. package/dist/schedule/fire/browser-preflight.d.ts.map +1 -0
  200. package/dist/schedule/fire/browser-preflight.js +73 -0
  201. package/dist/schedule/fire/browser-preflight.js.map +1 -0
  202. package/dist/schedule/fire/fire.d.ts +78 -0
  203. package/dist/schedule/fire/fire.d.ts.map +1 -0
  204. package/dist/schedule/fire/fire.js +482 -0
  205. package/dist/schedule/fire/fire.js.map +1 -0
  206. package/dist/schedule/fire/result.d.ts +13 -0
  207. package/dist/schedule/fire/result.d.ts.map +1 -0
  208. package/dist/schedule/fire/result.js +18 -0
  209. package/dist/schedule/fire/result.js.map +1 -0
  210. package/dist/schedule/fire/unattended-note.d.ts +22 -0
  211. package/dist/schedule/fire/unattended-note.d.ts.map +1 -0
  212. package/dist/schedule/fire/unattended-note.js +46 -0
  213. package/dist/schedule/fire/unattended-note.js.map +1 -0
  214. package/dist/schedule/floor.d.ts +116 -0
  215. package/dist/schedule/floor.d.ts.map +1 -0
  216. package/dist/schedule/floor.js +1051 -0
  217. package/dist/schedule/floor.js.map +1 -0
  218. package/dist/schedule/folder.d.ts +34 -0
  219. package/dist/schedule/folder.d.ts.map +1 -0
  220. package/dist/schedule/folder.js +84 -0
  221. package/dist/schedule/folder.js.map +1 -0
  222. package/dist/schedule/paths.d.ts +49 -0
  223. package/dist/schedule/paths.d.ts.map +1 -0
  224. package/dist/schedule/paths.js +53 -0
  225. package/dist/schedule/paths.js.map +1 -0
  226. package/dist/schedule/policy.d.ts +141 -0
  227. package/dist/schedule/policy.d.ts.map +1 -0
  228. package/dist/schedule/policy.js +378 -0
  229. package/dist/schedule/policy.js.map +1 -0
  230. package/dist/schedule/resume-command.d.ts +36 -0
  231. package/dist/schedule/resume-command.d.ts.map +1 -0
  232. package/dist/schedule/resume-command.js +72 -0
  233. package/dist/schedule/resume-command.js.map +1 -0
  234. package/dist/schedule/service/detect.d.ts +22 -0
  235. package/dist/schedule/service/detect.d.ts.map +1 -0
  236. package/dist/schedule/service/detect.js +29 -0
  237. package/dist/schedule/service/detect.js.map +1 -0
  238. package/dist/schedule/service/index.d.ts +48 -0
  239. package/dist/schedule/service/index.d.ts.map +1 -0
  240. package/dist/schedule/service/index.js +352 -0
  241. package/dist/schedule/service/index.js.map +1 -0
  242. package/dist/schedule/service/launchd.d.ts +24 -0
  243. package/dist/schedule/service/launchd.d.ts.map +1 -0
  244. package/dist/schedule/service/launchd.js +84 -0
  245. package/dist/schedule/service/launchd.js.map +1 -0
  246. package/dist/schedule/service/manifest.d.ts +44 -0
  247. package/dist/schedule/service/manifest.d.ts.map +1 -0
  248. package/dist/schedule/service/manifest.js +16 -0
  249. package/dist/schedule/service/manifest.js.map +1 -0
  250. package/dist/schedule/service/names.d.ts +9 -0
  251. package/dist/schedule/service/names.d.ts.map +1 -0
  252. package/dist/schedule/service/names.js +22 -0
  253. package/dist/schedule/service/names.js.map +1 -0
  254. package/dist/schedule/service/quote.d.ts +24 -0
  255. package/dist/schedule/service/quote.d.ts.map +1 -0
  256. package/dist/schedule/service/quote.js +64 -0
  257. package/dist/schedule/service/quote.js.map +1 -0
  258. package/dist/schedule/service/runner.d.ts +17 -0
  259. package/dist/schedule/service/runner.d.ts.map +1 -0
  260. package/dist/schedule/service/runner.js +31 -0
  261. package/dist/schedule/service/runner.js.map +1 -0
  262. package/dist/schedule/service/systemd.d.ts +37 -0
  263. package/dist/schedule/service/systemd.d.ts.map +1 -0
  264. package/dist/schedule/service/systemd.js +96 -0
  265. package/dist/schedule/service/systemd.js.map +1 -0
  266. package/dist/schedule/service/windows-task.d.ts +51 -0
  267. package/dist/schedule/service/windows-task.d.ts.map +1 -0
  268. package/dist/schedule/service/windows-task.js +134 -0
  269. package/dist/schedule/service/windows-task.js.map +1 -0
  270. package/dist/schedule/service/wsl.d.ts +57 -0
  271. package/dist/schedule/service/wsl.d.ts.map +1 -0
  272. package/dist/schedule/service/wsl.js +111 -0
  273. package/dist/schedule/service/wsl.js.map +1 -0
  274. package/dist/schedule/store/atomic.d.ts +43 -0
  275. package/dist/schedule/store/atomic.d.ts.map +1 -0
  276. package/dist/schedule/store/atomic.js +135 -0
  277. package/dist/schedule/store/atomic.js.map +1 -0
  278. package/dist/schedule/store/claims.d.ts +26 -0
  279. package/dist/schedule/store/claims.d.ts.map +1 -0
  280. package/dist/schedule/store/claims.js +57 -0
  281. package/dist/schedule/store/claims.js.map +1 -0
  282. package/dist/schedule/store/digest.d.ts +18 -0
  283. package/dist/schedule/store/digest.d.ts.map +1 -0
  284. package/dist/schedule/store/digest.js +139 -0
  285. package/dist/schedule/store/digest.js.map +1 -0
  286. package/dist/schedule/store/history.d.ts +21 -0
  287. package/dist/schedule/store/history.d.ts.map +1 -0
  288. package/dist/schedule/store/history.js +114 -0
  289. package/dist/schedule/store/history.js.map +1 -0
  290. package/dist/schedule/store/jobs.d.ts +61 -0
  291. package/dist/schedule/store/jobs.d.ts.map +1 -0
  292. package/dist/schedule/store/jobs.js +210 -0
  293. package/dist/schedule/store/jobs.js.map +1 -0
  294. package/dist/schedule/store/state.d.ts +19 -0
  295. package/dist/schedule/store/state.d.ts.map +1 -0
  296. package/dist/schedule/store/state.js +38 -0
  297. package/dist/schedule/store/state.js.map +1 -0
  298. package/dist/schedule/types.d.ts +256 -0
  299. package/dist/schedule/types.d.ts.map +1 -0
  300. package/dist/schedule/types.js +9 -0
  301. package/dist/schedule/types.js.map +1 -0
  302. package/dist/skills/catalog.d.ts +96 -0
  303. package/dist/skills/catalog.d.ts.map +1 -0
  304. package/dist/skills/catalog.js +226 -0
  305. package/dist/skills/catalog.js.map +1 -0
  306. package/dist/skills/save.d.ts +148 -0
  307. package/dist/skills/save.d.ts.map +1 -0
  308. package/dist/skills/save.js +344 -0
  309. package/dist/skills/save.js.map +1 -0
  310. package/dist/skills/store.d.ts +103 -16
  311. package/dist/skills/store.d.ts.map +1 -1
  312. package/dist/skills/store.js +230 -29
  313. package/dist/skills/store.js.map +1 -1
  314. package/dist/tui/App.d.ts.map +1 -1
  315. package/dist/tui/App.js +591 -72
  316. package/dist/tui/App.js.map +1 -1
  317. package/dist/tui/LiveActivity.d.ts +8 -1
  318. package/dist/tui/LiveActivity.d.ts.map +1 -1
  319. package/dist/tui/LiveActivity.js +20 -4
  320. package/dist/tui/LiveActivity.js.map +1 -1
  321. package/dist/tui/PermissionOverlay.d.ts +24 -4
  322. package/dist/tui/PermissionOverlay.d.ts.map +1 -1
  323. package/dist/tui/PermissionOverlay.js +113 -15
  324. package/dist/tui/PermissionOverlay.js.map +1 -1
  325. package/dist/tui/SaveSkillOverlay.d.ts +33 -0
  326. package/dist/tui/SaveSkillOverlay.d.ts.map +1 -0
  327. package/dist/tui/SaveSkillOverlay.js +118 -0
  328. package/dist/tui/SaveSkillOverlay.js.map +1 -0
  329. package/dist/tui/agent.d.ts +185 -5
  330. package/dist/tui/agent.d.ts.map +1 -1
  331. package/dist/tui/agent.js +325 -54
  332. package/dist/tui/agent.js.map +1 -1
  333. package/dist/tui/browser-notices.d.ts +33 -0
  334. package/dist/tui/browser-notices.d.ts.map +1 -0
  335. package/dist/tui/browser-notices.js +102 -0
  336. package/dist/tui/browser-notices.js.map +1 -0
  337. package/dist/tui/permission-review.d.ts.map +1 -1
  338. package/dist/tui/permission-review.js +53 -4
  339. package/dist/tui/permission-review.js.map +1 -1
  340. package/dist/tui/schedule/host-commands.d.ts +34 -0
  341. package/dist/tui/schedule/host-commands.d.ts.map +1 -0
  342. package/dist/tui/schedule/host-commands.js +309 -0
  343. package/dist/tui/schedule/host-commands.js.map +1 -0
  344. package/dist/tui/schedule/integration.d.ts +62 -0
  345. package/dist/tui/schedule/integration.d.ts.map +1 -0
  346. package/dist/tui/schedule/integration.js +145 -0
  347. package/dist/tui/schedule/integration.js.map +1 -0
  348. package/dist/tui/schedule/loop-host.d.ts +42 -0
  349. package/dist/tui/schedule/loop-host.d.ts.map +1 -0
  350. package/dist/tui/schedule/loop-host.js +153 -0
  351. package/dist/tui/schedule/loop-host.js.map +1 -0
  352. package/dist/tui/schedule/resume.d.ts +122 -0
  353. package/dist/tui/schedule/resume.d.ts.map +1 -0
  354. package/dist/tui/schedule/resume.js +324 -0
  355. package/dist/tui/schedule/resume.js.map +1 -0
  356. package/dist/tui/schedule/startup.d.ts +11 -0
  357. package/dist/tui/schedule/startup.d.ts.map +1 -0
  358. package/dist/tui/schedule/startup.js +121 -0
  359. package/dist/tui/schedule/startup.js.map +1 -0
  360. package/dist/tui/schedule/tool-host.d.ts +44 -0
  361. package/dist/tui/schedule/tool-host.d.ts.map +1 -0
  362. package/dist/tui/schedule/tool-host.js +294 -0
  363. package/dist/tui/schedule/tool-host.js.map +1 -0
  364. package/dist/tui/skills/learning.d.ts +74 -0
  365. package/dist/tui/skills/learning.d.ts.map +1 -0
  366. package/dist/tui/skills/learning.js +131 -0
  367. package/dist/tui/skills/learning.js.map +1 -0
  368. package/dist/tui/skills/suggest-setting.d.ts +22 -0
  369. package/dist/tui/skills/suggest-setting.d.ts.map +1 -0
  370. package/dist/tui/skills/suggest-setting.js +55 -0
  371. package/dist/tui/skills/suggest-setting.js.map +1 -0
  372. package/dist/tui/skills/suggestion-ledger.d.ts +35 -0
  373. package/dist/tui/skills/suggestion-ledger.d.ts.map +1 -0
  374. package/dist/tui/skills/suggestion-ledger.js +52 -0
  375. package/dist/tui/skills/suggestion-ledger.js.map +1 -0
  376. package/dist/tui/slashCommands.d.ts +20 -0
  377. package/dist/tui/slashCommands.d.ts.map +1 -1
  378. package/dist/tui/slashCommands.js +76 -2
  379. package/dist/tui/slashCommands.js.map +1 -1
  380. package/dist/tui/turn-interruption.d.ts +20 -1
  381. package/dist/tui/turn-interruption.d.ts.map +1 -1
  382. package/dist/tui/turn-interruption.js +43 -0
  383. package/dist/tui/turn-interruption.js.map +1 -1
  384. package/dist/tui/types.d.ts +9 -1
  385. package/dist/tui/types.d.ts.map +1 -1
  386. package/package.json +5 -3
  387. package/skills/README.md +15 -0
  388. package/skills/browser-automation/SKILL.md +84 -0
  389. package/skills/schedule-task/SKILL.md +140 -0
  390. package/skills/skill-creator/SKILL.md +101 -0
package/dist/tui/App.js CHANGED
@@ -16,8 +16,8 @@ import { TURN_LIMIT_FIELDS, turnLimitCommands } from './turn-limits-settings.js'
16
16
  import { discoverProviders } from '../integrations/providers/discover.js';
17
17
  import { ProviderSetup } from './ProviderSetup.js';
18
18
  import { ToolOutputViewer } from './ToolOutputViewer.js';
19
- import { relative } from 'node:path';
20
- import { DEFAULT_MAX_GOAL_ROUNDS, DiskMessageFeedbackStore, GoalRoundLimitError, HostCommandRegistry, SessionGoalActivation, StaleGoalError, TurnCancelled, asSessionId, createAssistantMessage, createUserMessage, generateSessionId, generateTurnId, isCompactionMessage, kernelHostCommands, } from '@namzu/sdk';
19
+ import { join, relative } from 'node:path';
20
+ import { DEFAULT_MAX_GOAL_ROUNDS, DiskMessageFeedbackStore, GoalRoundLimitError, HostCommandRegistry, SessionGoalActivation, StaleGoalError, TurnCancelled, asSessionId, createAssistantMessage, createUserMessage, generateSessionId, generateTurnId, isCompactionMessage, kernelHostCommands, SKILL_TOOL_NAME, } from '@namzu/sdk';
21
21
  import { Box, Text, useApp, useInput, useStdout, useWindowSize } from 'ink';
22
22
  import { useCallback, useEffect, useRef, useState } from 'react';
23
23
  import { pinTrustedProjectPath, resolveTrustedProjectContext, } from '../config/trusted-project-context.js';
@@ -35,7 +35,7 @@ import { checkUpdates } from '../integrations/updates.js';
35
35
  import { renderMemoryReport, renderMemorySaveResult, renderStoredMemorySection, renderTypedNoteResult, } from '../memory/presentation.js';
36
36
  import { appendMemoryWithStatus, readMemory } from '../memory/store.js';
37
37
  import { effectivePermissionMode, permissionModeDescription, permissionModeLabel, } from '../permissions/mode.js';
38
- import { composeSkillsPrompt, discoverSkills, loadSkillBody } from '../skills/store.js';
38
+ import { NO_SKILLS_FOUND, composeSkillsPrompt, discoverSkills, loadSkillBody, renderSkillRoster, skillTierLabel, } from '../skills/store.js';
39
39
  import { discoverUserCommands } from '../user-commands/store.js';
40
40
  import { AgentCockpit, AgentNarrationBand, AgentTaskPanel, AgentTranscript, activeSubagentCohorts, agentCockpitIsWide, agentPhasePageSize, agentPhases, agentPickerPageSize, agentTranscriptPageSize, agentWorkflowPageSize, agentWorkflows, maxAgentTranscriptTailOffset, } from './AgentExplorer.js';
41
41
  import { completionRow, launchReceipt, settleLine } from './agent-transcript-rows.js';
@@ -49,9 +49,16 @@ import { CopyPicker } from './CopyPicker.js';
49
49
  import { EditPromptPicker } from './EditPromptPicker.js';
50
50
  import { LiveActivity, formatElapsed } from './LiveActivity.js';
51
51
  import { webCallTitle, webLiveStatus, webSettledLine } from './web-activity.js';
52
- import { PermissionOverlay } from './PermissionOverlay.js';
52
+ import { PermissionOverlay, permissionAnswers } from './PermissionOverlay.js';
53
53
  import { Picker } from './Picker.js';
54
54
  import { ResumePicker } from './ResumePicker.js';
55
+ import { resolveNamzuHome } from '../integrations/state/home.js';
56
+ import { createScheduleIntegration } from './schedule/integration.js';
57
+ import { SaveSkillOverlay } from './SaveSkillOverlay.js';
58
+ import { SKILL_CREATOR_SKILL, buildSaveSkillTool, learnSkillPrompt, newSkillPrompt, } from '../skills/save.js';
59
+ import { SUGGESTIONS_STOPPED_NOTICE, createTurnActivity, judgeSkillSuggestion, observeTurnEvent, skillSuggestionNotice, } from './skills/learning.js';
60
+ import { FRESH_LEDGER, admitSuggestion, readSuggestionLedger, writeSuggestionLedger, } from './skills/suggestion-ledger.js';
61
+ import { setSkillSuggestions } from './skills/suggest-setting.js';
55
62
  import { StatusBar } from './StatusBar.js';
56
63
  import { isRepeatedNotice } from './notices.js';
57
64
  import { checklistProgress } from './Checklist.js';
@@ -81,7 +88,8 @@ import { renderCheckpoints, renderRestore } from '../checkpoints/store.js';
81
88
  import { expandFileMentions, listMentionableFiles } from './mentions.js';
82
89
  import { openInBrowser } from './open-browser.js';
83
90
  import { buildPermissionReview, buildPermissionSummary, permissionReviewPageRows, permissionReviewRefusal, permissionReviewRows, releasedByApproveAll, } from './permission-review.js';
84
- import { describeTurnInterruption, describeTurnStop } from './turn-interruption.js';
91
+ import { PAUSED_TURN_LINES, describeTurnInterruption, describeTurnStop, handoffContinuationNote, } from './turn-interruption.js';
92
+ import { browserSiteNotes, describeBrowserHandoff, runBrowserSlash } from './browser-notices.js';
85
93
  import { moveSelection } from './selection-window.js';
86
94
  import { describeShellEscape, describeShellEscapeForModel, runShellEscape, shellEscapeCommand, } from './shell-escape.js';
87
95
  import { baseBranchReviewPrompt, commitReviewPrompt, hostCommandNames, kernelCommandDescriptors, mergeHostCommands, renderAgents, renderOutcome, reviewPrompt, runSlash, } from './slashCommands.js';
@@ -314,6 +322,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
314
322
  const initialConversationIdRef = useRef(initialCtx.initialConversationId);
315
323
  const [history, setHistory] = useState([]);
316
324
  const [state, setState] = useState('idle');
325
+ /** For the scheduler integration's loop timer, which reads it outside render. */
326
+ const stateRef = useRef(state);
327
+ stateRef.current = state;
317
328
  const [phase, setPhase] = useState('probing');
318
329
  const [session, setSession] = useState(null);
319
330
  // Once a usable session publishes the transcript, temporary lifecycle
@@ -617,6 +628,25 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
617
628
  }, []);
618
629
  const [composerDraft, setComposerDraft] = useState(null);
619
630
  const [composerHasDraft, setComposerHasDraft] = useState(false);
631
+ // A turn a tool paused for a person (`ToolResult.handoff`), while its
632
+ // notice is the newest thing on screen: Enter continues it, Esc stops it.
633
+ // The ref is what the key handler reads; the state is what the footer shows.
634
+ /**
635
+ * The stream of the turn running now, so a tool's own screen can close the
636
+ * reply it streamed before the tool ran. The kernel hands the tool's start
637
+ * to this loop only when its batch settles, so a reply streamed with a
638
+ * tool call stays pending while that tool asks the operator something; a
639
+ * row written then (the schedule tool's confirmation) cannot reach
640
+ * scrollback past the pending reply, and the top of a tall one was cut
641
+ * off while the operator was asked about it.
642
+ */
643
+ const liveStreamRef = useRef(null);
644
+ const [handoffPark, setHandoffParkState] = useState(null);
645
+ const handoffParkRef = useRef(null);
646
+ const setHandoffPark = useCallback((value) => {
647
+ handoffParkRef.current = value;
648
+ setHandoffParkState(value);
649
+ }, []);
620
650
  const composerDraftTokenRef = useRef(0);
621
651
  /** Bounded child session projection published by the current AgentSession. */
622
652
  const [subagents, setSubagentsState] = useState([]);
@@ -837,6 +867,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
837
867
  * server's child process outlives the object that opened it.
838
868
  */
839
869
  const previousSessionRef = useRef(null);
870
+ /** The profile `/browser profile` chose, kept across session rebuilds. */
871
+ const browserProfileRef = useRef(undefined);
872
+ /** The current session's browser, for notices written from event handlers. */
873
+ const browserControlRef = useRef(undefined);
874
+ useEffect(() => {
875
+ browserControlRef.current = session?.browser;
876
+ }, [session]);
840
877
  useEffect(() => {
841
878
  const source = session?.subagents;
842
879
  setAgentSurface(null);
@@ -910,6 +947,89 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
910
947
  // turns attribute to the resumed conversation.
911
948
  const sessionsRef = useRef(null);
912
949
  const scopeRef = useRef(null);
950
+ /**
951
+ * Scheduled jobs and session loops (`./schedule/`). Built once; what it
952
+ * reads later — the transcript writer, the question and permission
953
+ * screens, the submit path, the session's model — is filled in each
954
+ * render through `scheduleLiveRef`.
955
+ */
956
+ const scheduleLiveRef = useRef(null);
957
+ const scheduleRef = useRef(null);
958
+ if (scheduleRef.current === null) {
959
+ scheduleRef.current = createScheduleIntegration({
960
+ home: () => sessionsRef.current?.root ?? resolveNamzuHome(),
961
+ cwd: () => ctxRef.current.cwd,
962
+ extraRoots: () => ctxRef.current.additionalDirectories ?? [],
963
+ config: () => ctxRef.current,
964
+ model: () => scheduleLiveRef.current?.model,
965
+ sessionId: () => (conversationMaterializedRef.current ? scopeRef.current?.sessionId : undefined),
966
+ loopsFile: () => {
967
+ const sessions = sessionsRef.current;
968
+ const scope = scopeRef.current;
969
+ if (!sessions || !scope || !conversationMaterializedRef.current)
970
+ return undefined;
971
+ try {
972
+ return join(sessions.paths.sessionDir({ sessionId: scope.sessionId }), 'loops.json');
973
+ }
974
+ catch {
975
+ return undefined;
976
+ }
977
+ },
978
+ isIdle: () => stateRef.current === 'idle' && abortRef.current === null,
979
+ say: (text) => scheduleLiveRef.current?.say(text),
980
+ ask: (question, signal) => scheduleLiveRef.current?.ask(question, signal) ?? Promise.resolve({ kind: 'skip' }),
981
+ askPermission: (request) => scheduleLiveRef.current?.askPermission(request) ??
982
+ Promise.resolve({ kind: 'reject', feedback: 'Nobody can answer yet.' }),
983
+ submit: (text) => scheduleLiveRef.current?.submit(text),
984
+ });
985
+ }
986
+ /**
987
+ * `save_skill`'s confirmation. The tool is the TUI's alone (never exec, a
988
+ * scheduled run or a sub-agent), and its question is this screen, not the
989
+ * permission gate, so no permission mode answers it for the operator.
990
+ */
991
+ const [saveSkillPrompt, setSaveSkillPromptState] = useState(null);
992
+ const saveSkillPromptRef = useRef(null);
993
+ const setSaveSkillPrompt = useCallback((next) => {
994
+ saveSkillPromptRef.current = next;
995
+ setSaveSkillPromptState(next);
996
+ }, []);
997
+ const saveSkillNotifyRef = useRef(null);
998
+ const saveSkillToolRef = useRef(null);
999
+ if (saveSkillToolRef.current === null) {
1000
+ saveSkillToolRef.current = buildSaveSkillTool({
1001
+ cwd: () => ctxRef.current.cwd,
1002
+ config: () => ctxRef.current.skills,
1003
+ sessionId: () => conversationMaterializedRef.current ? scopeRef.current?.sessionId : undefined,
1004
+ confirm: (request, signal) => new Promise((resolve) => {
1005
+ if (signal?.aborted) {
1006
+ resolve('cancel');
1007
+ return;
1008
+ }
1009
+ const entry = {
1010
+ request,
1011
+ resolve: (answer) => {
1012
+ signal?.removeEventListener('abort', onAbort);
1013
+ // The screen answers from its own key handler, which Ink calls
1014
+ // before App's (a child subscribes first). Clearing the ref now
1015
+ // would hand the same Esc or Ctrl+C to App, which reads it as
1016
+ // an interrupt of the turn this answer is for. Let the key's
1017
+ // dispatch finish first.
1018
+ queueMicrotask(() => {
1019
+ if (saveSkillPromptRef.current === entry)
1020
+ setSaveSkillPrompt(null);
1021
+ });
1022
+ resolve(answer);
1023
+ },
1024
+ };
1025
+ const onAbort = () => entry.resolve('cancel');
1026
+ signal?.addEventListener('abort', onAbort, { once: true });
1027
+ setSaveSkillPrompt(entry);
1028
+ saveSkillNotifyRef.current?.();
1029
+ }),
1030
+ saved: ({ name, path }) => scheduleLiveRef.current?.say(`✎ Saved skill ${name} to ${path}. The model is offered it from the next turn; /skills ${name} activates it now.`),
1031
+ });
1032
+ }
913
1033
  /**
914
1034
  * A provider session can be ready before a person has started a conversation.
915
1035
  * The scope still needs a mutable session cursor, but its generated id is
@@ -948,6 +1068,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
948
1068
  const [goalDriveVersion, setGoalDriveVersion] = useState(0);
949
1069
  const [goalStatus, setGoalStatus] = useState(null);
950
1070
  const pendingGoalResumeRef = useRef(null);
1071
+ /**
1072
+ * The conversation `namzu resume <id>` opened, until the screen is ready to
1073
+ * look at it once: a scheduled run parked on a decision there is continued
1074
+ * then, so its permission prompt is on screen without typing `/resume`.
1075
+ */
1076
+ const pendingScheduledResumeRef = useRef(null);
951
1077
  const wakeGoalDriver = useCallback(() => setGoalDriveVersion((version) => version + 1), []);
952
1078
  /**
953
1079
  * Conversation writes in the order the operator produced them.
@@ -1160,6 +1286,56 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1160
1286
  ]);
1161
1287
  return id;
1162
1288
  }, [nextId]);
1289
+ /**
1290
+ * The skill proposal's per-conversation facts: whether this conversation
1291
+ * has proposed one, and whether it has used a skill. Keyed by the
1292
+ * conversation generation, so `/clear`, `/resume` and a fork start over.
1293
+ */
1294
+ const learningRef = useRef({ generation: -1, proposed: false, skillUsed: false });
1295
+ /** `/skills save off|on` in this session, over whatever the config files say. */
1296
+ const skillSuggestOverrideRef = useRef(undefined);
1297
+ const conversationLearning = useCallback(() => {
1298
+ if (learningRef.current.generation !== conversationGenRef.current) {
1299
+ learningRef.current = {
1300
+ generation: conversationGenRef.current,
1301
+ proposed: false,
1302
+ skillUsed: false,
1303
+ };
1304
+ }
1305
+ return learningRef.current;
1306
+ }, []);
1307
+ /**
1308
+ * After a turn settles: one dim line proposing to save it as a skill, when
1309
+ * the turn earned it (`judgeSkillSuggestion`) and the anti-nag ledger lets
1310
+ * it through. Costs nothing and saves nothing; `/skills save` does that.
1311
+ */
1312
+ const offerSkillSuggestion = useCallback((st) => {
1313
+ if (!st.learning)
1314
+ return;
1315
+ const conversation = conversationLearning();
1316
+ const config = ctxRef.current.skills;
1317
+ const verdict = judgeSkillSuggestion(st.learning, {
1318
+ suggest: skillSuggestOverrideRef.current ?? config?.suggest,
1319
+ ...(config?.suggestMinToolCalls !== undefined
1320
+ ? { minToolCalls: config.suggestMinToolCalls }
1321
+ : {}),
1322
+ planMode: st.startedInPlanMode === true || permissionModeRef.current === 'plan',
1323
+ skillUsedInConversation: conversation.skillUsed,
1324
+ proposedInConversation: conversation.proposed,
1325
+ skillFlowTurn: st.skillFlow === true,
1326
+ });
1327
+ if (!verdict.suggest)
1328
+ return;
1329
+ const home = resolveNamzuHome();
1330
+ const admission = admitSuggestion(readSuggestionLedger(home));
1331
+ if (admission.show === 'nothing')
1332
+ return;
1333
+ conversation.proposed = true;
1334
+ writeSuggestionLedger(home, admission.next);
1335
+ pushMessage('system', admission.show === 'proposal'
1336
+ ? skillSuggestionNotice(verdict.steps, verdict.tools)
1337
+ : SUGGESTIONS_STOPPED_NOTICE, false, '✻', undefined, theme.text.muted);
1338
+ }, [conversationLearning, pushMessage]);
1163
1339
  /**
1164
1340
  * `/agents [running|available|batches]`, answered by the delegation UI's
1165
1341
  * `agentsSlashCommand` over this session's live monitor and the batches the
@@ -1532,7 +1708,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1532
1708
  })();
1533
1709
  }, [pushMessage]);
1534
1710
  const activateSkill = useCallback((name) => {
1535
- const info = discoverSkills({ cwd: ctx.cwd }).find((skill) => skill.name === name);
1711
+ const info = discoverSkills({
1712
+ cwd: ctx.cwd,
1713
+ ...(ctx.skills ? { config: ctx.skills } : {}),
1714
+ }).find((skill) => skill.name === name);
1536
1715
  if (!info) {
1537
1716
  pushMessage('system', `No skill named "${name}". See /skills.`);
1538
1717
  return;
@@ -1543,12 +1722,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1543
1722
  ...previous.filter((skill) => skill.name !== info.name),
1544
1723
  { name: info.name, body },
1545
1724
  ]);
1725
+ conversationLearning().skillUsed = true;
1546
1726
  pushMessage('system', `Activated skill: ${info.name}`);
1547
1727
  }
1548
1728
  catch (error) {
1549
1729
  pushMessage('system', `Could not load skill "${name}": ${error instanceof Error ? error.message : String(error)}`);
1550
1730
  }
1551
- }, [ctx.cwd, pushMessage]);
1731
+ }, [conversationLearning, ctx.cwd, ctx.skills, pushMessage]);
1552
1732
  const removeStoredCredential = useCallback((target) => {
1553
1733
  const path = credentialsPath();
1554
1734
  const hadClaude = readStoredSubscriptionCredential() !== null;
@@ -2151,6 +2331,23 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2151
2331
  const finalizeMessage = useCallback((id, finalContent) => {
2152
2332
  setMessages((prev) => prev.map((m) => m.id === id ? { ...m, content: finalContent ?? m.content, pending: false } : m));
2153
2333
  }, []);
2334
+ /**
2335
+ * Close the reply the running turn streamed before a tool took over, as a
2336
+ * tool's start does. For a tool that shows the operator something of its
2337
+ * own while it runs: the reply is complete by then (tools run after the
2338
+ * model's response ends), and left pending it holds every later row out
2339
+ * of scrollback. Text that arrives later starts a new reply.
2340
+ */
2341
+ const closeLiveReply = useCallback(() => {
2342
+ const st = liveStreamRef.current;
2343
+ if (!st)
2344
+ return;
2345
+ flushStream(st);
2346
+ if (st.assistantId) {
2347
+ finalizeMessage(st.assistantId);
2348
+ st.assistantId = null;
2349
+ }
2350
+ }, [finalizeMessage, flushStream]);
2154
2351
  // Open the SDK session store and select the durable conversation once.
2155
2352
  // This is an admission gate for every startup, not optional persistence: a
2156
2353
  // corrupt or split estate must not silently widen into cwd-local state and
@@ -2172,6 +2369,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2172
2369
  ? { text: persistedOutput, provenance: 'persisted' }
2173
2370
  : null;
2174
2371
  pendingGoalResumeRef.current = requestedConversationId;
2372
+ pendingScheduledResumeRef.current = requestedConversationId;
2175
2373
  initialConversationIdRef.current = undefined;
2176
2374
  conversationMaterializedRef.current = true;
2177
2375
  }
@@ -2243,21 +2441,50 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2243
2441
  if (signal?.aborted)
2244
2442
  return;
2245
2443
  const activeCtx = ctxRef.current;
2444
+ const primary = prefs.providers[0];
2445
+ if (primary && scheduleLiveRef.current) {
2446
+ scheduleLiveRef.current.model = {
2447
+ provider: primary.id,
2448
+ ...(primary.model ? { model: primary.model } : {}),
2449
+ };
2450
+ }
2451
+ const browserConfig = activeCtx.browser;
2452
+ const browserProfile = browserProfileRef.current ?? browserConfig?.defaultProfile;
2453
+ const browserOptions = browserConfig
2454
+ ? {
2455
+ ...(browserProfile ? { profile: browserProfile } : {}),
2456
+ ...(browserConfig.engine ? { engine: browserConfig.engine } : {}),
2457
+ ...(browserConfig.headless ? { headless: browserConfig.headless } : {}),
2458
+ ...(browserConfig.sites ? { sites: browserConfig.sites } : {}),
2459
+ ...(browserConfig.keepOpen ? { keepOpen: true } : {}),
2460
+ ...(sessionsRef.current ? { home: sessionsRef.current.root } : {}),
2461
+ }
2462
+ : undefined;
2246
2463
  const s = await createAgentSession(prefs, detectedNow, {
2247
2464
  scope,
2248
2465
  cwd: activeCtx.cwd,
2466
+ // A person is here to confirm: the model may propose scheduled jobs
2467
+ // and session loops (never in exec, a scheduled run or a sub-agent).
2468
+ extraTools: [
2469
+ ...(scheduleRef.current?.tools() ?? []),
2470
+ ...(saveSkillToolRef.current ? [saveSkillToolRef.current] : []),
2471
+ ],
2249
2472
  ...(activeCtx.structuredOutput ? { structuredOutput: activeCtx.structuredOutput } : {}),
2250
2473
  ...(activeCtx.additionalDirectories
2251
2474
  ? { additionalDirectories: activeCtx.additionalDirectories }
2252
2475
  : {}),
2253
2476
  ...(sessionsRef.current ? { stateRoot: sessionsRef.current.root } : {}),
2254
2477
  enableComputerUse: true,
2478
+ // The browser tools, when the config did not switch them off. The
2479
+ // profile is the one `/browser profile` chose, else the config's.
2480
+ ...(browserOptions ? { browser: browserOptions } : {}),
2255
2481
  rules: activeCtx.rules,
2256
2482
  ...(sessionsRef.current
2257
2483
  ? { sessionGoals: sessionsRef.current.goals, conversationSessions: sessionsRef.current }
2258
2484
  : {}),
2259
2485
  ...(activeCtx.mcpServers ? { mcpServers: activeCtx.mcpServers } : {}),
2260
2486
  ...(activeCtx.plugins ? { plugins: activeCtx.plugins } : {}),
2487
+ ...(activeCtx.skills ? { skills: activeCtx.skills } : {}),
2261
2488
  ...(activeCtx.web ? { web: activeCtx.web } : {}),
2262
2489
  ...(activeCtx.hooks ? { hooks: activeCtx.hooks } : {}),
2263
2490
  ...(activeCtx.compaction ? { compaction: activeCtx.compaction } : {}),
@@ -2862,7 +3089,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2862
3089
  return;
2863
3090
  }
2864
3091
  await session.abandonTurn(active.turnId, reason);
2865
- pushMessage('system', `Abandoned turn ${active.turnId}. The next prompt starts a new turn in this conversation.`);
3092
+ pushMessage('system', PAUSED_TURN_LINES.abandoned);
2866
3093
  }
2867
3094
  catch (err) {
2868
3095
  pushMessage('system', `Could not abandon the turn: ${err instanceof Error ? err.message : String(err)}`);
@@ -2875,7 +3102,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2875
3102
  */
2876
3103
  // `applyEvent` is declared further down; the resume path reads it at call time.
2877
3104
  const applyEventRef = useRef(null);
2878
- const resumeActiveTurn = useCallback(async () => {
3105
+ const resumeActiveTurn = useCallback(async (resume) => {
2879
3106
  const sessions = sessionsRef.current;
2880
3107
  const scope = scopeRef.current;
2881
3108
  if (!sessions || !scope || !conversationMaterializedRef.current || !session?.hasProvider)
@@ -2887,6 +3114,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2887
3114
  pushMessage('system', 'Wait for the running turn to finish before resuming the parked one.');
2888
3115
  return true;
2889
3116
  }
3117
+ setHandoffPark(null);
2890
3118
  const ac = new AbortController();
2891
3119
  abortRef.current = ac;
2892
3120
  setState('thinking');
@@ -2900,15 +3128,58 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2900
3128
  sessionId: scope.sessionId,
2901
3129
  notification: null,
2902
3130
  };
2903
- pushMessage('system', `Resuming turn ${active.turnId} from its checkpoint.`, false, '▶');
3131
+ liveStreamRef.current = st;
3132
+ let scheduledRun = false;
3133
+ let restoreBrowser;
2904
3134
  try {
3135
+ // A scheduled run parked on a decision: the operator answers the
3136
+ // parked batch here, and the turn continues under the job's rules.
3137
+ const scheduled = active.paused
3138
+ ? await scheduleRef.current?.prepareResume(permissionModeRef.current, {
3139
+ cwd: ctxRef.current.cwd,
3140
+ roots: session.directories?.list() ?? [],
3141
+ sandboxed: session.sandbox.workspace !== 'host',
3142
+ // The resumed turn runs on the job's provider: one this
3143
+ // session has no credential for is refused before asking.
3144
+ providers: detected.map((item) => item.entry.id),
3145
+ browser: session.browser !== undefined,
3146
+ })
3147
+ : undefined;
3148
+ if (scheduled && 'leave' in scheduled) {
3149
+ pushMessage('system', 'The scheduled run stays waiting. /resume asks again.');
3150
+ return true;
3151
+ }
3152
+ if (scheduled && 'abandon' in scheduled) {
3153
+ scheduledRun = true;
3154
+ if (!session.abandonTurn)
3155
+ throw new Error('this session cannot abandon turns');
3156
+ await session.abandonTurn(active.turnId, scheduled.abandon);
3157
+ pushMessage('system', PAUSED_TURN_LINES.abandonedScheduled);
3158
+ return true;
3159
+ }
3160
+ scheduledRun = scheduled !== undefined;
3161
+ const { browser: jobBrowser, ...scheduledParams } = scheduled ?? {};
3162
+ // A job's browser grant: this turn drives the job's profile, held
3163
+ // to its sites, and the session's own comes back when it ends.
3164
+ if (jobBrowser && session.browser) {
3165
+ restoreBrowser = await session.browser.runAs(jobBrowser);
3166
+ pushMessage('system', `The browser uses the job’s profile ${jobBrowser.profile} for this run, on its sites only.`);
3167
+ }
3168
+ // Said once the turn really continues: a scheduled park can still
3169
+ // be left waiting or abandoned above.
3170
+ pushMessage('system', PAUSED_TURN_LINES.resuming, false, '▶');
2905
3171
  for await (const event of session.resumePaused({
2906
3172
  turnId: active.turnId,
2907
3173
  signal: ac.signal,
2908
- // The operator's mode, read at every decision like a new turn's:
2909
- // `/resume` in plan mode stays read-only, and so do its children.
2910
- permissionMode: permissionModeRef.current,
2911
- currentPermissionMode: () => permissionModeRef.current,
3174
+ ...(scheduled ? scheduledParams : {
3175
+ // The operator's mode, read at every decision like a new turn's:
3176
+ // `/resume` in plan mode stays read-only, and so do its children.
3177
+ permissionMode: permissionModeRef.current,
3178
+ currentPermissionMode: () => permissionModeRef.current,
3179
+ ...(resume?.handoffReason
3180
+ ? { systemNote: handoffContinuationNote(resume.handoffReason) }
3181
+ : {}),
3182
+ }),
2912
3183
  })) {
2913
3184
  applyEventRef.current?.(event, st);
2914
3185
  }
@@ -2926,9 +3197,38 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2926
3197
  if (abortRef.current === ac)
2927
3198
  abortRef.current = null;
2928
3199
  setState('idle');
3200
+ // A scheduled run answered here records its end in its job now —
3201
+ // completed, failed or cancelled — not at a scheduler's next tick,
3202
+ // which may never come. One that parked again stays waiting.
3203
+ if (scheduledRun)
3204
+ await scheduleRef.current?.settleAnswered();
3205
+ await restoreBrowser?.().catch(() => undefined);
2929
3206
  }
2930
3207
  return true;
2931
- }, [finalizeMessage, flushStream, pushMessage, session, state]);
3208
+ }, [detected, finalizeMessage, flushStream, pushMessage, session, setHandoffPark, state]);
3209
+ // `namzu resume <id>` of a scheduled run parked on a decision: what the
3210
+ // notification and `/schedule` tell the operator to run. Continue it once
3211
+ // the screen is ready, exactly as `/resume` would, so the parked call is on
3212
+ // the permission screen; the job and session checks in `prepareResume`
3213
+ // still refuse a session that does not run as the job does. Any other
3214
+ // parked or interrupted turn is left for the operator's own `/resume`.
3215
+ useEffect(() => {
3216
+ const requested = pendingScheduledResumeRef.current;
3217
+ if (!requested || phase !== 'ready' || state !== 'idle' || !session?.hasProvider)
3218
+ return;
3219
+ const sessions = sessionsRef.current;
3220
+ if (!sessions || scopeRef.current?.sessionId !== requested || abortRef.current)
3221
+ return;
3222
+ pendingScheduledResumeRef.current = null;
3223
+ void (async () => {
3224
+ const active = await activeConversationTurn(sessions, asSessionId(requested)).catch(() => undefined);
3225
+ if (!active?.paused || !(await scheduleRef.current?.isParked()))
3226
+ return;
3227
+ if (scopeRef.current?.sessionId !== requested)
3228
+ return;
3229
+ await resumeActiveTurn();
3230
+ })();
3231
+ }, [phase, resumeActiveTurn, session, state]);
2932
3232
  const doResume = useCallback(async () => {
2933
3233
  const sessions = sessionsRef.current ?? (await ensureSessions(), sessionsRef.current);
2934
3234
  if (!sessions) {
@@ -3596,10 +3896,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3596
3896
  });
3597
3897
  }
3598
3898
  const summary = buildPermissionSummary(review.text);
3899
+ const siteNotes = browserSiteNotes(req.toolCalls, browserControlRef.current?.status());
3599
3900
  return new Promise((resolve) => {
3600
3901
  if (permissionResolveRef.current) {
3601
3902
  permissionQueueRef.current.push({
3602
- permission: { ...req, review: review.text, summary },
3903
+ permission: { ...req, review: review.text, summary, siteNotes },
3603
3904
  resolve,
3604
3905
  });
3605
3906
  setQueuedPermissionCount(permissionQueueRef.current.length);
@@ -3609,7 +3910,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3609
3910
  permissionOpenedAtRef.current = Date.now();
3610
3911
  setPermissionReviewOffset(0);
3611
3912
  setPermissionDetailsOpen(!summary.complete);
3612
- setPermission({ ...req, review: review.text, summary });
3913
+ setPermission({ ...req, review: review.text, summary, siteNotes });
3613
3914
  setState('awaiting-permission');
3614
3915
  sendTerminalNotification({ kind: 'approval-required' });
3615
3916
  });
@@ -3620,15 +3921,26 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3620
3921
  * and the tool's promise waits on the row the operator picks. One question
3621
3922
  * at a time: the tool is not concurrency-safe, so a second cannot arrive
3622
3923
  * while the first is up.
3924
+ *
3925
+ * `signal`, when the caller has one (the `schedule` tool's confirmation,
3926
+ * which waits on a person and so is given the tool's own abort signal),
3927
+ * is honoured the way `save_skill`'s screen honours the same signal: an
3928
+ * abort — the tool's own deadline elapsing, or the turn stopping —
3929
+ * answers as `abort` and takes the picker off screen, rather than
3930
+ * leaving it live after the tool has already given up on the answer.
3623
3931
  */
3624
- const askQuestion = useCallback((question) => new Promise((resolve) => {
3932
+ const askQuestion = useCallback((question, signal) => new Promise((resolve) => {
3933
+ if (signal?.aborted) {
3934
+ resolve({ kind: 'abort' });
3935
+ return;
3936
+ }
3937
+ closeLiveReply();
3625
3938
  questionRef.current = { question, resolve };
3626
3939
  const values = [
3627
3940
  ...question.options.map((option) => option.id),
3628
3941
  ...(question.allowFreeText ? [FREE_TEXT_ANSWER] : []),
3629
3942
  ];
3630
- setSelectedChoice(0);
3631
- setChoicePicker({
3943
+ const picker = {
3632
3944
  kind: 'user-question',
3633
3945
  title: question.question,
3634
3946
  notice: question.header
@@ -3644,9 +3956,17 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3644
3956
  ? [{ label: 'Something else…', description: 'Answer in your own words' }]
3645
3957
  : []),
3646
3958
  ],
3647
- });
3959
+ };
3960
+ const onAbort = () => {
3961
+ if (choicePickerRef.current === picker)
3962
+ setChoicePicker(null);
3963
+ resolveQuestion({ kind: 'abort' });
3964
+ };
3965
+ signal?.addEventListener('abort', onAbort, { once: true });
3966
+ setSelectedChoice(0);
3967
+ setChoicePicker(picker);
3648
3968
  sendTerminalNotification({ kind: 'approval-required' });
3649
- }), [sendTerminalNotification, setChoicePicker, setSelectedChoice]);
3969
+ }), [closeLiveReply, sendTerminalNotification, setChoicePicker, setSelectedChoice, resolveQuestion]);
3650
3970
  /**
3651
3971
  * Grow this turn's open task block, or open one. The whole plan rides along
3652
3972
  * so the block shows the checklist as it stood after the operation; a
@@ -3670,6 +3990,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3670
3990
  // `st` carries the streaming-assistant bubble id + accumulated text across
3671
3991
  // events within a turn/stream.
3672
3992
  const applyEvent = useCallback((event, st) => {
3993
+ if (st.learning)
3994
+ observeTurnEvent(st.learning, event);
3995
+ if (event.kind === 'tool-start' && event.toolName === SKILL_TOOL_NAME) {
3996
+ conversationLearning().skillUsed = true;
3997
+ }
3673
3998
  const ensureAssistant = () => {
3674
3999
  if (!st.assistantId)
3675
4000
  st.assistantId = pushMessage('assistant', '', true);
@@ -3935,6 +4260,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3935
4260
  setState(activeToolsRef.current.length > 0 ? 'tool' : 'thinking');
3936
4261
  break;
3937
4262
  }
4263
+ if (event.cancelled) {
4264
+ // A No on the tool's own screen: the person's answer, not a failure.
4265
+ pushMessage('tool', done?.label ?? formatToolCall(event.toolName, event.summary), false, '○', undefined, theme.text.muted, event.durationMs !== undefined ? formatElapsed(event.durationMs) : undefined);
4266
+ pushMessage('tool', event.resultLabel ?? 'Cancelled', false, '⎿');
4267
+ setState(activeToolsRef.current.length > 0 ? 'tool' : 'thinking');
4268
+ break;
4269
+ }
3938
4270
  const catalogue = !event.isError && event.toolName === 'agent_models' && event.output !== undefined
3939
4271
  ? modelCatalogueView(event.output) : undefined;
3940
4272
  if (catalogue !== undefined) {
@@ -4091,6 +4423,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4091
4423
  if (closing)
4092
4424
  pushMessage('system', closing, false, '✻', undefined, theme.text.muted);
4093
4425
  }
4426
+ offerSkillSuggestion(st);
4094
4427
  break;
4095
4428
  }
4096
4429
  case 'paused':
@@ -4102,7 +4435,22 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4102
4435
  st.outcome = 'stopped';
4103
4436
  st.queuePauseOutcome = 'paused';
4104
4437
  st.notification = { kind: 'turn-settled', outcome: 'stopped' };
4105
- pushMessage('system', describeTurnInterruption(event), false, '‖');
4438
+ if (event.handoff) {
4439
+ // Nothing failed and nothing needs approving: a tool needs the
4440
+ // operator to do something first. Continuing is one key.
4441
+ const browser = browserControlRef.current;
4442
+ const browserStatus = browser?.status();
4443
+ const browserNotice = describeBrowserHandoff(event.handoff, browserStatus);
4444
+ // Without a window the sign-in happens in `namzu browser
4445
+ // login`, which needs the profile this session holds.
4446
+ if (browserNotice && browser && browserStatus?.headless)
4447
+ void browser.release();
4448
+ pushMessage('system', browserNotice ??
4449
+ `${describeTurnInterruption(event)}\nWhen that is done, press Enter to continue · Esc to stop.`, false, '‖');
4450
+ setHandoffPark({ turnId: event.turnId, reason: event.handoff.reason });
4451
+ }
4452
+ else
4453
+ pushMessage('system', describeTurnInterruption(event), false, '‖');
4106
4454
  break;
4107
4455
  case 'error':
4108
4456
  closeAssistant();
@@ -4115,7 +4463,16 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4115
4463
  st.notification = null;
4116
4464
  break;
4117
4465
  }
4118
- }, [appendToMessage, finalizeMessage, flushStream, pushMessage, writeTaskBlock]);
4466
+ }, [
4467
+ appendToMessage,
4468
+ conversationLearning,
4469
+ finalizeMessage,
4470
+ flushStream,
4471
+ offerSkillSuggestion,
4472
+ pushMessage,
4473
+ setHandoffPark,
4474
+ writeTaskBlock,
4475
+ ]);
4119
4476
  applyEventRef.current = applyEvent;
4120
4477
  const runTurn = useCallback(async (prompt) => {
4121
4478
  // A passive queue effect can still hold the previous render's session
@@ -4243,7 +4600,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4243
4600
  outcome: null,
4244
4601
  sessionId: destination,
4245
4602
  notification: null,
4603
+ ...(prompt.kind === 'human'
4604
+ ? {
4605
+ learning: createTurnActivity(),
4606
+ ...(prompt.skillFlow ? { skillFlow: true } : {}),
4607
+ startedInPlanMode: permissionModeRef.current === 'plan',
4608
+ }
4609
+ : {}),
4246
4610
  };
4611
+ liveStreamRef.current = st;
4612
+ // A skill activated with /skills <name> stays in the prompt of every
4613
+ // later conversation too; a turn run under one already used a skill.
4614
+ if (st.learning && activeSkills.length > 0)
4615
+ st.learning.skillUsed = true;
4247
4616
  const ac = new AbortController();
4248
4617
  const turnToken = {};
4249
4618
  let abnormalTerminal;
@@ -4819,6 +5188,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4819
5188
  pushMessage('system', `Conversation history is being ${operation}. Wait for it to finish before sending another command or prompt.`);
4820
5189
  return;
4821
5190
  }
5191
+ // The operator moved on; a parked turn is still there for /resume.
5192
+ setHandoffPark(null);
4822
5193
  setHistory((prev) => [...prev, value]);
4823
5194
  const selectionIntent = !attachments?.length ? parseModelSelectionIntent(value) : undefined;
4824
5195
  if (selectionIntent) {
@@ -4859,6 +5230,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4859
5230
  // including the queue — so a command-driven turn is not a second way
4860
5231
  // to run one.
4861
5232
  let outgoing = value;
5233
+ let skillFlow = false;
4862
5234
  const slash = runSlash(value, slashCtx, hostCommands);
4863
5235
  if (slash) {
4864
5236
  switch (slash.kind) {
@@ -5330,20 +5702,16 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5330
5702
  // distinction the headless commands get from `--cwd`. They are
5331
5703
  // the same value today, and were the same value in `exec --json`
5332
5704
  // too until they were not.
5333
- const skills = discoverSkills({ cwd: ctx.cwd });
5705
+ const skills = discoverSkills({
5706
+ cwd: ctx.cwd,
5707
+ ...(ctx.skills ? { config: ctx.skills } : {}),
5708
+ });
5334
5709
  if (skills.length === 0) {
5335
- pushMessage('system', 'No skills found. Add one at ~/.namzu/skills/<name>/SKILL.md or ./skills/<name>/SKILL.md.');
5710
+ pushMessage('system', NO_SKILLS_FOUND);
5336
5711
  return;
5337
5712
  }
5338
5713
  const activeNames = new Set(activeSkills.map((s) => s.name));
5339
- const lines = skills.map((s) =>
5340
- // A refused skill is shown with its reason rather than hidden.
5341
- // Dropping it silently would leave someone wondering where a
5342
- // file they can see on disk went.
5343
- s.problem
5344
- ? `! ${s.name} — ${s.problem}`
5345
- : `${activeNames.has(s.name) ? '● ' : '○ '}${s.name} — ${s.description}`);
5346
- pushMessage('system', `Skills (● active):\n ${lines.join('\n ')}`);
5714
+ pushMessage('system', renderSkillRoster(skills, activeNames));
5347
5715
  return;
5348
5716
  }
5349
5717
  case 'plugins': {
@@ -5372,9 +5740,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5372
5740
  return;
5373
5741
  }
5374
5742
  case 'skill-picker': {
5375
- const skills = discoverSkills({ cwd: ctx.cwd });
5743
+ const skills = discoverSkills({
5744
+ cwd: ctx.cwd,
5745
+ ...(ctx.skills ? { config: ctx.skills } : {}),
5746
+ });
5376
5747
  if (skills.length === 0) {
5377
- pushMessage('system', 'No skills found. Add one at ~/.namzu/skills/<name>/SKILL.md or ./skills/<name>/SKILL.md.');
5748
+ pushMessage('system', NO_SKILLS_FOUND);
5378
5749
  return;
5379
5750
  }
5380
5751
  const activeNames = new Set(activeSkills.map((skill) => skill.name));
@@ -5387,7 +5758,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5387
5758
  label: skill.name,
5388
5759
  description: skill.problem
5389
5760
  ? `Unavailable: ${skill.problem}`
5390
- : `${skill.description} · ${skill.source}`,
5761
+ : `${skill.description} · ${skill.source} (${skillTierLabel(skill.tier)})`,
5391
5762
  current: activeNames.has(skill.name),
5392
5763
  })),
5393
5764
  });
@@ -5397,6 +5768,46 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5397
5768
  activateSkill(slash.name);
5398
5769
  return;
5399
5770
  }
5771
+ case 'new-skill':
5772
+ case 'save-skill': {
5773
+ const creator = discoverSkills({
5774
+ cwd: ctx.cwd,
5775
+ ...(ctx.skills ? { config: ctx.skills } : {}),
5776
+ }).find((skill) => skill.name === SKILL_CREATOR_SKILL);
5777
+ if (!creator || creator.problem || creator.invocation === 'operator') {
5778
+ pushMessage('system', `The ${SKILL_CREATOR_SKILL} skill is not available to the model here${creator?.problem ? ` (${creator.problem})` : ''}. Turn the built-in skills back on (skills.builtin) or write a SKILL.md by hand under ~/.namzu/skills/<name>/.`);
5779
+ return;
5780
+ }
5781
+ // Falls through to the queue-or-send a typed message takes, as
5782
+ // `prompt` does below.
5783
+ skillFlow = true;
5784
+ if (slash.kind === 'new-skill')
5785
+ outgoing = newSkillPrompt(slash.idea);
5786
+ else {
5787
+ outgoing = learnSkillPrompt(slash.name);
5788
+ // The proposal was taken up: this conversation proposes no
5789
+ // more, and the ignored-in-a-row count starts again.
5790
+ conversationLearning().proposed = true;
5791
+ const home = resolveNamzuHome();
5792
+ const ledger = readSuggestionLedger(home);
5793
+ if (ledger.unanswered > 0)
5794
+ writeSuggestionLedger(home, { ...ledger, unanswered: 0 });
5795
+ }
5796
+ break;
5797
+ }
5798
+ case 'skill-suggestions': {
5799
+ // The session follows the choice whatever the files say; the
5800
+ // message says whether a later start will too.
5801
+ skillSuggestOverrideRef.current = slash.on;
5802
+ if (slash.on)
5803
+ writeSuggestionLedger(resolveNamzuHome(), FRESH_LEDGER);
5804
+ const profile = ctx.configDebug?.selectedProfile?.name;
5805
+ pushMessage('system', setSkillSuggestions(slash.on, {
5806
+ cwd: ctx.cwd,
5807
+ ...(profile ? { profile } : {}),
5808
+ }).message);
5809
+ return;
5810
+ }
5400
5811
  case 'resume':
5401
5812
  void doResume();
5402
5813
  return;
@@ -5457,6 +5868,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5457
5868
  case 'none':
5458
5869
  return;
5459
5870
  case 'host-command': {
5871
+ if (scheduleRef.current?.handleSlash(slash.name, slash.args))
5872
+ return;
5460
5873
  // Dispatched through the kernel's registry, built with what
5461
5874
  // THIS session can answer from. The descriptors used for
5462
5875
  // the merge above carry no store — they are names — so the
@@ -5761,6 +6174,21 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5761
6174
  runConversationExport({ kind: 'file', path: slash.path });
5762
6175
  return;
5763
6176
  }
6177
+ case 'browser': {
6178
+ const generation = conversationGenRef.current;
6179
+ void runBrowserSlash(session?.browser, slash.args, {
6180
+ busy: state !== 'idle' || abortRef.current !== null,
6181
+ }).then((answer) => {
6182
+ // A later session keeps the operator's choice: `/model`
6183
+ // rebuilds the session, and the profile goes with it.
6184
+ if (answer.profile)
6185
+ browserProfileRef.current = answer.profile;
6186
+ if (conversationGenRef.current !== generation)
6187
+ return;
6188
+ pushMessage('system', answer.text);
6189
+ });
6190
+ return;
6191
+ }
5764
6192
  default: {
5765
6193
  // Exhaustive on purpose. Without it a `SlashAction` kind added
5766
6194
  // and not handled here falls out of the switch into the send
@@ -5786,8 +6214,16 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5786
6214
  kind: 'human',
5787
6215
  text: outgoing,
5788
6216
  ...(attachments && attachments.length > 0 ? { attachments: [...attachments] } : {}),
6217
+ ...(skillFlow ? { skillFlow: true } : {}),
5789
6218
  };
5790
- if (mode === 'submit') {
6219
+ // `/skills save` and `/skills new` are turns of their own: steered into
6220
+ // a running turn, they would lose the flag that keeps the skill-making
6221
+ // turn from being proposed as a skill, and the model would read them
6222
+ // mid-task. They wait in the queue for the turn to end.
6223
+ if (skillFlow && activeTurnInboxRef.current) {
6224
+ pushMessage('system', 'Queued: this runs when the current turn ends.');
6225
+ }
6226
+ if (mode === 'submit' && !skillFlow) {
5791
6227
  const inbox = activeTurnInboxRef.current;
5792
6228
  if (inbox) {
5793
6229
  const expanded = expandFileMentions(outgoing, ctx.cwd);
@@ -5805,6 +6241,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5805
6241
  activeSkills,
5806
6242
  advanceQueueContinuation,
5807
6243
  applyPermissionMode,
6244
+ conversationLearning,
5808
6245
  applyReasoningEffort,
5809
6246
  appLifetime,
5810
6247
  ctx.cwd,
@@ -5841,6 +6278,40 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5841
6278
  // a ref so selecting a help row re-enters the one ordinary slash-command
5842
6279
  // path instead of growing a second command executor.
5843
6280
  commandPickerSubmitRef.current = handleSubmit;
6281
+ saveSkillNotifyRef.current = () => sendTerminalNotification({ kind: 'approval-required' });
6282
+ scheduleLiveRef.current = {
6283
+ ...(scheduleLiveRef.current?.model ? { model: scheduleLiveRef.current.model } : {}),
6284
+ say: (text) => {
6285
+ closeLiveReply();
6286
+ pushMessage('system', text);
6287
+ },
6288
+ ask: askQuestion,
6289
+ askPermission: onPermission,
6290
+ submit: (text) => handleSubmit(text),
6291
+ };
6292
+ // Scheduled work since the TUI last looked: one line, after the first
6293
+ // paint, never blocking it. Loops fire when a turn ends, and on their timer.
6294
+ useEffect(() => {
6295
+ const timer = setTimeout(() => {
6296
+ const line = scheduleRef.current?.startupLine();
6297
+ if (line)
6298
+ scheduleLiveRef.current?.say(line);
6299
+ }, 250);
6300
+ return () => {
6301
+ clearTimeout(timer);
6302
+ scheduleRef.current?.dispose();
6303
+ };
6304
+ }, []);
6305
+ useEffect(() => {
6306
+ if (state !== 'idle')
6307
+ return;
6308
+ try {
6309
+ scheduleRef.current?.loops.tick();
6310
+ }
6311
+ catch {
6312
+ // A loop that cannot be read never takes the screen down with it.
6313
+ }
6314
+ }, [state]);
5844
6315
  // Keep the footer on the same durable goal record the driver mutates. A ref
5845
6316
  // alone cannot repaint React, so every goal mutation bumps goalDriveVersion;
5846
6317
  // generation/session checks stop a late disk read labelling a resumed chat.
@@ -6149,6 +6620,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6149
6620
  useInput((input, key) => {
6150
6621
  if (outputViewer !== null)
6151
6622
  return;
6623
+ // The save-skill screen owns its keys, Esc and Ctrl+C included: neither
6624
+ // may interrupt the turn that is waiting on its answer. A permission
6625
+ // request (an agent's, say) takes the screen over it and gets the keys.
6626
+ if (saveSkillPromptRef.current && permission === null)
6627
+ return;
6152
6628
  // Startup has refused admission, so there is no draft or active turn to
6153
6629
  // protect with the ready screen's two-press exit ladder.
6154
6630
  if (phase === 'unhealthy') {
@@ -6262,7 +6738,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6262
6738
  }
6263
6739
  if (permission && (key.upArrow || key.downArrow)) {
6264
6740
  const current = permissionChoiceRef.current;
6265
- const next = key.upArrow ? Math.max(0, current - 1) : Math.min(2, current + 1);
6741
+ const last = permissionAnswers(permission.toolCalls, { batchOnly: permission.batchOnly === true }).length - 1;
6742
+ const next = key.upArrow ? Math.max(0, current - 1) : Math.min(last, current + 1);
6266
6743
  setPermissionChoice(next);
6267
6744
  return;
6268
6745
  }
@@ -6301,14 +6778,25 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6301
6778
  // the operator never looked at. Refusals above never wait.
6302
6779
  if (!approvalIsDeliberate(permissionOpenedAtRef.current, Date.now()))
6303
6780
  return;
6304
- const numbered = ch === '1' ? 0 : ch === '2' ? 1 : ch === '3' ? 2 : null;
6781
+ // The answers on screen, so a digit or Enter means what it is next
6782
+ // to: a batch-only prompt (a scheduled run) has no "allow all", and
6783
+ // neither its `a` nor a `3` answers it.
6784
+ const answers = permissionAnswers(permission?.toolCalls ?? [], {
6785
+ batchOnly: permission?.batchOnly === true,
6786
+ });
6787
+ const numbered = /^[1-9]$/.test(ch) ? Number(ch) - 1 : null;
6305
6788
  const chosen = key.return ? permissionChoiceRef.current : numbered;
6306
- if (ch === 'y' || chosen === 0)
6307
- resolvePermission({ kind: 'approve' });
6308
- else if (ch === 'a' || chosen === 1)
6309
- resolvePermission({ kind: 'approve-all' });
6310
- else if (chosen === 2)
6311
- resolvePermission({ kind: 'reject' });
6789
+ const kind = ch === 'y'
6790
+ ? 'approve'
6791
+ : ch === 'a'
6792
+ ? answers.some((answer) => answer.kind === 'approve-all')
6793
+ ? 'approve-all'
6794
+ : undefined
6795
+ : chosen !== null
6796
+ ? answers[chosen]?.kind
6797
+ : undefined;
6798
+ if (kind)
6799
+ resolvePermission({ kind });
6312
6800
  return;
6313
6801
  }
6314
6802
  // The active child count already advertises this key beside Working.
@@ -6614,6 +7102,21 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6614
7102
  resetTranscript();
6615
7103
  return;
6616
7104
  }
7105
+ // A turn a tool paused for a person: Enter on an empty composer
7106
+ // continues it, Esc stops it (the turn is abandoned, like /abandon).
7107
+ if (handoffParkRef.current &&
7108
+ phase === 'ready' &&
7109
+ state === 'idle' &&
7110
+ !abortRef.current &&
7111
+ ((key.return && !composerHasDraft) || key.escape)) {
7112
+ const parked = handoffParkRef.current;
7113
+ setHandoffPark(null);
7114
+ if (key.return)
7115
+ void resumeActiveTurn({ handoffReason: parked.reason });
7116
+ else
7117
+ void abandonActiveTurn('The operator stopped the turn a tool paused for them.');
7118
+ return;
7119
+ }
6617
7120
  // Esc interrupts a running turn (Ctrl+C stays reserved for exit). Mirrors
6618
7121
  // the Ctrl+C interrupt path: abort, drop the queue, one "Interrupted." line.
6619
7122
  if (key.escape && (abortRef.current || pendingModelSwitchRef.current)) {
@@ -6738,29 +7241,33 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6738
7241
  ? 'starting a fresh conversation — input is paused'
6739
7242
  : compacting
6740
7243
  ? 'compacting conversation — input is paused'
6741
- : permission
6742
- ? hintForPhase(phase, state, session?.hasProvider === true)
6743
- : agentSurface?.kind === 'cockpit'
6744
- ? agentSurface.focus === 'workflows'
6745
- ? 'workflows — enter select · esc return'
6746
- : agentSurface.focus === 'phases'
6747
- ? 'agent phases — enter agents · esc return'
6748
- : 'agents — enter inspect · left phases · esc return'
6749
- : agentSurface?.kind === 'transcript'
6750
- ? 'observing agent — esc agents · q parent'
6751
- : textPrompt
6752
- ? 'editor open · enter apply · esc cancel'
6753
- : choicePicker
6754
- ? choicePickerSearchable(choicePicker)
6755
- ? 'type to filter · ↑↓ select · enter apply · esc back'
6756
- : effortSlider
6757
- ? '←/→ adjust · 1–9 select · enter apply · esc back'
6758
- : '↑↓ / 1–9 select · enter apply · esc back'
6759
- : copyPicker
6760
- ? 'copy target open — ↑↓ / 1–9 select · esc cancel'
6761
- : goalStatus && phase === 'ready' && state === 'idle'
6762
- ? undefined
6763
- : hintForPhase(phase, state, session?.hasProvider === true, composerHasDraft);
7244
+ : saveSkillPrompt
7245
+ ? 'save skill — ←/→ choose · enter apply · esc cancel'
7246
+ : permission
7247
+ ? hintForPhase(phase, state, session?.hasProvider === true)
7248
+ : agentSurface?.kind === 'cockpit'
7249
+ ? agentSurface.focus === 'workflows'
7250
+ ? 'workflows — enter select · esc return'
7251
+ : agentSurface.focus === 'phases'
7252
+ ? 'agent phases — enter agents · esc return'
7253
+ : 'agents — enter inspect · left phases · esc return'
7254
+ : agentSurface?.kind === 'transcript'
7255
+ ? 'observing agent — esc agents · q parent'
7256
+ : textPrompt
7257
+ ? 'editor open · enter apply · esc cancel'
7258
+ : choicePicker
7259
+ ? choicePickerSearchable(choicePicker)
7260
+ ? 'type to filter · ↑↓ select · enter apply · esc back'
7261
+ : effortSlider
7262
+ ? '←/→ adjust · 1–9 select · enter apply · esc back'
7263
+ : '↑↓ / 1–9 select · enter apply · esc back'
7264
+ : copyPicker
7265
+ ? 'copy target open — ↑↓ / 1–9 select · esc cancel'
7266
+ : handoffPark && phase === 'ready' && state === 'idle' && !composerHasDraft
7267
+ ? 'enter continue · esc stop'
7268
+ : goalStatus && phase === 'ready' && state === 'idle'
7269
+ ? undefined
7270
+ : hintForPhase(phase, state, session?.hasProvider === true, composerHasDraft);
6764
7271
  const permissionSource = reviewSource(permission, scopeRef.current?.sessionId);
6765
7272
  const permissionOwner = permissionSource
6766
7273
  ? subagents.find((agent) => agent.sessionId === permissionSource)
@@ -6799,7 +7306,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6799
7306
  // where it is decided rather than guessed on the screen: with no
6800
7307
  // session behind the picker, esc exits namzu and the footer says
6801
7308
  // so instead of offering a cancel nobody will see.
6802
- cancelExits: !session?.hasProvider })) : (_jsxs(_Fragment, { children: [agentSurface === null && outputViewer === null ? (_jsx(LiveActivity, { compact: compactWork, activeTools: visibleActiveTools, working: state === 'thinking' || state === 'tool', interruptible: abortRef.current !== null, animate: stdout.isTTY === true && permission === null && textPrompt === null, thinking: thinking, tokens: turnTokens })) : null, agentSurface === null && outputViewer === null && permission === null ? (_jsx(TaskList, { tasks: liveTasks })) : null, permission ? (_jsx(PermissionOverlay, { toolCalls: permission.toolCalls, review: permission.review, summary: permission.summary, detailsOpen: permissionDetailsOpen, reviewOffset: permissionReviewOffset, choice: permissionChoice, queuedCount: queuedPermissionCount, sourceLabel: permissionSourceLabel, columns: terminal.columns, rows: terminal.rows })) : null, textPrompt ? (_jsx(TextPrompt, { columns: Math.max(1, (terminal.columns ?? 80) - 2), title: textPrompt.title, placeholder: textPrompt.placeholder, initialValue: textPrompt.initialValue, emptyNotice: textPrompt.emptyNotice, hidden: permission !== null || (agentSurface !== null || outputViewer !== null), onSubmit: submitTextPrompt, onCancel: cancelTextPrompt }, textPrompt.token)) : permission === null &&
7309
+ cancelExits: !session?.hasProvider })) : (_jsxs(_Fragment, { children: [agentSurface === null && outputViewer === null ? (_jsx(LiveActivity, { compact: compactWork, activeTools: visibleActiveTools, working: state === 'thinking' || state === 'tool', interruptible: abortRef.current !== null, animate: stdout.isTTY === true &&
7310
+ permission === null &&
7311
+ textPrompt === null &&
7312
+ saveSkillPrompt === null, thinking: thinking, tokens: turnTokens, waitingForYou: choicePicker !== null ||
7313
+ permission !== null ||
7314
+ textPrompt !== null ||
7315
+ saveSkillPrompt !== null })) : null, agentSurface === null && outputViewer === null && permission === null ? (_jsx(TaskList, { tasks: liveTasks })) : null, permission ? (_jsx(PermissionOverlay, { toolCalls: permission.toolCalls, review: permission.review, summary: permission.summary, detailsOpen: permissionDetailsOpen, reviewOffset: permissionReviewOffset, choice: permissionChoice, queuedCount: queuedPermissionCount, sourceLabel: permissionSourceLabel, columns: terminal.columns, rows: terminal.rows, batchOnly: permission.batchOnly === true, siteNotes: permission.siteNotes })) : null, saveSkillPrompt && permission === null ? (_jsx(SaveSkillOverlay, { request: saveSkillPrompt.request, cwd: ctx.cwd, columns: Math.max(1, (terminal.columns ?? 80) - 2), rows: terminal.rows, onAnswer: saveSkillPrompt.resolve }, saveSkillPrompt.request.markdown)) : null, textPrompt ? (_jsx(TextPrompt, { columns: Math.max(1, (terminal.columns ?? 80) - 2), title: textPrompt.title, placeholder: textPrompt.placeholder, initialValue: textPrompt.initialValue, emptyNotice: textPrompt.emptyNotice, hidden: permission !== null || (agentSurface !== null || outputViewer !== null), onSubmit: submitTextPrompt, onCancel: cancelTextPrompt }, textPrompt.token)) : permission === null &&
6803
7316
  agentSurface === null &&
6804
7317
  outputViewer === null &&
6805
7318
  choicePicker?.kind === 'reasoning-effort' &&
@@ -6811,7 +7324,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6811
7324
  textPrompt === null &&
6812
7325
  choicePicker === null &&
6813
7326
  copyPicker === null &&
7327
+ saveSkillPrompt === null &&
6814
7328
  agentSurface === null && outputViewer === null, hidden: permission !== null ||
7329
+ saveSkillPrompt !== null ||
6815
7330
  textPrompt !== null ||
6816
7331
  choicePicker !== null ||
6817
7332
  copyPicker !== null ||
@@ -6836,14 +7351,18 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6836
7351
  conversationMutation !== null ||
6837
7352
  externalEditorRequest !== null ||
6838
7353
  (agentSurface !== null || outputViewer !== null), hidden: permission !== null ||
7354
+ saveSkillPrompt !== null ||
6839
7355
  textPrompt !== null ||
6840
7356
  choicePicker !== null ||
6841
7357
  copyPicker !== null ||
6842
7358
  (agentSurface !== null || outputViewer !== null),
6843
7359
  // A turn is running, so Esc is the interrupt and not
6844
7360
  // the composer's clear.
6845
- escapeInterrupts: !compacting &&
6846
- (state === 'thinking' || state === 'tool' || pendingModelSwitchRef.current !== null), onSubmit: handleSubmit, onNotice: (text) => pushMessage('system', text), onStepReasoningEffort: stepReasoningEffort, onExternalEdit: requestExternalEditor, onEditPrevious: openPromptEditor, draftToRestore: composerDraft, onDraftRestored: (token) => setComposerDraft((draft) => (draft?.token === token ? null : draft)), onDraftPresenceChange: setComposerHasDraft, onOpenAgentPanel: openAgentCockpit, userCommands: userCommands, builtins: hostCommands.map((command) => ({
7361
+ escapeInterrupts: (!compacting &&
7362
+ (state === 'thinking' ||
7363
+ state === 'tool' ||
7364
+ pendingModelSwitchRef.current !== null)) ||
7365
+ (handoffPark !== null && state === 'idle'), onSubmit: handleSubmit, onNotice: (text) => pushMessage('system', text), onStepReasoningEffort: stepReasoningEffort, onExternalEdit: requestExternalEditor, onEditPrevious: openPromptEditor, draftToRestore: composerDraft, onDraftRestored: (token) => setComposerDraft((draft) => (draft?.token === token ? null : draft)), onDraftPresenceChange: setComposerHasDraft, onOpenAgentPanel: openAgentCockpit, userCommands: userCommands, builtins: hostCommands.map((command) => ({
6847
7366
  ...command,
6848
7367
  description: command.unavailable?.(slashCtx) ?? command.description,
6849
7368
  })), mentionCandidates: mentionCandidates, history: history })] })] })), _jsx(StatusBar, { cwd: ctx.cwd, provider: session?.providerSummary ?? null, model: session?.modelSummary ?? null, effort: reasoningEffort, orchestrate: orchestrateMode, goal: statusGoal, state: state, hint: statusHint, permissionMode: displayedPermissionMode, canCycleMode: true }), showComposerSurface &&