@aitne/daemon 0.1.7 → 0.1.8

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 (959) hide show
  1. package/dist/adapters/dashboard-adapter.d.ts +18 -2
  2. package/dist/adapters/dashboard-adapter.d.ts.map +1 -1
  3. package/dist/adapters/dashboard-adapter.js +101 -51
  4. package/dist/adapters/dashboard-adapter.js.map +1 -1
  5. package/dist/adapters/discord.d.ts +8 -0
  6. package/dist/adapters/discord.d.ts.map +1 -1
  7. package/dist/adapters/discord.js +100 -21
  8. package/dist/adapters/discord.js.map +1 -1
  9. package/dist/adapters/message-hub.d.ts.map +1 -1
  10. package/dist/adapters/message-hub.js +7 -1
  11. package/dist/adapters/message-hub.js.map +1 -1
  12. package/dist/adapters/notification-manager.d.ts +102 -2
  13. package/dist/adapters/notification-manager.d.ts.map +1 -1
  14. package/dist/adapters/notification-manager.js +228 -8
  15. package/dist/adapters/notification-manager.js.map +1 -1
  16. package/dist/adapters/outbound-text.d.ts +16 -0
  17. package/dist/adapters/outbound-text.d.ts.map +1 -1
  18. package/dist/adapters/outbound-text.js +118 -1
  19. package/dist/adapters/outbound-text.js.map +1 -1
  20. package/dist/adapters/primary-platform-resolver.d.ts +69 -0
  21. package/dist/adapters/primary-platform-resolver.d.ts.map +1 -0
  22. package/dist/adapters/primary-platform-resolver.js +55 -0
  23. package/dist/adapters/primary-platform-resolver.js.map +1 -0
  24. package/dist/adapters/slack-adapter.d.ts +28 -0
  25. package/dist/adapters/slack-adapter.d.ts.map +1 -1
  26. package/dist/adapters/slack-adapter.js +134 -35
  27. package/dist/adapters/slack-adapter.js.map +1 -1
  28. package/dist/adapters/telegram-adapter.d.ts +7 -1
  29. package/dist/adapters/telegram-adapter.d.ts.map +1 -1
  30. package/dist/adapters/telegram-adapter.js +49 -18
  31. package/dist/adapters/telegram-adapter.js.map +1 -1
  32. package/dist/adapters/whatsapp-adapter.d.ts +33 -0
  33. package/dist/adapters/whatsapp-adapter.d.ts.map +1 -1
  34. package/dist/adapters/whatsapp-adapter.js +84 -0
  35. package/dist/adapters/whatsapp-adapter.js.map +1 -1
  36. package/dist/api/directory-picker.d.ts.map +1 -1
  37. package/dist/api/directory-picker.js +14 -0
  38. package/dist/api/directory-picker.js.map +1 -1
  39. package/dist/api/env-writer.d.ts.map +1 -1
  40. package/dist/api/env-writer.js +6 -1
  41. package/dist/api/env-writer.js.map +1 -1
  42. package/dist/api/helpers/agent-errors.d.ts +2600 -0
  43. package/dist/api/helpers/agent-errors.d.ts.map +1 -0
  44. package/dist/api/helpers/agent-errors.js +2506 -0
  45. package/dist/api/helpers/agent-errors.js.map +1 -0
  46. package/dist/api/integration-route-gate.d.ts.map +1 -1
  47. package/dist/api/integration-route-gate.js +10 -3
  48. package/dist/api/integration-route-gate.js.map +1 -1
  49. package/dist/api/routes/agent-schedule-plan-match.d.ts +5 -0
  50. package/dist/api/routes/agent-schedule-plan-match.d.ts.map +1 -0
  51. package/dist/api/routes/agent-schedule-plan-match.js +101 -0
  52. package/dist/api/routes/agent-schedule-plan-match.js.map +1 -0
  53. package/dist/api/routes/agent-schedule.d.ts +4 -0
  54. package/dist/api/routes/agent-schedule.d.ts.map +1 -0
  55. package/dist/api/routes/agent-schedule.js +750 -0
  56. package/dist/api/routes/agent-schedule.js.map +1 -0
  57. package/dist/api/routes/agent.d.ts.map +1 -1
  58. package/dist/api/routes/agent.js +209 -366
  59. package/dist/api/routes/agent.js.map +1 -1
  60. package/dist/api/routes/apple-calendar.d.ts.map +1 -1
  61. package/dist/api/routes/apple-calendar.js +109 -27
  62. package/dist/api/routes/apple-calendar.js.map +1 -1
  63. package/dist/api/routes/attachments.d.ts.map +1 -1
  64. package/dist/api/routes/attachments.js +113 -21
  65. package/dist/api/routes/attachments.js.map +1 -1
  66. package/dist/api/routes/backends.d.ts.map +1 -1
  67. package/dist/api/routes/backends.js +100 -4
  68. package/dist/api/routes/backends.js.map +1 -1
  69. package/dist/api/routes/books.d.ts.map +1 -1
  70. package/dist/api/routes/books.js +58 -18
  71. package/dist/api/routes/books.js.map +1 -1
  72. package/dist/api/routes/calendar.d.ts +3 -2
  73. package/dist/api/routes/calendar.d.ts.map +1 -1
  74. package/dist/api/routes/calendar.js +330 -55
  75. package/dist/api/routes/calendar.js.map +1 -1
  76. package/dist/api/routes/commands.d.ts.map +1 -1
  77. package/dist/api/routes/commands.js +20 -1
  78. package/dist/api/routes/commands.js.map +1 -1
  79. package/dist/api/routes/context/index.d.ts +33 -0
  80. package/dist/api/routes/context/index.d.ts.map +1 -0
  81. package/dist/api/routes/context/index.js +193 -0
  82. package/dist/api/routes/context/index.js.map +1 -0
  83. package/dist/api/routes/context/locks.d.ts +4 -0
  84. package/dist/api/routes/context/locks.d.ts.map +1 -0
  85. package/dist/api/routes/context/locks.js +136 -0
  86. package/dist/api/routes/context/locks.js.map +1 -0
  87. package/dist/api/routes/context/path-resolve.d.ts +15 -0
  88. package/dist/api/routes/context/path-resolve.d.ts.map +1 -0
  89. package/dist/api/routes/context/path-resolve.js +109 -0
  90. package/dist/api/routes/context/path-resolve.js.map +1 -0
  91. package/dist/api/routes/context/permissions.d.ts +35 -0
  92. package/dist/api/routes/context/permissions.d.ts.map +1 -0
  93. package/dist/api/routes/context/permissions.js +192 -0
  94. package/dist/api/routes/context/permissions.js.map +1 -0
  95. package/dist/api/routes/context/read.d.ts +4 -0
  96. package/dist/api/routes/context/read.d.ts.map +1 -0
  97. package/dist/api/routes/context/read.js +295 -0
  98. package/dist/api/routes/context/read.js.map +1 -0
  99. package/dist/api/routes/context/repair.d.ts +4 -0
  100. package/dist/api/routes/context/repair.d.ts.map +1 -0
  101. package/dist/api/routes/context/repair.js +114 -0
  102. package/dist/api/routes/context/repair.js.map +1 -0
  103. package/dist/api/routes/context/snapshots.d.ts +4 -0
  104. package/dist/api/routes/context/snapshots.d.ts.map +1 -0
  105. package/dist/api/routes/context/snapshots.js +177 -0
  106. package/dist/api/routes/context/snapshots.js.map +1 -0
  107. package/dist/api/routes/context/write.d.ts +4 -0
  108. package/dist/api/routes/context/write.d.ts.map +1 -0
  109. package/dist/api/routes/context/write.js +570 -0
  110. package/dist/api/routes/context/write.js.map +1 -0
  111. package/dist/api/routes/context.d.ts +2 -43
  112. package/dist/api/routes/context.d.ts.map +1 -1
  113. package/dist/api/routes/context.js +415 -558
  114. package/dist/api/routes/context.js.map +1 -1
  115. package/dist/api/routes/dashboard/config.d.ts +4 -0
  116. package/dist/api/routes/dashboard/config.d.ts.map +1 -0
  117. package/dist/api/routes/dashboard/config.js +499 -0
  118. package/dist/api/routes/dashboard/config.js.map +1 -0
  119. package/dist/api/routes/dashboard/conversations.d.ts +4 -0
  120. package/dist/api/routes/dashboard/conversations.d.ts.map +1 -0
  121. package/dist/api/routes/dashboard/conversations.js +309 -0
  122. package/dist/api/routes/dashboard/conversations.js.map +1 -0
  123. package/dist/api/routes/dashboard/cost-approvals.d.ts +29 -0
  124. package/dist/api/routes/dashboard/cost-approvals.d.ts.map +1 -0
  125. package/dist/api/routes/dashboard/cost-approvals.js +259 -0
  126. package/dist/api/routes/dashboard/cost-approvals.js.map +1 -0
  127. package/dist/api/routes/dashboard/index.d.ts +27 -0
  128. package/dist/api/routes/dashboard/index.d.ts.map +1 -0
  129. package/dist/api/routes/dashboard/index.js +47 -0
  130. package/dist/api/routes/dashboard/index.js.map +1 -0
  131. package/dist/api/routes/dashboard/messaging.d.ts +4 -0
  132. package/dist/api/routes/dashboard/messaging.d.ts.map +1 -0
  133. package/dist/api/routes/dashboard/messaging.js +351 -0
  134. package/dist/api/routes/dashboard/messaging.js.map +1 -0
  135. package/dist/api/routes/dashboard/notifications.d.ts +4 -0
  136. package/dist/api/routes/dashboard/notifications.d.ts.map +1 -0
  137. package/dist/api/routes/dashboard/notifications.js +109 -0
  138. package/dist/api/routes/dashboard/notifications.js.map +1 -0
  139. package/dist/api/routes/dashboard/oauth-google.d.ts +4 -0
  140. package/dist/api/routes/dashboard/oauth-google.d.ts.map +1 -0
  141. package/dist/api/routes/dashboard/oauth-google.js +293 -0
  142. package/dist/api/routes/dashboard/oauth-google.js.map +1 -0
  143. package/dist/api/routes/dashboard/schedule-readonly.d.ts +4 -0
  144. package/dist/api/routes/dashboard/schedule-readonly.d.ts.map +1 -0
  145. package/dist/api/routes/dashboard/schedule-readonly.js +46 -0
  146. package/dist/api/routes/dashboard/schedule-readonly.js.map +1 -0
  147. package/dist/api/routes/dashboard/secrets.d.ts +24 -0
  148. package/dist/api/routes/dashboard/secrets.d.ts.map +1 -0
  149. package/dist/api/routes/dashboard/secrets.js +307 -0
  150. package/dist/api/routes/dashboard/secrets.js.map +1 -0
  151. package/dist/api/routes/dashboard/snapshots.d.ts +4 -0
  152. package/dist/api/routes/dashboard/snapshots.d.ts.map +1 -0
  153. package/dist/api/routes/dashboard/snapshots.js +33 -0
  154. package/dist/api/routes/dashboard/snapshots.js.map +1 -0
  155. package/dist/api/routes/dashboard.d.ts.map +1 -1
  156. package/dist/api/routes/dashboard.js +20 -12
  157. package/dist/api/routes/dashboard.js.map +1 -1
  158. package/dist/api/routes/delegated.d.ts +5 -4
  159. package/dist/api/routes/delegated.d.ts.map +1 -1
  160. package/dist/api/routes/delegated.js +6 -5
  161. package/dist/api/routes/delegated.js.map +1 -1
  162. package/dist/api/routes/docs.d.ts.map +1 -1
  163. package/dist/api/routes/docs.js +72 -9
  164. package/dist/api/routes/docs.js.map +1 -1
  165. package/dist/api/routes/entities.d.ts.map +1 -1
  166. package/dist/api/routes/entities.js +112 -43
  167. package/dist/api/routes/entities.js.map +1 -1
  168. package/dist/api/routes/fs.d.ts.map +1 -1
  169. package/dist/api/routes/fs.js +27 -12
  170. package/dist/api/routes/fs.js.map +1 -1
  171. package/dist/api/routes/git-templates.d.ts.map +1 -1
  172. package/dist/api/routes/git-templates.js +107 -27
  173. package/dist/api/routes/git-templates.js.map +1 -1
  174. package/dist/api/routes/git.d.ts.map +1 -1
  175. package/dist/api/routes/git.js +55 -11
  176. package/dist/api/routes/git.js.map +1 -1
  177. package/dist/api/routes/github.d.ts.map +1 -1
  178. package/dist/api/routes/github.js +110 -17
  179. package/dist/api/routes/github.js.map +1 -1
  180. package/dist/api/routes/integrations/crud-patch.d.ts +17 -0
  181. package/dist/api/routes/integrations/crud-patch.d.ts.map +1 -0
  182. package/dist/api/routes/integrations/crud-patch.js +600 -0
  183. package/dist/api/routes/integrations/crud-patch.js.map +1 -0
  184. package/dist/api/routes/integrations/crud.d.ts +16 -0
  185. package/dist/api/routes/integrations/crud.d.ts.map +1 -0
  186. package/dist/api/routes/integrations/crud.js +158 -0
  187. package/dist/api/routes/integrations/crud.js.map +1 -0
  188. package/dist/api/routes/integrations/exec.d.ts +23 -0
  189. package/dist/api/routes/integrations/exec.d.ts.map +1 -0
  190. package/dist/api/routes/integrations/exec.js +356 -0
  191. package/dist/api/routes/integrations/exec.js.map +1 -0
  192. package/dist/api/routes/integrations/index.d.ts +62 -0
  193. package/dist/api/routes/integrations/index.d.ts.map +1 -0
  194. package/dist/api/routes/integrations/index.js +70 -0
  195. package/dist/api/routes/integrations/index.js.map +1 -0
  196. package/dist/api/routes/integrations/integrations/crud.d.ts +16 -0
  197. package/dist/api/routes/integrations/integrations/crud.d.ts.map +1 -0
  198. package/dist/api/routes/integrations/integrations/crud.js +158 -0
  199. package/dist/api/routes/integrations/integrations/crud.js.map +1 -0
  200. package/dist/api/routes/integrations/integrations/index.d.ts +55 -0
  201. package/dist/api/routes/integrations/integrations/index.d.ts.map +1 -0
  202. package/dist/api/routes/integrations/integrations/index.js +65 -0
  203. package/dist/api/routes/integrations/integrations/index.js.map +1 -0
  204. package/dist/api/routes/integrations/integrations/invoke.d.ts +38 -0
  205. package/dist/api/routes/integrations/integrations/invoke.d.ts.map +1 -0
  206. package/dist/api/routes/integrations/integrations/invoke.js +320 -0
  207. package/dist/api/routes/integrations/integrations/invoke.js.map +1 -0
  208. package/dist/api/routes/integrations/integrations/probe.d.ts +21 -0
  209. package/dist/api/routes/integrations/integrations/probe.d.ts.map +1 -0
  210. package/dist/api/routes/integrations/integrations/probe.js +247 -0
  211. package/dist/api/routes/integrations/integrations/probe.js.map +1 -0
  212. package/dist/api/routes/integrations/invoke.d.ts +38 -0
  213. package/dist/api/routes/integrations/invoke.d.ts.map +1 -0
  214. package/dist/api/routes/integrations/invoke.js +320 -0
  215. package/dist/api/routes/integrations/invoke.js.map +1 -0
  216. package/dist/api/routes/integrations/probe.d.ts +21 -0
  217. package/dist/api/routes/integrations/probe.d.ts.map +1 -0
  218. package/dist/api/routes/integrations/probe.js +247 -0
  219. package/dist/api/routes/integrations/probe.js.map +1 -0
  220. package/dist/api/routes/integrations.d.ts.map +1 -1
  221. package/dist/api/routes/integrations.js +65 -13
  222. package/dist/api/routes/integrations.js.map +1 -1
  223. package/dist/api/routes/knowledge.d.ts.map +1 -1
  224. package/dist/api/routes/knowledge.js +13 -2
  225. package/dist/api/routes/knowledge.js.map +1 -1
  226. package/dist/api/routes/mail/_pa_wip_mail/app-password.d.ts +4 -0
  227. package/dist/api/routes/mail/_pa_wip_mail/app-password.d.ts.map +1 -0
  228. package/dist/api/routes/mail/_pa_wip_mail/app-password.js +192 -0
  229. package/dist/api/routes/mail/_pa_wip_mail/app-password.js.map +1 -0
  230. package/dist/api/routes/mail/_pa_wip_mail/body-helpers.d.ts +55 -0
  231. package/dist/api/routes/mail/_pa_wip_mail/body-helpers.d.ts.map +1 -0
  232. package/dist/api/routes/mail/_pa_wip_mail/body-helpers.js +91 -0
  233. package/dist/api/routes/mail/_pa_wip_mail/body-helpers.js.map +1 -0
  234. package/dist/api/routes/mail/_pa_wip_mail/dependencies.d.ts +36 -0
  235. package/dist/api/routes/mail/_pa_wip_mail/dependencies.d.ts.map +1 -0
  236. package/dist/api/routes/mail/_pa_wip_mail/dependencies.js +2 -0
  237. package/dist/api/routes/mail/_pa_wip_mail/dependencies.js.map +1 -0
  238. package/dist/api/routes/mail/_pa_wip_mail/gating.d.ts +45 -0
  239. package/dist/api/routes/mail/_pa_wip_mail/gating.d.ts.map +1 -0
  240. package/dist/api/routes/mail/_pa_wip_mail/gating.js +98 -0
  241. package/dist/api/routes/mail/_pa_wip_mail/gating.js.map +1 -0
  242. package/dist/api/routes/mail/_pa_wip_mail/mail/accounts.d.ts +4 -0
  243. package/dist/api/routes/mail/_pa_wip_mail/mail/accounts.d.ts.map +1 -0
  244. package/dist/api/routes/mail/_pa_wip_mail/mail/accounts.js +289 -0
  245. package/dist/api/routes/mail/_pa_wip_mail/mail/accounts.js.map +1 -0
  246. package/dist/api/routes/mail/_pa_wip_mail/mail/app-password.d.ts +4 -0
  247. package/dist/api/routes/mail/_pa_wip_mail/mail/app-password.d.ts.map +1 -0
  248. package/dist/api/routes/mail/_pa_wip_mail/mail/app-password.js +192 -0
  249. package/dist/api/routes/mail/_pa_wip_mail/mail/app-password.js.map +1 -0
  250. package/dist/api/routes/mail/_pa_wip_mail/mail/body-helpers.d.ts +55 -0
  251. package/dist/api/routes/mail/_pa_wip_mail/mail/body-helpers.d.ts.map +1 -0
  252. package/dist/api/routes/mail/_pa_wip_mail/mail/body-helpers.js +91 -0
  253. package/dist/api/routes/mail/_pa_wip_mail/mail/body-helpers.js.map +1 -0
  254. package/dist/api/routes/mail/_pa_wip_mail/mail/dependencies.d.ts +36 -0
  255. package/dist/api/routes/mail/_pa_wip_mail/mail/dependencies.d.ts.map +1 -0
  256. package/dist/api/routes/mail/_pa_wip_mail/mail/dependencies.js +2 -0
  257. package/dist/api/routes/mail/_pa_wip_mail/mail/dependencies.js.map +1 -0
  258. package/dist/api/routes/mail/_pa_wip_mail/mail/drafts.d.ts +5 -0
  259. package/dist/api/routes/mail/_pa_wip_mail/mail/drafts.d.ts.map +1 -0
  260. package/dist/api/routes/mail/_pa_wip_mail/mail/drafts.js +139 -0
  261. package/dist/api/routes/mail/_pa_wip_mail/mail/drafts.js.map +1 -0
  262. package/dist/api/routes/mail/_pa_wip_mail/mail/gating.d.ts +45 -0
  263. package/dist/api/routes/mail/_pa_wip_mail/mail/gating.d.ts.map +1 -0
  264. package/dist/api/routes/mail/_pa_wip_mail/mail/gating.js +98 -0
  265. package/dist/api/routes/mail/_pa_wip_mail/mail/gating.js.map +1 -0
  266. package/dist/api/routes/mail/_pa_wip_mail/mail/index.d.ts +18 -0
  267. package/dist/api/routes/mail/_pa_wip_mail/mail/index.d.ts.map +1 -0
  268. package/dist/api/routes/mail/_pa_wip_mail/mail/index.js +40 -0
  269. package/dist/api/routes/mail/_pa_wip_mail/mail/index.js.map +1 -0
  270. package/dist/api/routes/mail/_pa_wip_mail/mail/messages.d.ts +15 -0
  271. package/dist/api/routes/mail/_pa_wip_mail/mail/messages.d.ts.map +1 -0
  272. package/dist/api/routes/mail/_pa_wip_mail/mail/messages.js +239 -0
  273. package/dist/api/routes/mail/_pa_wip_mail/mail/messages.js.map +1 -0
  274. package/dist/api/routes/mail/_pa_wip_mail/mail/outlook-config.d.ts +4 -0
  275. package/dist/api/routes/mail/_pa_wip_mail/mail/outlook-config.d.ts.map +1 -0
  276. package/dist/api/routes/mail/_pa_wip_mail/mail/outlook-config.js +73 -0
  277. package/dist/api/routes/mail/_pa_wip_mail/mail/outlook-config.js.map +1 -0
  278. package/dist/api/routes/mail/_pa_wip_mail/mail/provider-resolver.d.ts +64 -0
  279. package/dist/api/routes/mail/_pa_wip_mail/mail/provider-resolver.d.ts.map +1 -0
  280. package/dist/api/routes/mail/_pa_wip_mail/mail/provider-resolver.js +286 -0
  281. package/dist/api/routes/mail/_pa_wip_mail/mail/provider-resolver.js.map +1 -0
  282. package/dist/api/routes/mail/_pa_wip_mail/mail/providers.d.ts +4 -0
  283. package/dist/api/routes/mail/_pa_wip_mail/mail/providers.d.ts.map +1 -0
  284. package/dist/api/routes/mail/_pa_wip_mail/mail/providers.js +73 -0
  285. package/dist/api/routes/mail/_pa_wip_mail/mail/providers.js.map +1 -0
  286. package/dist/api/routes/mail/_pa_wip_mail/mail/tags-folders.d.ts +5 -0
  287. package/dist/api/routes/mail/_pa_wip_mail/mail/tags-folders.d.ts.map +1 -0
  288. package/dist/api/routes/mail/_pa_wip_mail/mail/tags-folders.js +35 -0
  289. package/dist/api/routes/mail/_pa_wip_mail/mail/tags-folders.js.map +1 -0
  290. package/dist/api/routes/mail/_pa_wip_mail/mail/validators.d.ts +23 -0
  291. package/dist/api/routes/mail/_pa_wip_mail/mail/validators.d.ts.map +1 -0
  292. package/dist/api/routes/mail/_pa_wip_mail/mail/validators.js +131 -0
  293. package/dist/api/routes/mail/_pa_wip_mail/mail/validators.js.map +1 -0
  294. package/dist/api/routes/mail/_pa_wip_mail/provider-resolver.d.ts +64 -0
  295. package/dist/api/routes/mail/_pa_wip_mail/provider-resolver.d.ts.map +1 -0
  296. package/dist/api/routes/mail/_pa_wip_mail/provider-resolver.js +286 -0
  297. package/dist/api/routes/mail/_pa_wip_mail/provider-resolver.js.map +1 -0
  298. package/dist/api/routes/mail/_pa_wip_mail/search-health.d.ts +4 -0
  299. package/dist/api/routes/mail/_pa_wip_mail/search-health.d.ts.map +1 -0
  300. package/dist/api/routes/mail/_pa_wip_mail/search-health.js +131 -0
  301. package/dist/api/routes/mail/_pa_wip_mail/search-health.js.map +1 -0
  302. package/dist/api/routes/mail/_pa_wip_mail/validators.d.ts +23 -0
  303. package/dist/api/routes/mail/_pa_wip_mail/validators.d.ts.map +1 -0
  304. package/dist/api/routes/mail/_pa_wip_mail/validators.js +131 -0
  305. package/dist/api/routes/mail/_pa_wip_mail/validators.js.map +1 -0
  306. package/dist/api/routes/mail/accounts.d.ts +4 -0
  307. package/dist/api/routes/mail/accounts.d.ts.map +1 -0
  308. package/dist/api/routes/mail/accounts.js +289 -0
  309. package/dist/api/routes/mail/accounts.js.map +1 -0
  310. package/dist/api/routes/mail/app-password.d.ts +4 -0
  311. package/dist/api/routes/mail/app-password.d.ts.map +1 -0
  312. package/dist/api/routes/mail/app-password.js +192 -0
  313. package/dist/api/routes/mail/app-password.js.map +1 -0
  314. package/dist/api/routes/mail/body-helpers.d.ts +56 -0
  315. package/dist/api/routes/mail/body-helpers.d.ts.map +1 -0
  316. package/dist/api/routes/mail/body-helpers.js +91 -0
  317. package/dist/api/routes/mail/body-helpers.js.map +1 -0
  318. package/dist/api/routes/mail/dependencies.d.ts +36 -0
  319. package/dist/api/routes/mail/dependencies.d.ts.map +1 -0
  320. package/dist/api/routes/mail/dependencies.js +2 -0
  321. package/dist/api/routes/mail/dependencies.js.map +1 -0
  322. package/dist/api/routes/mail/drafts.d.ts +5 -0
  323. package/dist/api/routes/mail/drafts.d.ts.map +1 -0
  324. package/dist/api/routes/mail/drafts.js +139 -0
  325. package/dist/api/routes/mail/drafts.js.map +1 -0
  326. package/dist/api/routes/mail/gating.d.ts +45 -0
  327. package/dist/api/routes/mail/gating.d.ts.map +1 -0
  328. package/dist/api/routes/mail/gating.js +99 -0
  329. package/dist/api/routes/mail/gating.js.map +1 -0
  330. package/dist/api/routes/mail/index.d.ts +18 -0
  331. package/dist/api/routes/mail/index.d.ts.map +1 -0
  332. package/dist/api/routes/mail/index.js +40 -0
  333. package/dist/api/routes/mail/index.js.map +1 -0
  334. package/dist/api/routes/mail/messages.d.ts +15 -0
  335. package/dist/api/routes/mail/messages.d.ts.map +1 -0
  336. package/dist/api/routes/mail/messages.js +239 -0
  337. package/dist/api/routes/mail/messages.js.map +1 -0
  338. package/dist/api/routes/mail/outlook-config.d.ts +4 -0
  339. package/dist/api/routes/mail/outlook-config.d.ts.map +1 -0
  340. package/dist/api/routes/mail/outlook-config.js +73 -0
  341. package/dist/api/routes/mail/outlook-config.js.map +1 -0
  342. package/dist/api/routes/mail/provider-resolver.d.ts +64 -0
  343. package/dist/api/routes/mail/provider-resolver.d.ts.map +1 -0
  344. package/dist/api/routes/mail/provider-resolver.js +286 -0
  345. package/dist/api/routes/mail/provider-resolver.js.map +1 -0
  346. package/dist/api/routes/mail/providers.d.ts +4 -0
  347. package/dist/api/routes/mail/providers.d.ts.map +1 -0
  348. package/dist/api/routes/mail/providers.js +73 -0
  349. package/dist/api/routes/mail/providers.js.map +1 -0
  350. package/dist/api/routes/mail/search-health.d.ts +4 -0
  351. package/dist/api/routes/mail/search-health.d.ts.map +1 -0
  352. package/dist/api/routes/mail/search-health.js +131 -0
  353. package/dist/api/routes/mail/search-health.js.map +1 -0
  354. package/dist/api/routes/mail/tags-folders.d.ts +5 -0
  355. package/dist/api/routes/mail/tags-folders.d.ts.map +1 -0
  356. package/dist/api/routes/mail/tags-folders.js +35 -0
  357. package/dist/api/routes/mail/tags-folders.js.map +1 -0
  358. package/dist/api/routes/mail/validators.d.ts +23 -0
  359. package/dist/api/routes/mail/validators.d.ts.map +1 -0
  360. package/dist/api/routes/mail/validators.js +131 -0
  361. package/dist/api/routes/mail/validators.js.map +1 -0
  362. package/dist/api/routes/mail.d.ts.map +1 -1
  363. package/dist/api/routes/mail.js +259 -67
  364. package/dist/api/routes/mail.js.map +1 -1
  365. package/dist/api/routes/managed-tasks.d.ts.map +1 -1
  366. package/dist/api/routes/managed-tasks.js +142 -54
  367. package/dist/api/routes/managed-tasks.js.map +1 -1
  368. package/dist/api/routes/mcp.d.ts.map +1 -1
  369. package/dist/api/routes/mcp.js +115 -47
  370. package/dist/api/routes/mcp.js.map +1 -1
  371. package/dist/api/routes/metrics.d.ts +1 -1
  372. package/dist/api/routes/metrics.js +2 -2
  373. package/dist/api/routes/notion.d.ts.map +1 -1
  374. package/dist/api/routes/notion.js +230 -54
  375. package/dist/api/routes/notion.js.map +1 -1
  376. package/dist/api/routes/observations.d.ts.map +1 -1
  377. package/dist/api/routes/observations.js +40 -169
  378. package/dist/api/routes/observations.js.map +1 -1
  379. package/dist/api/routes/obsidian.d.ts.map +1 -1
  380. package/dist/api/routes/obsidian.js +193 -32
  381. package/dist/api/routes/obsidian.js.map +1 -1
  382. package/dist/api/routes/profile-questions.d.ts.map +1 -1
  383. package/dist/api/routes/profile-questions.js +19 -3
  384. package/dist/api/routes/profile-questions.js.map +1 -1
  385. package/dist/api/routes/receipts.d.ts.map +1 -1
  386. package/dist/api/routes/receipts.js +64 -24
  387. package/dist/api/routes/receipts.js.map +1 -1
  388. package/dist/api/routes/recurring-schedules.d.ts.map +1 -1
  389. package/dist/api/routes/recurring-schedules.js +243 -13
  390. package/dist/api/routes/recurring-schedules.js.map +1 -1
  391. package/dist/api/routes/repositories.d.ts.map +1 -1
  392. package/dist/api/routes/repositories.js +278 -104
  393. package/dist/api/routes/repositories.js.map +1 -1
  394. package/dist/api/routes/schedule-model-resolver.d.ts +153 -0
  395. package/dist/api/routes/schedule-model-resolver.d.ts.map +1 -0
  396. package/dist/api/routes/schedule-model-resolver.js +282 -0
  397. package/dist/api/routes/schedule-model-resolver.js.map +1 -0
  398. package/dist/api/routes/schedule-options.d.ts +25 -0
  399. package/dist/api/routes/schedule-options.d.ts.map +1 -0
  400. package/dist/api/routes/schedule-options.js +77 -0
  401. package/dist/api/routes/schedule-options.js.map +1 -0
  402. package/dist/api/routes/schedule-validation.d.ts +146 -0
  403. package/dist/api/routes/schedule-validation.d.ts.map +1 -0
  404. package/dist/api/routes/schedule-validation.js +153 -0
  405. package/dist/api/routes/schedule-validation.js.map +1 -0
  406. package/dist/api/routes/setup.d.ts.map +1 -1
  407. package/dist/api/routes/setup.js +100 -26
  408. package/dist/api/routes/setup.js.map +1 -1
  409. package/dist/api/routes/skills.d.ts.map +1 -1
  410. package/dist/api/routes/skills.js +81 -30
  411. package/dist/api/routes/skills.js.map +1 -1
  412. package/dist/api/routes/sot-bindings.d.ts.map +1 -1
  413. package/dist/api/routes/sot-bindings.js +46 -11
  414. package/dist/api/routes/sot-bindings.js.map +1 -1
  415. package/dist/api/routes/sse.d.ts.map +1 -1
  416. package/dist/api/routes/sse.js +6 -1
  417. package/dist/api/routes/sse.js.map +1 -1
  418. package/dist/api/routes/system.d.ts.map +1 -1
  419. package/dist/api/routes/system.js +15 -2
  420. package/dist/api/routes/system.js.map +1 -1
  421. package/dist/api/routes/travel-bookings.d.ts.map +1 -1
  422. package/dist/api/routes/travel-bookings.js +26 -9
  423. package/dist/api/routes/travel-bookings.js.map +1 -1
  424. package/dist/api/routes/travel-time.d.ts.map +1 -1
  425. package/dist/api/routes/travel-time.js +50 -10
  426. package/dist/api/routes/travel-time.js.map +1 -1
  427. package/dist/api/routes/wiki.d.ts.map +1 -1
  428. package/dist/api/routes/wiki.js +49 -8
  429. package/dist/api/routes/wiki.js.map +1 -1
  430. package/dist/api/server.d.ts +15 -3
  431. package/dist/api/server.d.ts.map +1 -1
  432. package/dist/api/server.js +32 -10
  433. package/dist/api/server.js.map +1 -1
  434. package/dist/bootstrap/adapters.d.ts.map +1 -1
  435. package/dist/bootstrap/adapters.js +7 -4
  436. package/dist/bootstrap/adapters.js.map +1 -1
  437. package/dist/bootstrap/api.d.ts +205 -0
  438. package/dist/bootstrap/api.d.ts.map +1 -0
  439. package/dist/bootstrap/api.js +443 -0
  440. package/dist/bootstrap/api.js.map +1 -0
  441. package/dist/bootstrap/db.d.ts +119 -0
  442. package/dist/bootstrap/db.d.ts.map +1 -0
  443. package/dist/bootstrap/db.js +294 -0
  444. package/dist/bootstrap/db.js.map +1 -0
  445. package/dist/bootstrap/event-pipeline.d.ts +308 -0
  446. package/dist/bootstrap/event-pipeline.d.ts.map +1 -0
  447. package/dist/bootstrap/event-pipeline.js +704 -0
  448. package/dist/bootstrap/event-pipeline.js.map +1 -0
  449. package/dist/bootstrap/observers.d.ts +148 -0
  450. package/dist/bootstrap/observers.d.ts.map +1 -0
  451. package/dist/bootstrap/observers.js +558 -0
  452. package/dist/bootstrap/observers.js.map +1 -0
  453. package/dist/bootstrap/schedule-helpers.d.ts +122 -0
  454. package/dist/bootstrap/schedule-helpers.d.ts.map +1 -1
  455. package/dist/bootstrap/schedule-helpers.js +202 -4
  456. package/dist/bootstrap/schedule-helpers.js.map +1 -1
  457. package/dist/config.d.ts.map +1 -1
  458. package/dist/config.js +20 -3
  459. package/dist/config.js.map +1 -1
  460. package/dist/core/abort-utils.d.ts +14 -0
  461. package/dist/core/abort-utils.d.ts.map +1 -0
  462. package/dist/core/abort-utils.js +36 -0
  463. package/dist/core/abort-utils.js.map +1 -0
  464. package/dist/core/agent-core.d.ts +22 -3
  465. package/dist/core/agent-core.d.ts.map +1 -1
  466. package/dist/core/agent-core.js +3 -1
  467. package/dist/core/agent-core.js.map +1 -1
  468. package/dist/core/backends/auth-health-monitor.js +2 -3
  469. package/dist/core/backends/auth-health-monitor.js.map +1 -1
  470. package/dist/core/backends/backend-router.d.ts +11 -1
  471. package/dist/core/backends/backend-router.d.ts.map +1 -1
  472. package/dist/core/backends/backend-router.js +97 -2
  473. package/dist/core/backends/backend-router.js.map +1 -1
  474. package/dist/core/backends/claude-code-core.d.ts +51 -0
  475. package/dist/core/backends/claude-code-core.d.ts.map +1 -1
  476. package/dist/core/backends/claude-code-core.js +134 -11
  477. package/dist/core/backends/claude-code-core.js.map +1 -1
  478. package/dist/core/backends/claude-tool-collection.js +1 -1
  479. package/dist/core/backends/codex-core.d.ts +13 -1
  480. package/dist/core/backends/codex-core.d.ts.map +1 -1
  481. package/dist/core/backends/codex-core.js +436 -22
  482. package/dist/core/backends/codex-core.js.map +1 -1
  483. package/dist/core/backends/gemini-cli-core.d.ts +5 -1
  484. package/dist/core/backends/gemini-cli-core.d.ts.map +1 -1
  485. package/dist/core/backends/gemini-cli-core.js +298 -15
  486. package/dist/core/backends/gemini-cli-core.js.map +1 -1
  487. package/dist/core/backends/install-methods.d.ts.map +1 -1
  488. package/dist/core/backends/install-methods.js +22 -0
  489. package/dist/core/backends/install-methods.js.map +1 -1
  490. package/dist/core/backends/model-registry.d.ts +9 -4
  491. package/dist/core/backends/model-registry.d.ts.map +1 -1
  492. package/dist/core/backends/model-registry.js +153 -23
  493. package/dist/core/backends/model-registry.js.map +1 -1
  494. package/dist/core/backends/native-skill-discovery-probe.d.ts +80 -0
  495. package/dist/core/backends/native-skill-discovery-probe.d.ts.map +1 -0
  496. package/dist/core/backends/native-skill-discovery-probe.js +175 -0
  497. package/dist/core/backends/native-skill-discovery-probe.js.map +1 -0
  498. package/dist/core/backends/opencode-basic-auth-fetch.d.ts +27 -0
  499. package/dist/core/backends/opencode-basic-auth-fetch.d.ts.map +1 -0
  500. package/dist/core/backends/opencode-basic-auth-fetch.js +40 -0
  501. package/dist/core/backends/opencode-basic-auth-fetch.js.map +1 -0
  502. package/dist/core/backends/opencode-config-builder.d.ts +86 -0
  503. package/dist/core/backends/opencode-config-builder.d.ts.map +1 -0
  504. package/dist/core/backends/opencode-config-builder.js +172 -0
  505. package/dist/core/backends/opencode-config-builder.js.map +1 -0
  506. package/dist/core/backends/opencode-core.d.ts +316 -0
  507. package/dist/core/backends/opencode-core.d.ts.map +1 -0
  508. package/dist/core/backends/opencode-core.js +1502 -0
  509. package/dist/core/backends/opencode-core.js.map +1 -0
  510. package/dist/core/backends/opencode-event-mapper.d.ts +133 -0
  511. package/dist/core/backends/opencode-event-mapper.d.ts.map +1 -0
  512. package/dist/core/backends/opencode-event-mapper.js +198 -0
  513. package/dist/core/backends/opencode-event-mapper.js.map +1 -0
  514. package/dist/core/backends/opencode-mcp.d.ts +82 -0
  515. package/dist/core/backends/opencode-mcp.d.ts.map +1 -0
  516. package/dist/core/backends/opencode-mcp.js +165 -0
  517. package/dist/core/backends/opencode-mcp.js.map +1 -0
  518. package/dist/core/backends/opencode-server-manager.d.ts +114 -0
  519. package/dist/core/backends/opencode-server-manager.d.ts.map +1 -0
  520. package/dist/core/backends/opencode-server-manager.js +222 -0
  521. package/dist/core/backends/opencode-server-manager.js.map +1 -0
  522. package/dist/core/backends/opencode-types.d.ts +46 -0
  523. package/dist/core/backends/opencode-types.d.ts.map +1 -0
  524. package/dist/core/backends/opencode-types.js +14 -0
  525. package/dist/core/backends/opencode-types.js.map +1 -0
  526. package/dist/core/backends/plan-presets.d.ts +18 -5
  527. package/dist/core/backends/plan-presets.d.ts.map +1 -1
  528. package/dist/core/backends/plan-presets.js +144 -23
  529. package/dist/core/backends/plan-presets.js.map +1 -1
  530. package/dist/core/backends/process-config-cascade.d.ts +35 -0
  531. package/dist/core/backends/process-config-cascade.d.ts.map +1 -1
  532. package/dist/core/backends/process-config-cascade.js +35 -1
  533. package/dist/core/backends/process-config-cascade.js.map +1 -1
  534. package/dist/core/backends/prompt-utils.d.ts.map +1 -1
  535. package/dist/core/backends/prompt-utils.js +0 -2
  536. package/dist/core/backends/prompt-utils.js.map +1 -1
  537. package/dist/core/backends/quota-reset-hints.d.ts +44 -0
  538. package/dist/core/backends/quota-reset-hints.d.ts.map +1 -0
  539. package/dist/core/backends/quota-reset-hints.js +117 -0
  540. package/dist/core/backends/quota-reset-hints.js.map +1 -0
  541. package/dist/core/bang-commands/commands-close.d.ts +24 -0
  542. package/dist/core/bang-commands/commands-close.d.ts.map +1 -0
  543. package/dist/core/bang-commands/commands-close.js +24 -0
  544. package/dist/core/bang-commands/commands-close.js.map +1 -0
  545. package/dist/core/bang-commands/commands-cost.d.ts.map +1 -1
  546. package/dist/core/bang-commands/commands-cost.js +13 -0
  547. package/dist/core/bang-commands/commands-cost.js.map +1 -1
  548. package/dist/core/bang-commands/commands-help.d.ts.map +1 -1
  549. package/dist/core/bang-commands/commands-help.js +44 -5
  550. package/dist/core/bang-commands/commands-help.js.map +1 -1
  551. package/dist/core/bang-commands/commands-report.d.ts.map +1 -1
  552. package/dist/core/bang-commands/commands-report.js +1 -0
  553. package/dist/core/bang-commands/commands-report.js.map +1 -1
  554. package/dist/core/bang-commands/commands-stop-start.d.ts.map +1 -1
  555. package/dist/core/bang-commands/commands-stop-start.js +2 -0
  556. package/dist/core/bang-commands/commands-stop-start.js.map +1 -1
  557. package/dist/core/bang-commands/commands-wiki.d.ts.map +1 -1
  558. package/dist/core/bang-commands/commands-wiki.js +12 -1
  559. package/dist/core/bang-commands/commands-wiki.js.map +1 -1
  560. package/dist/core/bang-commands/format-utils.d.ts +13 -1
  561. package/dist/core/bang-commands/format-utils.d.ts.map +1 -1
  562. package/dist/core/bang-commands/format-utils.js +21 -2
  563. package/dist/core/bang-commands/format-utils.js.map +1 -1
  564. package/dist/core/bang-commands/index.d.ts +1 -0
  565. package/dist/core/bang-commands/index.d.ts.map +1 -1
  566. package/dist/core/bang-commands/index.js +3 -0
  567. package/dist/core/bang-commands/index.js.map +1 -1
  568. package/dist/core/bang-commands/registry.d.ts +50 -0
  569. package/dist/core/bang-commands/registry.d.ts.map +1 -1
  570. package/dist/core/bang-commands/registry.js +164 -19
  571. package/dist/core/bang-commands/registry.js.map +1 -1
  572. package/dist/core/channel-timeline.d.ts +18 -1
  573. package/dist/core/channel-timeline.d.ts.map +1 -1
  574. package/dist/core/channel-timeline.js +44 -0
  575. package/dist/core/channel-timeline.js.map +1 -1
  576. package/dist/core/context/activity-view-runner.js +9 -1
  577. package/dist/core/context/activity-view-runner.js.map +1 -1
  578. package/dist/core/context/default-schedules-runner.js +44 -4
  579. package/dist/core/context/default-schedules-runner.js.map +1 -1
  580. package/dist/core/context/domain-index-runner.js +9 -1
  581. package/dist/core/context/domain-index-runner.js.map +1 -1
  582. package/dist/core/context/entity-source-rename.d.ts.map +1 -1
  583. package/dist/core/context/entity-source-rename.js +9 -1
  584. package/dist/core/context/entity-source-rename.js.map +1 -1
  585. package/dist/core/context/policy-index-runner.js +9 -1
  586. package/dist/core/context/policy-index-runner.js.map +1 -1
  587. package/dist/core/context/reconciler-runner.js +9 -1
  588. package/dist/core/context/reconciler-runner.js.map +1 -1
  589. package/dist/core/context-builder.d.ts +97 -2
  590. package/dist/core/context-builder.d.ts.map +1 -1
  591. package/dist/core/context-builder.js +303 -30
  592. package/dist/core/context-builder.js.map +1 -1
  593. package/dist/core/context-frontmatter.d.ts +6 -0
  594. package/dist/core/context-frontmatter.d.ts.map +1 -1
  595. package/dist/core/context-frontmatter.js +120 -8
  596. package/dist/core/context-frontmatter.js.map +1 -1
  597. package/dist/core/context-health.js +21 -9
  598. package/dist/core/context-health.js.map +1 -1
  599. package/dist/core/context-validation/_pa_wip_context_validation/index.d.ts +19 -0
  600. package/dist/core/context-validation/_pa_wip_context_validation/index.d.ts.map +1 -0
  601. package/dist/core/context-validation/_pa_wip_context_validation/index.js +19 -0
  602. package/dist/core/context-validation/_pa_wip_context_validation/index.js.map +1 -0
  603. package/dist/core/context-validation/_pa_wip_context_validation/prepare-write.d.ts +94 -0
  604. package/dist/core/context-validation/_pa_wip_context_validation/prepare-write.d.ts.map +1 -0
  605. package/dist/core/context-validation/_pa_wip_context_validation/prepare-write.js +130 -0
  606. package/dist/core/context-validation/_pa_wip_context_validation/prepare-write.js.map +1 -0
  607. package/dist/core/context-validation/_pa_wip_context_validation/routine-rulebook.d.ts +60 -0
  608. package/dist/core/context-validation/_pa_wip_context_validation/routine-rulebook.d.ts.map +1 -0
  609. package/dist/core/context-validation/_pa_wip_context_validation/routine-rulebook.js +156 -0
  610. package/dist/core/context-validation/_pa_wip_context_validation/routine-rulebook.js.map +1 -0
  611. package/dist/core/context-validation/_pa_wip_context_validation/section.d.ts +41 -0
  612. package/dist/core/context-validation/_pa_wip_context_validation/section.d.ts.map +1 -0
  613. package/dist/core/context-validation/_pa_wip_context_validation/section.js +82 -0
  614. package/dist/core/context-validation/_pa_wip_context_validation/section.js.map +1 -0
  615. package/dist/core/context-validation/_pa_wip_context_validation/snapshot-debounce.d.ts +52 -0
  616. package/dist/core/context-validation/_pa_wip_context_validation/snapshot-debounce.d.ts.map +1 -0
  617. package/dist/core/context-validation/_pa_wip_context_validation/snapshot-debounce.js +110 -0
  618. package/dist/core/context-validation/_pa_wip_context_validation/snapshot-debounce.js.map +1 -0
  619. package/dist/core/context-validation/_pa_wip_context_validation/today.d.ts +85 -0
  620. package/dist/core/context-validation/_pa_wip_context_validation/today.d.ts.map +1 -0
  621. package/dist/core/context-validation/_pa_wip_context_validation/today.js +162 -0
  622. package/dist/core/context-validation/_pa_wip_context_validation/today.js.map +1 -0
  623. package/dist/core/context-validation/index.d.ts +19 -0
  624. package/dist/core/context-validation/index.d.ts.map +1 -0
  625. package/dist/core/context-validation/index.js +19 -0
  626. package/dist/core/context-validation/index.js.map +1 -0
  627. package/dist/core/context-validation/prepare-write.d.ts +95 -0
  628. package/dist/core/context-validation/prepare-write.d.ts.map +1 -0
  629. package/dist/core/context-validation/prepare-write.js +135 -0
  630. package/dist/core/context-validation/prepare-write.js.map +1 -0
  631. package/dist/core/context-validation/routine-rulebook.d.ts +60 -0
  632. package/dist/core/context-validation/routine-rulebook.d.ts.map +1 -0
  633. package/dist/core/context-validation/routine-rulebook.js +156 -0
  634. package/dist/core/context-validation/routine-rulebook.js.map +1 -0
  635. package/dist/core/context-validation/section.d.ts +41 -0
  636. package/dist/core/context-validation/section.d.ts.map +1 -0
  637. package/dist/core/context-validation/section.js +82 -0
  638. package/dist/core/context-validation/section.js.map +1 -0
  639. package/dist/core/context-validation/snapshot-debounce.d.ts +52 -0
  640. package/dist/core/context-validation/snapshot-debounce.d.ts.map +1 -0
  641. package/dist/core/context-validation/snapshot-debounce.js +110 -0
  642. package/dist/core/context-validation/snapshot-debounce.js.map +1 -0
  643. package/dist/core/context-validation/today.d.ts +85 -0
  644. package/dist/core/context-validation/today.d.ts.map +1 -0
  645. package/dist/core/context-validation/today.js +168 -0
  646. package/dist/core/context-validation/today.js.map +1 -0
  647. package/dist/core/daemon-api-cli.d.ts +6 -5
  648. package/dist/core/daemon-api-cli.d.ts.map +1 -1
  649. package/dist/core/daemon-api-cli.js +73 -32
  650. package/dist/core/daemon-api-cli.js.map +1 -1
  651. package/dist/core/dispatcher-error-handling.d.ts +13 -0
  652. package/dist/core/dispatcher-error-handling.d.ts.map +1 -1
  653. package/dist/core/dispatcher-error-handling.js +69 -0
  654. package/dist/core/dispatcher-error-handling.js.map +1 -1
  655. package/dist/core/dispatcher-hourly-check.d.ts +73 -1
  656. package/dist/core/dispatcher-hourly-check.d.ts.map +1 -1
  657. package/dist/core/dispatcher-hourly-check.js +249 -151
  658. package/dist/core/dispatcher-hourly-check.js.map +1 -1
  659. package/dist/core/dispatcher-message-handler.d.ts +11 -1
  660. package/dist/core/dispatcher-message-handler.d.ts.map +1 -1
  661. package/dist/core/dispatcher-message-handler.js +165 -47
  662. package/dist/core/dispatcher-message-handler.js.map +1 -1
  663. package/dist/core/dispatcher-morning-routine.d.ts +38 -30
  664. package/dist/core/dispatcher-morning-routine.d.ts.map +1 -1
  665. package/dist/core/dispatcher-morning-routine.js +162 -104
  666. package/dist/core/dispatcher-morning-routine.js.map +1 -1
  667. package/dist/core/dispatcher-prompt.d.ts +4 -1
  668. package/dist/core/dispatcher-prompt.d.ts.map +1 -1
  669. package/dist/core/dispatcher-prompt.js +18 -2
  670. package/dist/core/dispatcher-prompt.js.map +1 -1
  671. package/dist/core/dispatcher-repository-helpers.d.ts.map +1 -1
  672. package/dist/core/dispatcher-repository-helpers.js +5 -1
  673. package/dist/core/dispatcher-repository-helpers.js.map +1 -1
  674. package/dist/core/dispatcher-result-processor.d.ts.map +1 -1
  675. package/dist/core/dispatcher-result-processor.js +9 -4
  676. package/dist/core/dispatcher-result-processor.js.map +1 -1
  677. package/dist/core/dispatcher-scheduled-tasks.d.ts +1 -1
  678. package/dist/core/dispatcher-scheduled-tasks.d.ts.map +1 -1
  679. package/dist/core/dispatcher-scheduled-tasks.js +79 -16
  680. package/dist/core/dispatcher-scheduled-tasks.js.map +1 -1
  681. package/dist/core/dispatcher-types.d.ts +84 -12
  682. package/dist/core/dispatcher-types.d.ts.map +1 -1
  683. package/dist/core/dispatcher-types.js.map +1 -1
  684. package/dist/core/dispatcher.d.ts +50 -1
  685. package/dist/core/dispatcher.d.ts.map +1 -1
  686. package/dist/core/dispatcher.js +143 -8
  687. package/dist/core/dispatcher.js.map +1 -1
  688. package/dist/core/dm-freshness-metrics.d.ts +6 -5
  689. package/dist/core/dm-freshness-metrics.d.ts.map +1 -1
  690. package/dist/core/dm-freshness-metrics.js +7 -6
  691. package/dist/core/dm-freshness-metrics.js.map +1 -1
  692. package/dist/core/evening-review-verify.d.ts +164 -0
  693. package/dist/core/evening-review-verify.d.ts.map +1 -0
  694. package/dist/core/evening-review-verify.js +637 -0
  695. package/dist/core/evening-review-verify.js.map +1 -0
  696. package/dist/core/fetch-window-prompt-loader.d.ts +47 -0
  697. package/dist/core/fetch-window-prompt-loader.d.ts.map +1 -0
  698. package/dist/core/fetch-window-prompt-loader.js +72 -0
  699. package/dist/core/fetch-window-prompt-loader.js.map +1 -0
  700. package/dist/core/management-md.d.ts.map +1 -1
  701. package/dist/core/management-md.js +23 -9
  702. package/dist/core/management-md.js.map +1 -1
  703. package/dist/core/management-registry.d.ts +13 -21
  704. package/dist/core/management-registry.d.ts.map +1 -1
  705. package/dist/core/management-registry.js +27 -48
  706. package/dist/core/management-registry.js.map +1 -1
  707. package/dist/core/metrics.d.ts +88 -1
  708. package/dist/core/metrics.d.ts.map +1 -1
  709. package/dist/core/metrics.js +78 -2
  710. package/dist/core/metrics.js.map +1 -1
  711. package/dist/core/morning/agent-journal-appender.d.ts +197 -0
  712. package/dist/core/morning/agent-journal-appender.d.ts.map +1 -0
  713. package/dist/core/morning/agent-journal-appender.js +458 -0
  714. package/dist/core/morning/agent-journal-appender.js.map +1 -0
  715. package/dist/core/morning/handoff-parser.d.ts +45 -0
  716. package/dist/core/morning/handoff-parser.d.ts.map +1 -0
  717. package/dist/core/morning/handoff-parser.js +117 -0
  718. package/dist/core/morning/handoff-parser.js.map +1 -0
  719. package/dist/core/morning/journal-skeleton-builder.d.ts +157 -0
  720. package/dist/core/morning/journal-skeleton-builder.d.ts.map +1 -0
  721. package/dist/core/morning/journal-skeleton-builder.js +303 -0
  722. package/dist/core/morning/journal-skeleton-builder.js.map +1 -0
  723. package/dist/core/morning/orchestrator.d.ts +312 -0
  724. package/dist/core/morning/orchestrator.d.ts.map +1 -0
  725. package/dist/core/morning/orchestrator.js +827 -0
  726. package/dist/core/morning/orchestrator.js.map +1 -0
  727. package/dist/core/morning/parent-audit-emitter.d.ts +82 -0
  728. package/dist/core/morning/parent-audit-emitter.d.ts.map +1 -0
  729. package/dist/core/morning/parent-audit-emitter.js +120 -0
  730. package/dist/core/morning/parent-audit-emitter.js.map +1 -0
  731. package/dist/core/morning/roadmap-skeleton-builder.d.ts +159 -0
  732. package/dist/core/morning/roadmap-skeleton-builder.d.ts.map +1 -0
  733. package/dist/core/morning/roadmap-skeleton-builder.js +338 -0
  734. package/dist/core/morning/roadmap-skeleton-builder.js.map +1 -0
  735. package/dist/core/output-language-policy.js +1 -1
  736. package/dist/core/output-language-policy.js.map +1 -1
  737. package/dist/core/policy-files.d.ts +19 -2
  738. package/dist/core/policy-files.d.ts.map +1 -1
  739. package/dist/core/policy-files.js +65 -7
  740. package/dist/core/policy-files.js.map +1 -1
  741. package/dist/core/pre-pass-freshness.d.ts +28 -0
  742. package/dist/core/pre-pass-freshness.d.ts.map +1 -0
  743. package/dist/core/pre-pass-freshness.js +10 -0
  744. package/dist/core/pre-pass-freshness.js.map +1 -0
  745. package/dist/core/previous-week-digest.d.ts +130 -0
  746. package/dist/core/previous-week-digest.d.ts.map +1 -0
  747. package/dist/core/previous-week-digest.js +257 -0
  748. package/dist/core/previous-week-digest.js.map +1 -0
  749. package/dist/core/prompts.js +3 -3
  750. package/dist/core/prompts.js.map +1 -1
  751. package/dist/core/quiet-hours-sync.d.ts.map +1 -1
  752. package/dist/core/quiet-hours-sync.js +7 -0
  753. package/dist/core/quiet-hours-sync.js.map +1 -1
  754. package/dist/core/recurrence.d.ts +13 -0
  755. package/dist/core/recurrence.d.ts.map +1 -1
  756. package/dist/core/recurrence.js +108 -13
  757. package/dist/core/recurrence.js.map +1 -1
  758. package/dist/core/release-assets.d.ts +21 -1
  759. package/dist/core/release-assets.d.ts.map +1 -1
  760. package/dist/core/release-assets.js +58 -3
  761. package/dist/core/release-assets.js.map +1 -1
  762. package/dist/core/repository-management-docs.d.ts.map +1 -1
  763. package/dist/core/repository-management-docs.js +14 -4
  764. package/dist/core/repository-management-docs.js.map +1 -1
  765. package/dist/core/review-context.d.ts.map +1 -1
  766. package/dist/core/review-context.js +29 -1
  767. package/dist/core/review-context.js.map +1 -1
  768. package/dist/core/roadmap-maintenance.d.ts +213 -0
  769. package/dist/core/roadmap-maintenance.d.ts.map +1 -0
  770. package/dist/core/roadmap-maintenance.js +706 -0
  771. package/dist/core/roadmap-maintenance.js.map +1 -0
  772. package/dist/core/roadmap-validate.d.ts +5 -0
  773. package/dist/core/roadmap-validate.d.ts.map +1 -1
  774. package/dist/core/roadmap-validate.js +6 -69
  775. package/dist/core/roadmap-validate.js.map +1 -1
  776. package/dist/core/routine-acquisition-plan.d.ts +43 -7
  777. package/dist/core/routine-acquisition-plan.d.ts.map +1 -1
  778. package/dist/core/routine-acquisition-plan.js +99 -8
  779. package/dist/core/routine-acquisition-plan.js.map +1 -1
  780. package/dist/core/routine-fetch-window-retry.d.ts +41 -2
  781. package/dist/core/routine-fetch-window-retry.d.ts.map +1 -1
  782. package/dist/core/routine-fetch-window-retry.js +91 -8
  783. package/dist/core/routine-fetch-window-retry.js.map +1 -1
  784. package/dist/core/routine-fetch-window-runner.d.ts +55 -21
  785. package/dist/core/routine-fetch-window-runner.d.ts.map +1 -1
  786. package/dist/core/routine-fetch-window-runner.js +258 -35
  787. package/dist/core/routine-fetch-window-runner.js.map +1 -1
  788. package/dist/core/routine-windows.d.ts +17 -13
  789. package/dist/core/routine-windows.d.ts.map +1 -1
  790. package/dist/core/routine-windows.js +78 -36
  791. package/dist/core/routine-windows.js.map +1 -1
  792. package/dist/core/scheduler.d.ts +121 -37
  793. package/dist/core/scheduler.d.ts.map +1 -1
  794. package/dist/core/scheduler.js +359 -80
  795. package/dist/core/scheduler.js.map +1 -1
  796. package/dist/core/session-manager.d.ts +25 -6
  797. package/dist/core/session-manager.d.ts.map +1 -1
  798. package/dist/core/session-manager.js +32 -15
  799. package/dist/core/session-manager.js.map +1 -1
  800. package/dist/core/skeleton.d.ts.map +1 -1
  801. package/dist/core/skeleton.js +23 -23
  802. package/dist/core/skeleton.js.map +1 -1
  803. package/dist/core/skills-compiler.d.ts +275 -25
  804. package/dist/core/skills-compiler.d.ts.map +1 -1
  805. package/dist/core/skills-compiler.js +844 -205
  806. package/dist/core/skills-compiler.js.map +1 -1
  807. package/dist/core/skills-manifest.d.ts +104 -0
  808. package/dist/core/skills-manifest.d.ts.map +1 -1
  809. package/dist/core/skills-manifest.js +350 -39
  810. package/dist/core/skills-manifest.js.map +1 -1
  811. package/dist/core/wiki/git-precompile.d.ts +9 -0
  812. package/dist/core/wiki/git-precompile.d.ts.map +1 -1
  813. package/dist/core/wiki/git-precompile.js +7 -1
  814. package/dist/core/wiki/git-precompile.js.map +1 -1
  815. package/dist/core/workdir.d.ts +30 -1
  816. package/dist/core/workdir.d.ts.map +1 -1
  817. package/dist/core/workdir.js +140 -15
  818. package/dist/core/workdir.js.map +1 -1
  819. package/dist/db/entities-store.d.ts +5 -5
  820. package/dist/db/entities-store.js +5 -5
  821. package/dist/db/hourly-check-signals.d.ts.map +1 -1
  822. package/dist/db/hourly-check-signals.js +121 -35
  823. package/dist/db/hourly-check-signals.js.map +1 -1
  824. package/dist/db/observations.d.ts +1 -1
  825. package/dist/db/observations.js +1 -1
  826. package/dist/db/observations.js.map +1 -1
  827. package/dist/db/recurring-schedules.d.ts +32 -1
  828. package/dist/db/recurring-schedules.d.ts.map +1 -1
  829. package/dist/db/recurring-schedules.js +29 -10
  830. package/dist/db/recurring-schedules.js.map +1 -1
  831. package/dist/db/repositories-store.d.ts +2 -1
  832. package/dist/db/repositories-store.d.ts.map +1 -1
  833. package/dist/db/repositories-store.js +38 -3
  834. package/dist/db/repositories-store.js.map +1 -1
  835. package/dist/db/schema.d.ts.map +1 -1
  836. package/dist/db/schema.js +157 -51
  837. package/dist/db/schema.js.map +1 -1
  838. package/dist/db/wiki-store.d.ts.map +1 -1
  839. package/dist/db/wiki-store.js +3 -0
  840. package/dist/db/wiki-store.js.map +1 -1
  841. package/dist/index.js +308 -1480
  842. package/dist/index.js.map +1 -1
  843. package/dist/messaging/magic-phrase.d.ts +16 -0
  844. package/dist/messaging/magic-phrase.d.ts.map +1 -1
  845. package/dist/messaging/magic-phrase.js +53 -0
  846. package/dist/messaging/magic-phrase.js.map +1 -1
  847. package/dist/messaging/owner-channels.d.ts +24 -0
  848. package/dist/messaging/owner-channels.d.ts.map +1 -1
  849. package/dist/messaging/owner-channels.js +38 -0
  850. package/dist/messaging/owner-channels.js.map +1 -1
  851. package/dist/observers/calendar-poller.d.ts +2 -0
  852. package/dist/observers/calendar-poller.d.ts.map +1 -1
  853. package/dist/observers/calendar-poller.js +76 -54
  854. package/dist/observers/calendar-poller.js.map +1 -1
  855. package/dist/observers/delegated-sync-worker.d.ts +62 -0
  856. package/dist/observers/delegated-sync-worker.d.ts.map +1 -1
  857. package/dist/observers/delegated-sync-worker.js +128 -1
  858. package/dist/observers/delegated-sync-worker.js.map +1 -1
  859. package/dist/observers/git-watcher.d.ts +22 -0
  860. package/dist/observers/git-watcher.d.ts.map +1 -1
  861. package/dist/observers/git-watcher.js +31 -7
  862. package/dist/observers/git-watcher.js.map +1 -1
  863. package/dist/observers/imminent-event-scheduler.d.ts +2 -0
  864. package/dist/observers/imminent-event-scheduler.d.ts.map +1 -1
  865. package/dist/observers/imminent-event-scheduler.js +29 -0
  866. package/dist/observers/imminent-event-scheduler.js.map +1 -1
  867. package/dist/observers/notion-poller.d.ts +2 -0
  868. package/dist/observers/notion-poller.d.ts.map +1 -1
  869. package/dist/observers/notion-poller.js +44 -6
  870. package/dist/observers/notion-poller.js.map +1 -1
  871. package/dist/observers/poll-guard.d.ts +63 -0
  872. package/dist/observers/poll-guard.d.ts.map +1 -0
  873. package/dist/observers/poll-guard.js +89 -0
  874. package/dist/observers/poll-guard.js.map +1 -0
  875. package/dist/safety/absolute-block-audit.d.ts +17 -0
  876. package/dist/safety/absolute-block-audit.d.ts.map +1 -1
  877. package/dist/safety/absolute-block-audit.js +28 -2
  878. package/dist/safety/absolute-block-audit.js.map +1 -1
  879. package/dist/safety/agent-write-tracker.d.ts +42 -1
  880. package/dist/safety/agent-write-tracker.d.ts.map +1 -1
  881. package/dist/safety/agent-write-tracker.js +81 -1
  882. package/dist/safety/agent-write-tracker.js.map +1 -1
  883. package/dist/safety/always-disallowed.d.ts +34 -0
  884. package/dist/safety/always-disallowed.d.ts.map +1 -1
  885. package/dist/safety/always-disallowed.js +114 -7
  886. package/dist/safety/always-disallowed.js.map +1 -1
  887. package/dist/safety/audit.d.ts +20 -0
  888. package/dist/safety/audit.d.ts.map +1 -1
  889. package/dist/safety/audit.js +126 -0
  890. package/dist/safety/audit.js.map +1 -1
  891. package/dist/safety/risk-classifier.d.ts +17 -1
  892. package/dist/safety/risk-classifier.d.ts.map +1 -1
  893. package/dist/safety/risk-classifier.js +75 -7
  894. package/dist/safety/risk-classifier.js.map +1 -1
  895. package/dist/safety/subprocess-block-scanner.d.ts +89 -0
  896. package/dist/safety/subprocess-block-scanner.d.ts.map +1 -0
  897. package/dist/safety/subprocess-block-scanner.js +177 -0
  898. package/dist/safety/subprocess-block-scanner.js.map +1 -0
  899. package/dist/scheduler/hourly-check-gate.d.ts +23 -9
  900. package/dist/scheduler/hourly-check-gate.d.ts.map +1 -1
  901. package/dist/scheduler/hourly-check-gate.js +11 -6
  902. package/dist/scheduler/hourly-check-gate.js.map +1 -1
  903. package/dist/secrets/backend-api-key-env.d.ts.map +1 -1
  904. package/dist/secrets/backend-api-key-env.js +1 -0
  905. package/dist/secrets/backend-api-key-env.js.map +1 -1
  906. package/dist/services/delegated-backend-invoker.d.ts.map +1 -1
  907. package/dist/services/delegated-backend-invoker.js +8 -1
  908. package/dist/services/delegated-backend-invoker.js.map +1 -1
  909. package/dist/services/delegated-invoker-audit.d.ts.map +1 -1
  910. package/dist/services/delegated-invoker-audit.js +5 -1
  911. package/dist/services/delegated-invoker-audit.js.map +1 -1
  912. package/dist/services/delegated-proxy-config.d.ts +3 -2
  913. package/dist/services/delegated-proxy-config.d.ts.map +1 -1
  914. package/dist/services/delegated-proxy-config.js.map +1 -1
  915. package/dist/services/integrations/extract-write-item-id.d.ts +5 -2
  916. package/dist/services/integrations/extract-write-item-id.d.ts.map +1 -1
  917. package/dist/services/integrations/extract-write-item-id.js.map +1 -1
  918. package/dist/services/mcp/generators/index.d.ts +1 -0
  919. package/dist/services/mcp/generators/index.d.ts.map +1 -1
  920. package/dist/services/mcp/generators/index.js +3 -0
  921. package/dist/services/mcp/generators/index.js.map +1 -1
  922. package/dist/services/mcp/sdk-observations-server.d.ts +60 -0
  923. package/dist/services/mcp/sdk-observations-server.d.ts.map +1 -0
  924. package/dist/services/mcp/sdk-observations-server.js +161 -0
  925. package/dist/services/mcp/sdk-observations-server.js.map +1 -0
  926. package/dist/services/mcp/session-materializer.d.ts.map +1 -1
  927. package/dist/services/mcp/session-materializer.js +13 -9
  928. package/dist/services/mcp/session-materializer.js.map +1 -1
  929. package/dist/services/mcp/types.d.ts +1 -0
  930. package/dist/services/mcp/types.d.ts.map +1 -1
  931. package/dist/services/notion.d.ts +19 -1
  932. package/dist/services/notion.d.ts.map +1 -1
  933. package/dist/services/notion.js +41 -2
  934. package/dist/services/notion.js.map +1 -1
  935. package/dist/services/observations-batch.d.ts +100 -0
  936. package/dist/services/observations-batch.d.ts.map +1 -0
  937. package/dist/services/observations-batch.js +258 -0
  938. package/dist/services/observations-batch.js.map +1 -0
  939. package/dist/services/voice/transcriber.d.ts +23 -0
  940. package/dist/services/voice/transcriber.d.ts.map +1 -1
  941. package/dist/services/voice/transcriber.js +55 -0
  942. package/dist/services/voice/transcriber.js.map +1 -1
  943. package/dist/settings/runtime-settings.d.ts +9 -6
  944. package/dist/settings/runtime-settings.d.ts.map +1 -1
  945. package/dist/settings/runtime-settings.js +50 -17
  946. package/dist/settings/runtime-settings.js.map +1 -1
  947. package/package.json +3 -2
  948. package/dist/api/delegated-proxy-helper.d.ts +0 -33
  949. package/dist/api/delegated-proxy-helper.d.ts.map +0 -1
  950. package/dist/api/delegated-proxy-helper.js +0 -54
  951. package/dist/api/delegated-proxy-helper.js.map +0 -1
  952. package/dist/core/roadmap-merge.d.ts +0 -7
  953. package/dist/core/roadmap-merge.d.ts.map +0 -1
  954. package/dist/core/roadmap-merge.js +0 -187
  955. package/dist/core/roadmap-merge.js.map +0 -1
  956. package/dist/db/test-schemas.d.ts +0 -23
  957. package/dist/db/test-schemas.d.ts.map +0 -1
  958. package/dist/db/test-schemas.js +0 -111
  959. package/dist/db/test-schemas.js.map +0 -1
@@ -0,0 +1,2506 @@
1
+ /**
2
+ * Authoritative registry of every agent-consumable error code. Adding a new
3
+ * code requires a registry entry; the helper logs a warning and emits a
4
+ * placeholder hint when an unregistered code is passed (so a typo doesn't
5
+ * silently ship a useless error).
6
+ *
7
+ * Codes are namespaced by resource:
8
+ * - `schedule.*` — POST /api/schedule + POST /api/schedule/batch
9
+ * - `agent_actions.*` — PATCH /api/agent-actions/self
10
+ *
11
+ * The codes here must be reachable via a crafted bad request — every test
12
+ * file for these endpoints carries a coverage assertion against the registry
13
+ * (see schedule-batch.test.ts / agent-actions.test.ts).
14
+ */
15
+ export const AGENT_ERROR_REGISTRY = {
16
+ // ── POST /api/schedule + /api/schedule/batch row-level codes ─────────────
17
+ "schedule.body_not_object": {
18
+ expected: "JSON object",
19
+ hint: "Request body must be a JSON object. For /api/schedule/batch wrap rows in '{\"rows\":[…]}'. For /api/schedule supply the row fields directly at the top level.",
20
+ skillAnchor: "schedule#request-shape",
21
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#body_not_object",
22
+ constraint: { type: "object", required: true },
23
+ },
24
+ "schedule.rows_field_missing": {
25
+ expected: "array of row objects under 'rows'",
26
+ hint: "Wrap your row objects in a 'rows' array: POST {\"rows\":[{…},{…}]}. An empty array is accepted as a documented no-op.",
27
+ skillAnchor: "schedule#batch-shape",
28
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#rows_field_missing",
29
+ constraint: { type: "array", required: true },
30
+ },
31
+ "schedule.rows_too_many": {
32
+ expected: "at most 50 rows per batch",
33
+ hint: "Split the batch into chunks of at most 50 rows. The morning routine's typical batch size is 4-8 rows; 50 is a generous cap.",
34
+ skillAnchor: "schedule#batch-shape",
35
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#rows_too_many",
36
+ constraint: { type: "array", maximum: 50 },
37
+ },
38
+ "schedule.scheduled_for_invalid": {
39
+ expected: "ISO8601 timestamp parseable by Date()",
40
+ hint: "Use an ISO 8601 string with timezone offset, e.g. '2026-05-15T14:30:00-04:00'. Convert relative expressions via <current_time> in your context.",
41
+ skillAnchor: "schedule#scheduledFor-bounds",
42
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#scheduled_for_invalid",
43
+ constraint: { type: "iso8601", required: true },
44
+ },
45
+ "schedule.scheduled_for_in_past": {
46
+ expected: "ISO8601 timestamp >= now",
47
+ hint: "scheduledFor must be at least 1 minute in the future. Pick a time later today or tomorrow, then resubmit.",
48
+ skillAnchor: "schedule#scheduledFor-bounds",
49
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#scheduled_for_in_past",
50
+ constraint: { type: "iso8601" },
51
+ },
52
+ "schedule.task_type_unknown": {
53
+ expected: "one of 'wake' | 'dm_session' | 'check' | 'dm'",
54
+ hint: "taskType is the dispatcher's switch. Pick: 'wake' = future agent session that decides+acts at fire time (most common); 'dm_session' = agent session that composes ONE DM and exits (notifications with model judgment); 'dm' = precomposed DM with no LLM call at fire time (use this on POST /api/schedule/dm, NOT POST /api/schedule); 'check' = non-LLM probe (rare — internal). When in doubt for routines, use 'wake'.",
55
+ skillAnchor: "schedule#taskType",
56
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#task_type_unknown",
57
+ constraint: { type: "enum", enum: ["wake", "dm_session", "check", "dm"], required: true },
58
+ },
59
+ "schedule.description_too_short": {
60
+ expected: "string with >= 20 characters",
61
+ hint: "The wake-up agent has NO memory — description is its only context. Include what + why + who + expected output. Bad: 'Meeting prep'. Good: '15-min reminder for the 14:00 design review. Attendees: Sarah, Mike. Notify the user via Slack.'",
62
+ skillAnchor: "schedule#description-shape",
63
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#description_too_short",
64
+ constraint: { type: "string", minLength: 20, required: true },
65
+ },
66
+ "schedule.prompt_too_short": {
67
+ expected: "string with >= 20 characters when set",
68
+ hint: "If you supply prompt as an override for description, it must also be >= 20 chars. Or omit it and the row falls back to description.",
69
+ skillAnchor: "schedule#description-shape",
70
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#prompt_too_short",
71
+ constraint: { type: "string", minLength: 20 },
72
+ },
73
+ "schedule.task_context_field_missing": {
74
+ expected: "string with >= 30 characters explaining why this task is being scheduled",
75
+ hint: "Stage A must populate taskContext.background and taskContext.expected_output so the future session can produce high-quality output without re-deriving context. Example background: 'User flagged Q2 roadmap risks in yesterday's DM; pre-brief should surface the two open items before the 15:00 standup.'",
76
+ skillAnchor: "schedule#taskContext-required-fields",
77
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#task_context_field_missing",
78
+ constraint: { type: "string", minLength: 30, required: true },
79
+ },
80
+ "schedule.task_context_field_too_short": {
81
+ expected: "non-trivial string content",
82
+ hint: "Required taskContext fields must carry real content. background needs >=30 chars, expected_output needs >=20 chars. Omitting them is not an option — they're how the future session reconstructs context.",
83
+ skillAnchor: "schedule#taskContext-required-fields",
84
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#task_context_field_too_short",
85
+ constraint: { type: "string" },
86
+ },
87
+ "schedule.task_context_field_wrong_type": {
88
+ expected: "matching type per the schema",
89
+ hint: "taskContext fields have typed slots: background = string (>=30 chars); expected_output = string (>=20 chars); references = string[] (paths or URLs the future session should read); tier_override = null | 'lite' | 'medium' | 'high'; tone = free string ('terse', 'celebratory', etc.); deadline = ISO8601 string or null. The response's `received` shows the actual JS type the daemon saw — coerce to the expected one and resubmit.",
90
+ skillAnchor: "schedule#taskContext-required-fields",
91
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#task_context_field_wrong_type",
92
+ },
93
+ "schedule.model_unknown": {
94
+ // Free-form token after SCHEDULE_API_REDESIGN_PLAN §4.3. The route
95
+ // attaches a `validValues` payload listing every alias + every
96
+ // currently-registered model per backend, so the LLM's retry can
97
+ // pick from the live list rather than guessing.
98
+ expected: "'sonnet' | 'opus' | a registered model id | omitted",
99
+ hint: "Pass one of: a legacy alias ('sonnet'/'opus' — rewritten to tier 'medium'/'high'); a full registered model id (e.g. 'claude-opus-4-7', 'gpt-5.4'); the composite form '<backendId>/<modelId>' when an id collides across backends; or omit `model` and use `tier` (recommended). The response's validValues lists every alias + every currently-registered model per backend — pick from that list and resubmit.",
100
+ skillAnchor: "schedule#model-selection",
101
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#model_unknown",
102
+ constraint: { type: "string", maxLength: 120 },
103
+ },
104
+ "schedule.model_ambiguous": {
105
+ // Today unreachable from the live registry (`claude-opus-4-7` vs
106
+ // opencode's `anthropic/claude-opus-4-7` differ), but the registry
107
+ // is editable and a future entry could collide. The route's
108
+ // validValues carries the per-backend matches list so the caller's
109
+ // retry can disambiguate via the composite form.
110
+ expected: "an unambiguous model token (use '<backendId>/<modelId>')",
111
+ hint: "The model id you supplied is registered under more than one backend. Resubmit using the composite '<backendId>/<modelId>' form (e.g. 'claude/claude-opus-4-7') so the daemon doesn't have to guess which backend you meant. The response's validValues.matches lists the colliding entries.",
112
+ skillAnchor: "schedule#model-selection",
113
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#model_ambiguous",
114
+ constraint: { type: "string" },
115
+ },
116
+ "schedule.model_deprecated": {
117
+ // Severity is `warning` per SCHEDULE_API_REDESIGN_PLAN §5.0.5 — the
118
+ // row is still created and surfaced via `envelope.warnings[]`. A
119
+ // hard reject would brick long-lived recurring rules on a registry
120
+ // version bump; nudging via warning lets the LLM refine on the next
121
+ // turn without losing the immediate write.
122
+ expected: "a non-deprecated model id (or omit and use `tier`)",
123
+ hint: "The model id you supplied is registered but flagged deprecated — the registry may remove it on a future release. The row was still persisted. Consider switching to a non-deprecated id from the response's validValues.availableModels list, or use `tier` instead so the dispatcher picks the latest non-deprecated model for the resolved process key.",
124
+ skillAnchor: "schedule#model-selection",
125
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#model_deprecated",
126
+ severity: "warning",
127
+ constraint: { type: "string" },
128
+ },
129
+ "schedule.backend_id_unknown": {
130
+ // Defensive — `validateModelToken` returns BackendId-narrowed
131
+ // values from `BACKEND_IDS`. A composite-form token whose prefix
132
+ // isn't a real backend is treated as a fall-through (the suffix is
133
+ // re-scanned across all backends). This code is therefore reserved
134
+ // for direct API consumers that hand-construct a backend tag, or
135
+ // for the route's defense-in-depth final guard.
136
+ expected: "'claude' | 'codex' | 'gemini' | 'opencode'",
137
+ hint: "The backend id portion of the composite-form token did not match a registered backend. Use one of the four BackendId values listed in validValues — the daemon does not dispatch to any others.",
138
+ skillAnchor: "schedule#model-selection",
139
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#backend_id_unknown",
140
+ constraint: { type: "enum", enum: ["claude", "codex", "gemini", "opencode"] },
141
+ },
142
+ "schedule.tier_unknown": {
143
+ expected: "'lite' | 'medium' | 'high' | omitted",
144
+ hint: "tier is the abstract cost knob — prefer this over `model` for new schedules. Omit to use the dispatcher's process-key default (medium). Use 'lite' for hourly polling/health checks (Haiku-class) and 'high' for one-off generative work (Opus-class).",
145
+ skillAnchor: "schedule#tier-selection",
146
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#tier_unknown",
147
+ constraint: { type: "enum", enum: ["lite", "medium", "high"] },
148
+ },
149
+ "schedule.tier_and_model_conflict": {
150
+ // SCHEDULE_API_REDESIGN_PLAN §4.3 / §12.2 — recommendation accepted
151
+ // as hard-reject so the row's intent is self-documenting (a row
152
+ // that carries both gives no signal about which the operator
153
+ // actually wanted). The LLM's error loop adapts in one retry.
154
+ expected: "exactly one of `tier` OR `model` (not both)",
155
+ hint: "You supplied BOTH `tier` and `model`. Pick one: use `tier` ('lite' | 'medium' | 'high') as the abstract cost knob — this is the recommended path because the dispatcher picks the latest non-deprecated model per backend automatically. Use `model` only when you must pin a specific id (e.g. reproducing a past run on a deprecated id). On PATCH, you can clear one and set the other in the same request (pass `null` to clear).",
156
+ skillAnchor: "schedule#tier-vs-model",
157
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#tier_and_model_conflict",
158
+ },
159
+ "schedule.batch_atomic_invalid": {
160
+ expected: "boolean (default true)",
161
+ hint: "atomic controls rollback. true (default) wraps all rows in one transaction; any row error rolls back all inserts. false commits successful rows individually. Use true for morning routine to retry the whole batch as one unit.",
162
+ skillAnchor: "schedule#batch-shape",
163
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#batch_atomic_invalid",
164
+ constraint: { type: "boolean" },
165
+ },
166
+ // ── recurring_schedules.* / recurrenceRule field-level codes ──────────────
167
+ //
168
+ // POST /api/recurring-schedules + PATCH /api/recurring-schedules/:id
169
+ // call `translateZodError` with a fieldCodeMap that maps each
170
+ // recurrenceRule sub-field onto the codes below. SCHEDULE_API_REDESIGN_PLAN
171
+ // §5.4 — the legacy single-issue `recurring_schedules.validation_error`
172
+ // collapse is gone; every Zod failure now carries the specific code
173
+ // the LLM needs to retry.
174
+ "schedule.frequency_unknown": {
175
+ expected: "'hourly' | 'daily' | 'weekly' | 'monthly'",
176
+ hint: "recurrenceRule.frequency must be one of: 'hourly' (every N hours at :MM), 'daily' (one fire per day at HH:MM), 'weekly' (HH:MM on the listed daysOfWeek), 'monthly' (HH:MM on the listed daysOfMonth). See validValues for the exact enum.",
177
+ skillAnchor: "recurring#frequency",
178
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#frequency_unknown",
179
+ constraint: { type: "enum", enum: ["hourly", "daily", "weekly", "monthly"] },
180
+ },
181
+ "schedule.frequency_field_mismatch": {
182
+ expected: "field set that matches the chosen frequency",
183
+ hint: "Each frequency requires a specific combination of fields. hourly: no `time`, no day-of-* fields, no `onMissingDay` — use `intervalHours` (1..23, default 1) + `minuteOfHour` (0..59, default 0). daily: `time` only (HH:MM). weekly: `time` + `daysOfWeek` (0=Sun..6=Sat). monthly: `time` + `daysOfMonth` (1..31), `onMissingDay` optional ('skip' | 'lastDayOfMonth', default 'lastDayOfMonth'). See validValues.requiredFor / forbiddenFor for the exact matrix.",
184
+ skillAnchor: "recurring#frequency-fields",
185
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#frequency_field_mismatch",
186
+ },
187
+ "schedule.interval_hours_out_of_range": {
188
+ expected: "integer in [1, 23]",
189
+ hint: "intervalHours sets the hourly cadence — N=1 fires every hour, N=2 every two hours anchored at midnight in the rule's timezone. The cap is 23 because N=24 collapses to a daily fire; if you want daily, switch frequency to 'daily' and set `time`. Sub-hour intervals are not supported.",
190
+ skillAnchor: "recurring#hourly",
191
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#interval_hours_out_of_range",
192
+ constraint: { type: "integer", minimum: 1, maximum: 23 },
193
+ },
194
+ "schedule.minute_of_hour_out_of_range": {
195
+ expected: "integer in [0, 59]",
196
+ hint: "minuteOfHour pins which minute the hourly cadence lands on. Default 0 (fires at :00). Example: minuteOfHour=15 with intervalHours=2 fires at 00:15, 02:15, 04:15, …",
197
+ skillAnchor: "recurring#hourly",
198
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#minute_of_hour_out_of_range",
199
+ constraint: { type: "integer", minimum: 0, maximum: 59 },
200
+ },
201
+ "schedule.time_format_invalid": {
202
+ expected: "string matching ^\\d{2}:\\d{2}$ (24h)",
203
+ hint: "recurrenceRule.time is 24-hour HH:MM local to the rule's timezone — e.g. '09:00', '21:30'. No seconds, no AM/PM, no offset.",
204
+ skillAnchor: "recurring#time",
205
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#time_format_invalid",
206
+ constraint: { type: "string", pattern: "^\\d{2}:\\d{2}$" },
207
+ },
208
+ "schedule.days_of_week_invalid": {
209
+ expected: "non-empty array of distinct integers in [0, 6]",
210
+ hint: "daysOfWeek is required for frequency='weekly' and forbidden otherwise. Integers 0..6 map to Sun..Sat (see validValues.labels). Duplicates are rejected at the schema layer — a duplicate is always a caller bug, never intent. Example: [1,3,5] = Mon/Wed/Fri.",
211
+ skillAnchor: "recurring#weekly",
212
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#days_of_week_invalid",
213
+ constraint: { type: "array", minimum: 1, maximum: 7 },
214
+ },
215
+ "schedule.days_of_month_invalid": {
216
+ expected: "non-empty array of distinct integers in [1, 31]",
217
+ hint: "daysOfMonth is required for frequency='monthly' and forbidden otherwise. Integers 1..31. Duplicates are rejected. Days 29-31 may fall outside short months — control the behavior via `onMissingDay` ('skip' to drop those months, 'lastDayOfMonth' to fire on the actual last day instead).",
218
+ skillAnchor: "recurring#monthly",
219
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#days_of_month_invalid",
220
+ constraint: { type: "array", minimum: 1, maximum: 31 },
221
+ },
222
+ "schedule.on_missing_day_unknown": {
223
+ expected: "'skip' | 'lastDayOfMonth' (or omit; defaults to 'lastDayOfMonth')",
224
+ hint: "onMissingDay only applies to frequency='monthly'. 'skip' drops months that don't contain the requested day (e.g. [31] skips Feb/Apr/Jun/Sep/Nov). 'lastDayOfMonth' fires on the actual last day of those months instead (the bit-compatible default — preserves the pre-redesign clamp behavior).",
225
+ skillAnchor: "recurring#on-missing-day",
226
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#on_missing_day_unknown",
227
+ constraint: { type: "enum", enum: ["skip", "lastDayOfMonth"] },
228
+ },
229
+ "schedule.on_missing_day_unused": {
230
+ // SCHEDULE_API_REDESIGN_PLAN §5 — `onMissingDay` only changes
231
+ // behavior when `daysOfMonth` contains a day that doesn't exist in
232
+ // every month (29, 30, 31). Setting it on `[1,15]` is a no-op
233
+ // intent signal — accept the row but nudge the caller via the
234
+ // warnings[] channel so future PATCHes converge on the right
235
+ // shape. Warning-level: the row is still persisted (201/200),
236
+ // `retryable` ignores it.
237
+ expected: "onMissingDay omitted, OR daysOfMonth contains 29, 30, or 31",
238
+ hint: "You set `onMissingDay` but `daysOfMonth` has no entry in [29, 30, 31] — the field has no effect because every month already contains the requested day. Either drop `onMissingDay` to keep the row clean, or extend `daysOfMonth` (e.g. add 31 if this is meant to be a month-end rule).",
239
+ skillAnchor: "recurring#on-missing-day",
240
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#on_missing_day_unused",
241
+ severity: "warning",
242
+ },
243
+ "schedule.timezone_unknown": {
244
+ expected: "valid IANA timezone string",
245
+ hint: "recurrenceRule.timezone must parse via Intl.DateTimeFormat with a real IANA zone (e.g. 'Asia/Tokyo', 'America/New_York', 'UTC'). When omitted, the daemon falls back to the configured primary timezone, then the system zone.",
246
+ skillAnchor: "recurring#timezone",
247
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#timezone_unknown",
248
+ constraint: { type: "string" },
249
+ },
250
+ "schedule.recurrence_rule_invalid": {
251
+ expected: "well-formed RecurrenceRule object",
252
+ hint: "recurrenceRule must be an object with `frequency` plus the per-frequency fields. The route emits more specific codes (schedule.frequency_unknown / schedule.frequency_field_mismatch / schedule.time_format_invalid / …) when it can identify the offending field; this fallback fires on structurally-invalid bodies the Zod traversal could not localise.",
253
+ skillAnchor: "recurring#shape",
254
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#recurrence_rule_invalid",
255
+ },
256
+ "schedule.recurring_id_invalid": {
257
+ // Mirrors the existing `recurring_schedules.invalid_id` legacy
258
+ // path so the dashboard's id-form switch keeps working while the
259
+ // structured envelope migrates over. New consumers read errors[0].code.
260
+ expected: "positive integer id",
261
+ hint: "The id path segment must parse as a positive integer. Use the value returned by POST /api/recurring-schedules (item.id), not a slug or 0.",
262
+ skillAnchor: "recurring#identifiers",
263
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#recurring_id_invalid",
264
+ legacyErrorCode: "invalid_id",
265
+ constraint: { type: "integer", minimum: 1 },
266
+ },
267
+ "schedule.recurring_not_found": {
268
+ expected: "recurring schedule id that exists",
269
+ hint: "No recurring schedule exists with this id. List /api/recurring-schedules to see the current rows, or POST a new one. Deleted rows are gone permanently — the table does not soft-delete.",
270
+ skillAnchor: "recurring#identifiers",
271
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#recurring_not_found",
272
+ legacyErrorCode: "not_found",
273
+ },
274
+ "schedule.recurring_no_changes": {
275
+ // Mirrors the existing PATCH refine "At least one field must be
276
+ // provided for update". Emitted from the recurring update path's
277
+ // Zod translator when the body is `{}`.
278
+ expected: "at least one field set on the PATCH body",
279
+ hint: "An empty PATCH body is rejected — supply at least one of `description`, `prompt`, `recurrenceRule`, `model`, `tier`, `taskContext`, `enabled`. PATCH does not re-materialize the already-pending agent_schedule row unless you change `recurrenceRule` or `enabled`.",
280
+ skillAnchor: "recurring#patch-semantics",
281
+ docsUrl: "agent-assets/skills/schedule/references/errors.md#recurring_no_changes",
282
+ },
283
+ // ── PATCH /api/agent-actions/self codes ──────────────────────────────────
284
+ "agent_actions.session_identity_missing": {
285
+ expected: "x-pa-event-correlation-id and x-process-key headers identifying the running session",
286
+ hint: "The pa-api shim auto-injects these from PA_EVENT_CORRELATION_ID and PA_PROCESS_KEY when running inside a dispatcher-spawned session. If you are calling this endpoint directly from the dashboard or a test, supply both headers explicitly.",
287
+ skillAnchor: "agent-actions#self-write-auth",
288
+ retryable: false,
289
+ },
290
+ "agent_actions.session_row_not_found": {
291
+ expected: "an in-flight agent_actions row matching (event_id, action_type)",
292
+ hint: "No row was found for the running session's correlation id + process key. Either the dispatcher has not yet inserted the row, or the row's result column is already terminal (success/failed). Verify the dispatcher writes an in_progress row before launching the session.",
293
+ skillAnchor: "agent-actions#self-write-auth",
294
+ retryable: false,
295
+ },
296
+ "agent_actions.metadata_field_invalid": {
297
+ expected: "object with optional dayType/anomalies/filesTouched/inboxStats/scheduleBatchSize",
298
+ hint: "The metadata body slot is shallow-merged into the row's metadata column. Pass an object literal; arrays go inside named keys (e.g. anomalies:[...]). Non-JSON-serialisable values (functions, Symbols) are rejected.",
299
+ skillAnchor: "agent-actions#metadata-shape",
300
+ constraint: { type: "object", required: true },
301
+ },
302
+ "agent_actions.body_not_object": {
303
+ expected: "JSON object",
304
+ hint: "Body must be {\"metadata\":{…}}. The metadata key is required; other top-level keys are ignored.",
305
+ skillAnchor: "agent-actions#metadata-shape",
306
+ constraint: { type: "object", required: true },
307
+ },
308
+ // `agent_actions.cross_row_write_forbidden` is NOT registered. The
309
+ // morning-routine-optimization rev4 banner records why the cross-row
310
+ // 403 contract was withdrawn rather than shipped: Aitne is
311
+ // single-owner, daemon is 127.0.0.1-only, correlation_ids are UUIDs,
312
+ // and the realistic failure modes (pa-api shim bug, dispatcher env
313
+ // bug) are not defended by cryptographic token binding. The endpoint
314
+ // stays at the shipped header-lookup shape and emits 404
315
+ // `agent_actions.session_row_not_found` when the lookup misses;
316
+ // there is no 403 path for cross-row attempts. Keeping the code
317
+ // unregistered preserves this file's reachability invariant (every
318
+ // registered code must be reachable from a crafted request).
319
+ // ── /api/context/* — MD-centric memory CRUD (the only legal write path
320
+ // for agent context. See docs/design/06-memory.md). Every entry
321
+ // carries a `legacyErrorCode` because the dashboard's knowledge-page
322
+ // error branches (`context-files-content.tsx`) still switch on the
323
+ // pre-envelope short codes ("forbidden", "morning_routine_lock_held",
324
+ // "validation_error", …).
325
+ "context.path_invalid": {
326
+ expected: "context-relative path within the allowed write whitelist",
327
+ hint: "The path failed safePath(): it escaped getContextDir() or pointed outside the allowed file set. Use a path like 'today', 'roadmap', 'projects/<slug>', 'user/profile' — no '..', no absolute paths. Read GET /api/context/list/projects to discover live slugs.",
328
+ skillAnchor: "context#allowed-paths",
329
+ legacyErrorCode: "invalid_path",
330
+ constraint: { type: "string" },
331
+ },
332
+ "context.path_required": {
333
+ expected: "non-empty 'path' query / route parameter",
334
+ hint: "The endpoint needs a context path (e.g. 'today', 'projects/launch-prep'). For wildcard read/write routes the path is the URL tail after /api/context/; for snapshot restore the path is captured from the snapshot row.",
335
+ skillAnchor: "context#allowed-paths",
336
+ legacyErrorCode: "path_required",
337
+ constraint: { type: "string", required: true },
338
+ },
339
+ "context.path_not_found": {
340
+ expected: "an existing context MD file at the resolved path",
341
+ hint: "GET / PATCH / DELETE on a path that does not exist on disk. Use GET /api/context/list/projects (or .../list/rules) to discover available paths, or PUT first to create the file.",
342
+ skillAnchor: "context#allowed-paths",
343
+ legacyErrorCode: "not_found",
344
+ retryable: false,
345
+ },
346
+ "context.write_forbidden": {
347
+ expected: "path inside the daemon's write whitelist",
348
+ hint: "This file is read-only from the agent side. The whitelist covers user.md, today.md, roadmap.md, projects/*, rules/policies/*, user/*, agent/journal, agent/profile-questions, weekly/*, monthly/*, schedule/*, dossiers/*, work/meetings/*, and custom routines. Files outside this set are owned by the daemon or operator. If you need to record something, GET /api/context/list/projects and find a writable parent, or NOTIFY the user.",
349
+ skillAnchor: "context#write-whitelist",
350
+ legacyErrorCode: "forbidden",
351
+ retryable: false,
352
+ },
353
+ "context.lock_held": {
354
+ expected: "morning-routine lock not held by another holder",
355
+ hint: "Another session is currently running the morning routine. Wait for it to finish (the holder block in the response carries the lockId and acquiredAt). Acquire-lock requests on a held lock return 409.",
356
+ skillAnchor: "context#locks",
357
+ legacyErrorCode: "lock_held",
358
+ retryable: true,
359
+ },
360
+ "context.lock_not_held": {
361
+ expected: "valid lockId held by this caller",
362
+ hint: "Release was rejected — the lockId in the body did not match the current holder. The release contract is single-shot: each acquire returns exactly one lockId; releasing twice or releasing without the id returns 400. If the dispatcher did not capture the id, re-acquire and immediately release.",
363
+ skillAnchor: "context#locks",
364
+ legacyErrorCode: "lock_not_held",
365
+ retryable: false,
366
+ },
367
+ "context.morning_routine_lock_held": {
368
+ expected: "morning_routine lock released OR X-Lock-Id matching the holder",
369
+ hint: "today.md write rejected because the morning routine is in progress. If you are the routine, pass X-Lock-Id: <id> with the lockId from POST /context/lock/morning-routine. If you are not the routine, wait for it to finish (typically <2 minutes) and retry.",
370
+ skillAnchor: "context#locks",
371
+ legacyErrorCode: "morning_routine_lock_held",
372
+ retryable: true,
373
+ },
374
+ "context.roadmap_write_lock_held": {
375
+ expected: "roadmap write lock released OR X-Lock-Id matching the holder",
376
+ hint: "roadmap.md write rejected because another flow is currently rewriting roadmap. Pass X-Lock-Id: <id> from POST /context/lock/roadmap if you acquired it, or wait for the holder (typically routine.roadmap_refresh) to finish.",
377
+ skillAnchor: "context#locks",
378
+ legacyErrorCode: "roadmap_write_lock_held",
379
+ retryable: true,
380
+ },
381
+ "context.body_not_object": {
382
+ expected: "JSON object body",
383
+ hint: "PUT / PATCH / lock-release / archive-today bodies must be a JSON object. Wrap your payload in '{}'. An empty body is rejected by PUT (content required) but accepted by lock-release as a no-op.",
384
+ skillAnchor: "context#request-shape",
385
+ legacyErrorCode: "validation_error",
386
+ constraint: { type: "object", required: true },
387
+ },
388
+ "context.invalid_json_body": {
389
+ expected: "syntactically valid JSON",
390
+ hint: "Body failed JSON.parse(). Check for trailing commas, single quotes, or unescaped newlines in string content. Use jq -nc for compact valid JSON before piping into curl --data-binary.",
391
+ skillAnchor: "context#request-shape",
392
+ legacyErrorCode: "invalid_json_body",
393
+ },
394
+ "context.snapshot_id_invalid": {
395
+ expected: "positive safe integer matching md_file_snapshots.id",
396
+ hint: "Snapshot id must be a positive integer (no decimals, no scientific notation). GET /api/context/snapshots/:path to list available snapshot ids.",
397
+ skillAnchor: "context#snapshots",
398
+ legacyErrorCode: "invalid_id",
399
+ constraint: { type: "integer", minimum: 1 },
400
+ },
401
+ "context.snapshot_not_found": {
402
+ expected: "an existing row in md_file_snapshots",
403
+ hint: "No snapshot exists with that id. Snapshots are pruned over time — verify the id from a fresh GET /api/context/snapshots/:path before restoring.",
404
+ skillAnchor: "context#snapshots",
405
+ legacyErrorCode: "not_found",
406
+ retryable: false,
407
+ },
408
+ "context.roadmap_id_generation_failed": {
409
+ expected: "successful id generation after collision retries",
410
+ hint: "The daemon exhausted retries trying to mint a non-colliding 4-char suffix for the roadmap id. This indicates an unusually full roadmap day or a corrupt randomBytes injection. Retry once; if it fails again, file an issue — DO NOT manually mint an id, the IDs must be daemon-issued so audit hooks fire.",
411
+ skillAnchor: "context#roadmap-ids",
412
+ legacyErrorCode: "roadmap_id_generation_failed",
413
+ retryable: true,
414
+ },
415
+ "context.creation_date_invalid": {
416
+ expected: "YYYY-MM-DD calendar date string",
417
+ hint: "Roadmap-id creationDate must be a calendar date in YYYY-MM-DD form (no time component). Use localDateStr(now, <timezone>) shape; omit the field entirely to let the daemon stamp it from config.timezone.",
418
+ skillAnchor: "context#roadmap-ids",
419
+ legacyErrorCode: "validation_error",
420
+ constraint: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$" },
421
+ },
422
+ "context.directory_invalid": {
423
+ expected: "directory listed in the allowed-directory set",
424
+ hint: "Directory listings are restricted to the curated allow-list returned in the 400 response's `allowed` field (e.g. 'projects', 'rules/policies', 'user-details', 'dossiers'). Pick from that list — wildcards and absolute paths are rejected.",
425
+ skillAnchor: "context#allowed-paths",
426
+ legacyErrorCode: "invalid_directory",
427
+ },
428
+ "context.content_validation_failed": {
429
+ expected: "content matching the file's schema (today/roadmap/projects/...)",
430
+ hint: "The body content failed validateContextContent(). The response's `message` field carries the specific reason: today.md H1 date mismatch, frontmatter shape, roadmap row schema, project doc skeleton, etc. Read the message verbatim and adjust the offending section — do NOT retry without changing the content.",
431
+ skillAnchor: "context#content-validation",
432
+ legacyErrorCode: "validation_error",
433
+ },
434
+ // morning-routine-optimization.md §"PUT /api/context/daily/<date>
435
+ // skeleton-preservation validator" — Stage B (the lite-tier journal
436
+ // author) MUST preserve the seven skeleton-owned frontmatter fields
437
+ // byte-for-byte from the `<journal_skeleton>` it received in its
438
+ // prompt: date, weekday, type, owner, agent_generated,
439
+ // calendar_events, messages_handled. `type` and `owner` are pinned
440
+ // by the existing context-frontmatter validator; the remaining five
441
+ // are pinned here with per-field structured errors so Stage B can
442
+ // self-correct in one retry rather than guessing which field
443
+ // drifted. Body is NOT validated against the skeleton (Stage B
444
+ // authors the body per rules/journal-format.md).
445
+ "context.daily_skeleton_field_drift": {
446
+ expected: "skeleton-owned frontmatter field present and well-typed",
447
+ hint: "A required skeleton-owned frontmatter field is missing or malformed on the daily/<date>.md write. The errors array carries one entry per offending field with the exact `received` value and the `expected` shape. Re-emit the file with the missing fields restored from the `<journal_skeleton>` you were given — the skeleton's frontmatter is preserved byte-for-byte; do not paraphrase it. Body authoring is yours per rules/journal-format.md.",
448
+ skillAnchor: "context#daily-skeleton-fields",
449
+ legacyErrorCode: "daily_skeleton_drift",
450
+ retryable: true,
451
+ },
452
+ "context.vault_unreachable": {
453
+ expected: "primary vault filesystem reachable",
454
+ hint: "The vault path resolved by config.primaryVault is currently unreachable (mount lost, drive not present, etc.). Reads work; writes are blocked until the path returns. Surface the situation to the operator — do NOT retry blindly. See `state.reason` and `state.path` in the response for details.",
455
+ skillAnchor: "context#vault-degraded",
456
+ legacyErrorCode: "primary_vault_unreachable",
457
+ retryable: false,
458
+ },
459
+ "context.migration_in_progress": {
460
+ expected: "no /api/setup/migrate-context run in flight",
461
+ hint: "Context writes are temporarily globally blocked because the setup migration is moving the data directory. Wait until the migration completes (typically <10s for normal installs; up to a minute for large vaults) and retry. Reads remain available.",
462
+ skillAnchor: "context#migration-gate",
463
+ legacyErrorCode: "migration_in_progress",
464
+ retryable: true,
465
+ },
466
+ "context.stub_target_unsupported": {
467
+ expected: "one of the REPAIRABLE_STUB_TARGETS paths",
468
+ hint: "POST /api/context/repair-stub only accepts a curated allow-list of paths (e.g. 'rules/policies/_index', 'user/_index'). The response's expected list (if surfaced) or the REPAIRABLE_STUB_TARGETS constant in context-health.ts is authoritative. For any other file, PUT the content directly via /api/context/<path>.",
469
+ skillAnchor: "context#stub-repair",
470
+ legacyErrorCode: "unsupported_stub_target",
471
+ retryable: false,
472
+ },
473
+ "context.template_unavailable": {
474
+ expected: "agent-assets/templates directory resolvable",
475
+ hint: "Skeleton template root unresolved on this install — typically a setup-time misconfiguration (agent-assets/ not bundled, or PA_DATA_DIR points at a wrong tree). The agent has no recovery path here: skip the stub-repair step, continue the routine with the unrepaired stub, and surface ONE notification to the user suggesting `aitne doctor`. Do NOT loop on this code.",
476
+ skillAnchor: "context#stub-repair",
477
+ legacyErrorCode: "templates_unavailable",
478
+ retryable: false,
479
+ },
480
+ "context.template_not_found": {
481
+ expected: "matching template file under agent-assets/templates/",
482
+ hint: "No skeleton template exists for the requested stub path. Either the path you passed is wrong, or this particular stub doesn't have a template (and should be authored directly). Check REPAIRABLE_STUB_TARGETS for the supported set.",
483
+ skillAnchor: "context#stub-repair",
484
+ legacyErrorCode: "template_not_found",
485
+ retryable: false,
486
+ },
487
+ "context.append_only_violation": {
488
+ expected: "section header retained — only body content may change",
489
+ hint: "This file is append-only (e.g. agent/journal.md uses mode='append_to_file' or mode='append'). PATCH replace/clear modes are rejected. Use mode='append' or mode='append_to_file' to add content; never overwrite past entries.",
490
+ skillAnchor: "context#patch-modes",
491
+ legacyErrorCode: "append_only",
492
+ retryable: false,
493
+ },
494
+ "context.write_conflict": {
495
+ expected: "If-Match / parent revision matching current on-disk state",
496
+ hint: "PATCH rejected — the file changed since you fetched it. Recovery in 3 steps: (1) GET /api/context/<path> again and capture the new `ETag` response header; (2) re-derive your patch against the fresh content (section may have moved, bullets may have been added); (3) re-PATCH with `If-Match: <new ETag>`. Bare curl without the If-Match header always 409s on tracked files (today.md, roadmap.md, projects/*). If the conflict repeats twice on the same field, another writer is racing — wait 10s before the third attempt, or fall back to mode='append_to_file' to dodge the section-level conflict entirely.",
497
+ skillAnchor: "context#write-conflict",
498
+ legacyErrorCode: "conflict",
499
+ retryable: true,
500
+ },
501
+ "context.unsupported_operation": {
502
+ expected: "endpoint capable of handling this file extension",
503
+ hint: "PATCH is unsupported for .base files — they are full-replace only. Use PUT /api/context/<path>.base with the complete new content instead. Other operation/file combinations carry their own message in the response.",
504
+ skillAnchor: "context#file-extensions",
505
+ legacyErrorCode: "unsupported_operation",
506
+ retryable: false,
507
+ },
508
+ "context.section_not_found": {
509
+ expected: "matching ## heading in the file",
510
+ hint: "PATCH targeted a section heading that does not exist in the file. Section matching rules: case-sensitive, exact literal '## <name>' (the leading `## ` and a single space are mandatory; '## X' with two spaces, '### X' deeper, or '##X' tight all miss). Recovery in order: (1) GET the file and read the current ## headings — pick the actual one verbatim; (2) if the section truly belongs in a new heading, use mode='append_to_file' with the heading included in `content` (e.g. `\\n## New Section\\n<body>`); (3) for structural rewrites, PUT the full file body. Do not approximate the heading — title-cased / pluralised guesses keep missing.",
511
+ skillAnchor: "context#patch-modes",
512
+ legacyErrorCode: "section_not_found",
513
+ retryable: false,
514
+ },
515
+ "context.cutoff_required": {
516
+ expected: "ISO date string under 'cutoff' when mode='clear_before'",
517
+ hint: "mode='clear_before' requires a cutoff date so the daemon knows which dated bullets to drop. Use YYYY-MM-DD (or a full ISO timestamp); rows with a stamp older than cutoff are removed.",
518
+ skillAnchor: "context#patch-modes",
519
+ legacyErrorCode: "cutoff_required",
520
+ constraint: { type: "iso8601", required: true },
521
+ },
522
+ // ── /api/obsidian/* — external Obsidian vault proxy. The agent calls
523
+ // these from the `obsidian-*` skills; every entry surfaces a
524
+ // `legacyErrorCode` because adapter tests assert on the bare
525
+ // `data.error === "obsidian_not_running"` shape.
526
+ "obsidian.not_configured": {
527
+ expected: "external Obsidian vault configured in setup",
528
+ hint: "No external Obsidian vault is configured for this install — `externalObsidianVaultPath`/`externalObsidianVaultName` are unset. Stop calling /api/obsidian/* in this session; either route the work to /api/wiki/* (internal vault), /api/context/* (management store), or notify the user that Obsidian is not configured. GET /api/health → integrationStatuses.obsidian for the live state.",
529
+ skillAnchor: "obsidian-vault-rules#external-vault-setup",
530
+ legacyErrorCode: "obsidian_not_configured",
531
+ retryable: false,
532
+ },
533
+ "obsidian.not_running": {
534
+ expected: "Obsidian.app process running on the host",
535
+ hint: "The Obsidian CLI requires the Obsidian desktop app to be running (Catalyst feature). Notify the user that Obsidian is not running and ask them to launch it, then retry after they confirm. Do NOT silently retry — there is no automatic recovery path.",
536
+ skillAnchor: "obsidian-vault-rules#obsidian-must-be-running",
537
+ legacyErrorCode: "obsidian_not_running",
538
+ retryable: false,
539
+ },
540
+ "obsidian.invalid_path": {
541
+ expected: "vault-relative path: word chars / spaces / hyphens / dots / forward slashes, no '..', no leading '/'",
542
+ hint: "Vault paths reject `..`, absolute paths, and characters outside `[a-z0-9 _\\-./CJK]`. Use 'folder/subfolder/My Note' shape (no leading slash, no .md extension required for the CLI). Search via GET /api/obsidian/search?q=… to discover a real path before writing.",
543
+ skillAnchor: "obsidian-vault-rules#path-rules",
544
+ legacyErrorCode: "invalid file path",
545
+ constraint: { type: "string", maxLength: 500 },
546
+ },
547
+ "obsidian.invalid_note_name": {
548
+ expected: "vault-relative note name: same rules as path",
549
+ hint: "Note names for create take the same shape as paths: no '..', no leading '/', characters limited to `[a-z0-9 _\\-./CJK]`. Use 'Daily/2026-05-15' or 'My Note' — the CLI appends .md automatically.",
550
+ skillAnchor: "obsidian-vault-rules#path-rules",
551
+ legacyErrorCode: "invalid note name",
552
+ constraint: { type: "string", maxLength: 500 },
553
+ },
554
+ "obsidian.name_and_content_required": {
555
+ expected: "both 'name' (string) and 'content' (string) in the JSON body",
556
+ hint: "POST /api/obsidian/notes requires `{ name: '<vault-path>', content: '<markdown>' }`. Both fields are mandatory and must be non-empty strings. To create an empty note, pass content: '' (empty string), not omit the field.",
557
+ skillAnchor: "obsidian-vault-rules#create-note",
558
+ legacyErrorCode: "name and content are required",
559
+ constraint: { type: "object", required: true },
560
+ },
561
+ "obsidian.content_required": {
562
+ expected: "'content' string in the JSON body",
563
+ hint: "PUT / PATCH /api/obsidian/notes endpoints require `{ content: '<markdown>' }`. Pass an empty string to clear content; omitting the field is a different error. PATCH /obsidian/notes also needs `file`; PATCH /obsidian/daily only needs `content`.",
564
+ skillAnchor: "obsidian-vault-rules#update-note",
565
+ legacyErrorCode: "content is required",
566
+ constraint: { type: "string", required: true },
567
+ },
568
+ "obsidian.file_and_content_required": {
569
+ expected: "both 'file' (vault path) and 'content' (string) in the JSON body",
570
+ hint: "PATCH /api/obsidian/notes (append) requires `{ file: '<vault-path>', content: '<markdown to append>' }`. Use PATCH /api/obsidian/daily (no `file`) to append to today's daily note instead.",
571
+ skillAnchor: "obsidian-vault-rules#append-note",
572
+ legacyErrorCode: "file and content are required",
573
+ constraint: { type: "object", required: true },
574
+ },
575
+ "obsidian.query_required": {
576
+ expected: "non-empty 'q' query parameter",
577
+ hint: "GET /api/obsidian/search?q=<term> needs a non-empty query string. The Obsidian CLI search is FTS-style — pass a phrase, tag like `#project`, or path fragment. Empty string is rejected; use the notes endpoint for path-only enumeration.",
578
+ skillAnchor: "obsidian-vault-rules#search",
579
+ legacyErrorCode: "query parameter 'q' is required",
580
+ constraint: { type: "string", required: true, minLength: 1 },
581
+ },
582
+ "obsidian.not_found": {
583
+ expected: "an existing note at the resolved vault path",
584
+ hint: "The note does not exist. The Obsidian CLI collapses 'no such note' and 'read failure' into one error — GET /api/obsidian/search?q=<word-from-title> to discover the correct path, then retry. Do NOT keep retrying the same path.",
585
+ skillAnchor: "obsidian-vault-rules#read-note",
586
+ legacyErrorCode: "not_found",
587
+ retryable: false,
588
+ },
589
+ "obsidian.upstream_error": {
590
+ expected: "successful Obsidian CLI invocation",
591
+ hint: "Obsidian CLI returned a non-zero error. `message` is the raw CLI stderr — read it before retrying. Branches: 'note locked' / 'vault busy' → another writer is mid-flight, wait 5s and retry ONCE; 'note has unsaved changes' → the note is open in the Obsidian UI with edits the CLI refuses to overwrite, notify the user with 'Please save or close <path> in Obsidian, then retry' and skip this write; 'unauthorized' / 'token expired' → CLI auth lost, notify with 'Re-link Obsidian CLI: open Obsidian → Settings → Catalyst → CLI → regenerate token' and stop calling /api/obsidian/* this turn; 'plugin not running' → user closed Obsidian, see obsidian.not_running. Do NOT loop without changing call shape.",
592
+ skillAnchor: "obsidian-vault-rules#errors",
593
+ legacyErrorCode: "obsidian_error",
594
+ retryable: true,
595
+ },
596
+ // ── /api/calendar/* — Google Calendar + Outlook Calendar (direct mode).
597
+ // Delegated/native mode is gated by the route-gate middleware
598
+ // before these handlers run; the codes below are reached only in
599
+ // direct mode or when the integration is misconfigured.
600
+ "calendar.not_configured": {
601
+ expected: "Google Calendar configured (direct mode) on this install",
602
+ hint: "Google Calendar is not configured for direct access — either OAuth is not set up, the service is disabled, or the integration is in delegated/native mode (in which case the route is already 410-gated). Notify the user that calendar is unavailable and skip the calendar branch of the routine. GET /api/health.integrationStatuses.google.services.calendar shows the live state.",
603
+ skillAnchor: "calendar#configuration",
604
+ legacyErrorCode: "calendar_not_configured",
605
+ retryable: false,
606
+ },
607
+ "calendar.invalid_date_format": {
608
+ expected: "'today' or YYYY-MM-DD date string",
609
+ hint: "The `date` query parameter must be the literal 'today' or a calendar date like '2026-05-15'. Other shapes (ISO timestamps, '5/15/26', RFC-3339) are rejected. Use localDateStr(now, <timezone>) shape.",
610
+ skillAnchor: "calendar#list-events",
611
+ legacyErrorCode: "invalid date format — expected YYYY-MM-DD or 'today'",
612
+ constraint: { type: "string", pattern: "^(today|\\d{4}-\\d{2}-\\d{2})$", required: true },
613
+ },
614
+ "calendar.invalid_send_updates": {
615
+ expected: "one of 'all' | 'externalOnly' | 'none'",
616
+ hint: "The `sendUpdates` query parameter controls invite emails. Pass 'none' (silent — typical for batch agent edits), 'externalOnly' (notify external attendees but not internal), or 'all' (notify everyone). Omit the param to default to 'none'.",
617
+ skillAnchor: "calendar#sendUpdates",
618
+ legacyErrorCode: "invalid sendUpdates — must be 'all', 'externalOnly', or 'none'",
619
+ constraint: { type: "enum", enum: ["all", "externalOnly", "none"] },
620
+ },
621
+ "calendar.not_found": {
622
+ expected: "an existing event at the supplied calendarId + eventId",
623
+ hint: "Google returned 404 for this event. The event may have been deleted by another client, or the eventId is wrong. Re-list with GET /api/calendar/events?date=<day> to find the current id, or skip if the event no longer exists.",
624
+ skillAnchor: "calendar#event-id",
625
+ legacyErrorCode: "not_found",
626
+ retryable: false,
627
+ },
628
+ "calendar.upstream_error": {
629
+ expected: "successful Google Calendar API call",
630
+ hint: "Google Calendar returned an error. `message` is Google's verbatim text. Branches: 429 / 'rateLimitExceeded' / 'userRateLimitExceeded' → backoff 60s and retry ONCE, then skip calendar for this turn. 412 / 'preconditionFailed' → another client edited the event between your GET and PATCH; GET the event again to refresh the etag and replay the same change ONCE. 401 / 'authError' → OAuth refresh token revoked or expired; notify the user to re-link Google in /settings/integrations and skip — agent cannot refresh interactively. 403 / 'insufficientPermissions' → the integration scope was downgraded; same fix as 401. 404 → event was deleted between your last list and this call; drop it and continue. Cap retries at one per code per turn.",
631
+ skillAnchor: "calendar#errors",
632
+ legacyErrorCode: "calendar_error",
633
+ retryable: true,
634
+ },
635
+ "calendar.validation_error": {
636
+ expected: "request body matching the calendar event schema",
637
+ hint: "Body failed Zod validation. The response's `details` array carries per-field issues — read each `path` + `message` and fix. Common issues: start/end must be `{ dateTime: ISO, timeZone: 'IANA/Name' }` OR `{ date: 'YYYY-MM-DD' }` (all-day), not both; attendees must be array of `{ email }` objects.",
638
+ skillAnchor: "calendar#event-shape",
639
+ legacyErrorCode: "validation_error",
640
+ },
641
+ "calendar.outlook_not_configured": {
642
+ expected: "an active Outlook mail account (calendar reuses its MSAL token)",
643
+ hint: "Outlook Calendar piggybacks on the first active Outlook mail account's MSAL token. Either no Outlook account is set up, or the cached token can't acquire Calendars.ReadWrite. Notify the user to add an Outlook account in /settings/mail and confirm calendar scopes were granted.",
644
+ skillAnchor: "calendar#outlook-setup",
645
+ legacyErrorCode: "outlook_not_configured",
646
+ retryable: false,
647
+ },
648
+ "calendar.outlook_disabled": {
649
+ expected: "outlook_calendar integration mode != 'disabled'",
650
+ hint: "Outlook Calendar is set to 'disabled' in /settings/integrations. Skip the calendar branch for Outlook routines. Don't re-enable from the agent side — the user controls integration modes via the dashboard.",
651
+ skillAnchor: "calendar#outlook-mode",
652
+ legacyErrorCode: "outlook_calendar_disabled",
653
+ retryable: false,
654
+ },
655
+ "calendar.outlook_delegated": {
656
+ expected: "outlook_calendar integration mode = 'direct' for this route",
657
+ hint: "Outlook Calendar is in 'delegated' mode — direct reads are blocked. Route cross-backend work through POST /api/integrations/outlook_calendar/exec (task-mode chokepoint; the legacy /invoke RPC was retired) or wait for the delegated sync worker's next cadence to land observations.",
658
+ skillAnchor: "calendar#outlook-mode",
659
+ legacyErrorCode: "outlook_calendar_delegated",
660
+ retryable: false,
661
+ },
662
+ // ── /api/wiki/* — internal/external wiki. Mostly already structured
663
+ // (forbidden codes, invalid_body with zod issues). Entries below
664
+ // enrich the terse spots and the layer-auth 403s.
665
+ "wiki.not_enabled": {
666
+ expected: "an active wiki_workspaces row matching the URL workspace name",
667
+ hint: "The wiki is opt-in; either no workspace named `<workspace>` exists, or it has `active=0`. Use GET /api/wiki/workspaces to list available workspaces. To enable, open /settings/wiki in the dashboard and click 'Enable Wiki' — the agent cannot enable it.",
668
+ skillAnchor: "wiki-vault-rules#enable",
669
+ legacyErrorCode: "wiki_not_enabled",
670
+ retryable: false,
671
+ },
672
+ "wiki.workspace_not_found": {
673
+ expected: "an existing workspace row, including archived ones for DELETE/PATCH",
674
+ hint: "No workspace named `<workspace>` exists at all (not even archived). GET /api/wiki/workspaces to list ids and names. To create the default workspace, POST /api/wiki/workspaces with an empty body.",
675
+ skillAnchor: "wiki-vault-rules#workspace-crud",
676
+ legacyErrorCode: "not_found",
677
+ retryable: false,
678
+ },
679
+ "wiki.invalid_body": {
680
+ expected: "request body matching the workspace/file/bridge zod schema",
681
+ hint: "Body failed Zod validation. The response's `issues` array carries per-field paths — read each `path`/`message` and fix that field exactly. Common shapes by endpoint: (a) POST /wiki/workspaces external — `{ kind: 'external', rootPath: '<abs>', language?, name? }`; (b) POST /wiki/<ws>/files — `{ path: '<layer>/<slug>.md', content: '<markdown>', frontmatter? }`; (c) PATCH /wiki/<ws>/files/<path> — `{ content: '<markdown>' }`; (d) bridge proposals — `{ rawPath, wikiPath, confidence: 0-1, evidence: string }`. Do NOT retry without changing the listed fields.",
682
+ skillAnchor: "wiki-vault-rules#schemas",
683
+ legacyErrorCode: "invalid_body",
684
+ },
685
+ "wiki.invalid_root_path": {
686
+ expected: "writable directory outside primary vault / external obsidian / dataDir / other wiki",
687
+ hint: "Wiki root path validation rejects paths that overlap with the management vault, the external Obsidian vault, the daemon's data dir, or another active wiki workspace. POST /api/fs/probe?path=<absolute> first to see which collision applies, then pick a sibling path.",
688
+ skillAnchor: "wiki-vault-rules#root-path",
689
+ legacyErrorCode: "invalid_root_path",
690
+ retryable: false,
691
+ },
692
+ "wiki.invalid_json": {
693
+ expected: "syntactically valid JSON body or empty body",
694
+ hint: "POST /api/wiki/workspaces accepts either no body (default-workspace quick path) or a JSON object — anything else fails JSON.parse(). Check for trailing commas, single quotes, or unescaped newlines.",
695
+ skillAnchor: "wiki-vault-rules#workspace-crud",
696
+ legacyErrorCode: "invalid_json",
697
+ },
698
+ "wiki.append_only_raw": {
699
+ expected: "POST to a new path in 10_raw/ (never overwrite)",
700
+ hint: "Files in 10_raw/ are create-only (the source-of-truth layer). POSTing to an existing 10_raw/<slug>.md file is rejected. If you need to add to an existing source, append a new dated note under 30_outputs/ or create a 20_wiki/ article that links to it.",
701
+ skillAnchor: "wiki-vault-rules#layer-rules",
702
+ legacyErrorCode: "append_only",
703
+ retryable: false,
704
+ },
705
+ "wiki.append_only_log": {
706
+ expected: "PATCH to log.md (POST overwrite is rejected)",
707
+ hint: "log.md grows append-only via PATCH. POST overwrites are rejected once log.md exists. Use PATCH /api/wiki/<workspace>/files/log.md with `{ content: '<new line>' }` to add entries.",
708
+ skillAnchor: "wiki-vault-rules#log",
709
+ legacyErrorCode: "append_only",
710
+ retryable: false,
711
+ },
712
+ "wiki.raw_patch_forbidden": {
713
+ expected: "POST (create) rather than PATCH for 10_raw/ files",
714
+ hint: "10_raw/ files cannot be patched after creation — they are immutable sources. Use POST to create a new sibling note instead of mutating an existing one.",
715
+ skillAnchor: "wiki-vault-rules#layer-rules",
716
+ legacyErrorCode: "append_only",
717
+ retryable: false,
718
+ },
719
+ "wiki.invalid_path": {
720
+ expected: "vault-relative path inside a known layer (00_inbox/10_raw/20_wiki/30_outputs/90_meta or log.md)",
721
+ hint: "Wiki paths must live inside the four-layer schema. The classifier rejects unknown roots, paths containing '..' or '\\', and shapes that don't match the per-layer regex (e.g. 30_outputs/ requires `<YYYY-MM-DD>-<kind>-<slug>.md`). See wiki-vault-rules for the layer table.",
722
+ skillAnchor: "wiki-vault-rules#path-shapes",
723
+ legacyErrorCode: "invalid_path",
724
+ },
725
+ "wiki.invalid_layer": {
726
+ expected: "path classifying into a known layer",
727
+ hint: "Path is well-formed but the leading directory doesn't match any of `00_inbox/`, `10_raw/`, `20_wiki/`, `30_outputs/`, `90_meta/`, or the bare `log.md`. Move the file under the right layer for its kind (raw source → 10_raw, article → 20_wiki, etc.).",
728
+ skillAnchor: "wiki-vault-rules#layer-rules",
729
+ legacyErrorCode: "invalid_layer",
730
+ },
731
+ "wiki.file_not_found": {
732
+ expected: "an existing file under the workspace root",
733
+ hint: "GET on a wiki path that does not exist on disk. Use GET /api/wiki/<workspace>/index to list files, or GET /api/wiki/<workspace>/search?q=… to find it by title. POST first to create it if you intended a write.",
734
+ skillAnchor: "wiki-vault-rules#read-file",
735
+ legacyErrorCode: "not_found",
736
+ retryable: false,
737
+ },
738
+ "wiki.not_file": {
739
+ expected: "the resolved path is a regular file",
740
+ hint: "GET hit a directory rather than a file. Append the file name (e.g. `20_wiki/<slug>.md`) instead of the directory path.",
741
+ skillAnchor: "wiki-vault-rules#read-file",
742
+ legacyErrorCode: "not_file",
743
+ retryable: false,
744
+ },
745
+ "wiki.forbidden_missing_process_key": {
746
+ expected: "x-process-key header on every wiki call",
747
+ hint: "Wiki endpoints gate on `x-process-key` for layer-aware auth. The pa-api shim auto-injects this from `PA_PROCESS_KEY`; if you are calling outside a dispatcher session, supply the header explicitly (e.g. `x-process-key: wiki.compile` for compile, `wiki.ingest_url` for raw ingest, `message.dm` for bridge proposals).",
748
+ skillAnchor: "wiki-vault-rules#process-keys",
749
+ legacyErrorCode: "forbidden",
750
+ retryable: false,
751
+ },
752
+ "wiki.forbidden_read": {
753
+ expected: "x-process-key with a `wiki.*` or DM-read prefix",
754
+ hint: "GET on wiki files requires either a wiki-tier process key (`wiki.compile`, `wiki.ask`, …) or a DM-read process key (`message.dm`, `message.mention`, `dashboard.chat`). The agent's current process key did not match either set.",
755
+ skillAnchor: "wiki-vault-rules#process-keys",
756
+ legacyErrorCode: "forbidden",
757
+ retryable: false,
758
+ },
759
+ "wiki.forbidden_write": {
760
+ expected: "process key authorized to write to this layer",
761
+ hint: "Writes are layer-gated: `wiki.ingest_url` → 10_raw/; `wiki.compile` → 20_wiki/ + 90_meta/; `wiki.ask`/`wiki.trace`/`wiki.connect` → 30_outputs/; `wiki.lint` → 90_meta/health/. DM-tier callers can ONLY write bridge files in 10_raw/ AND only when both `bridge_enabled` and `dm_agent_write_enabled` are on. The `code` field disambiguates which gate failed.",
762
+ skillAnchor: "wiki-vault-rules#process-keys",
763
+ legacyErrorCode: "forbidden",
764
+ retryable: false,
765
+ },
766
+ "wiki.import_conflict": {
767
+ expected: "no destination-side filename collisions in the existing vault",
768
+ hint: "Import-migrate aborted because flattened filenames collide with existing files in 20_wiki/. The response's `conflicts` array lists each collision. Re-run with `?allowConflicts=true` to overwrite, or pre-rename the conflicting files in the source vault first.",
769
+ skillAnchor: "wiki-vault-rules#import",
770
+ legacyErrorCode: "import_conflict",
771
+ retryable: true,
772
+ },
773
+ "wiki.import_split_unsupported": {
774
+ expected: "decision = 'adopt' or 'migrate'",
775
+ hint: "The 'split' import decision is reserved for the multi-workspace phase and not yet implemented. Re-POST with `decision: 'adopt'` (keep source layout) or `decision: 'migrate'` (flatten to the standard schema).",
776
+ skillAnchor: "wiki-vault-rules#import",
777
+ legacyErrorCode: "import_split_unsupported",
778
+ retryable: false,
779
+ },
780
+ // ── /api/fs/probe — wiki vault path picker.
781
+ "fs.missing_path": {
782
+ expected: "non-empty 'path' query parameter",
783
+ hint: "GET /api/fs/probe?path=<absolute-path>. The path must be absolute (no `..`, no `~`) and is checked against the wiki/primary-vault collision matrix. POST /api/system/pick-directory is the OS-native picker — use it first to get a valid absolute path.",
784
+ skillAnchor: "wiki-vault-rules#root-path-picker",
785
+ legacyErrorCode: "missing_path",
786
+ constraint: { type: "string", required: true },
787
+ },
788
+ "fs.invalid_path": {
789
+ expected: "absolute path that is not on the system blocklist",
790
+ hint: "The path failed `normalizeRequestedPath()`: it was relative, contained `..` traversal, started with a system prefix (`/etc`, `/var`, `/System`, …), or matched a known secret-file pattern. Pick a path inside the user's home directory or an external mount; the response's `error`/`message` carries the specific reason.",
791
+ skillAnchor: "wiki-vault-rules#root-path-picker",
792
+ retryable: false,
793
+ },
794
+ // ── /api/chat/attachments — Phase 1 attachment upload/download.
795
+ "attachments.too_many_uploads": {
796
+ expected: "fewer than 5 concurrent uploads per principal",
797
+ hint: "Throttled — there are already 5 in-flight uploads on this auth key. Wait for one to complete (200/4xx response) before starting another. Multipart parses count from the first byte until the response is sent.",
798
+ skillAnchor: "attachments#upload",
799
+ legacyErrorCode: "too_many_uploads",
800
+ retryable: true,
801
+ },
802
+ "attachments.invalid_content_type": {
803
+ expected: "Content-Type: multipart/form-data; boundary=…",
804
+ hint: "Attachment uploads must be multipart/form-data with a single `file` field. application/json, x-www-form-urlencoded, and raw binary are rejected. Use `curl -F 'file=@<path>' -F 'caption=<text>'` for shell uploads.",
805
+ skillAnchor: "attachments#upload",
806
+ legacyErrorCode: "invalid_request",
807
+ retryable: false,
808
+ },
809
+ "attachments.invalid_multipart": {
810
+ expected: "well-formed multipart body with at least one 'file' field",
811
+ hint: "Busboy failed to parse the request — the multipart boundary is malformed, the body was empty before the file field, or the stream closed early. Re-send with a clean -F flag; do not concatenate two -F uploads in one request.",
812
+ skillAnchor: "attachments#upload",
813
+ legacyErrorCode: "invalid_multipart",
814
+ retryable: true,
815
+ },
816
+ "attachments.missing_turn_token": {
817
+ expected: "X-Turn-Token header from the currently-running session",
818
+ hint: "Outbound (agent→user) attachment uploads require X-Turn-Token. The dispatcher mints one per session and the pa-api shim auto-injects `PA_TURN_TOKEN`. If you're calling outside a session, this endpoint is the wrong one — use POST /api/chat/attachments (inbound) instead.",
819
+ skillAnchor: "attachments#outbound",
820
+ legacyErrorCode: "missing_turn_token",
821
+ retryable: false,
822
+ },
823
+ "attachments.invalid_turn_token": {
824
+ expected: "X-Turn-Token bound to an active session",
825
+ hint: "The supplied token doesn't correspond to a currently-running turn. The dispatcher rotates tokens per turn; an attachment from a previous turn is rejected. Re-fetch the token from the current session env (`PA_TURN_TOKEN`) and retry.",
826
+ skillAnchor: "attachments#outbound",
827
+ legacyErrorCode: "invalid_turn_token",
828
+ retryable: false,
829
+ },
830
+ "attachments.ingest_rejected": {
831
+ expected: "file matching the accepted MIME / size constraints",
832
+ hint: "The attachment store rejected the file. The `error` field carries the IngestRejectedError reason (disallowed_mime, too_large, mime_mismatch, magic_bytes_mismatch, …); the `message` is verbatim. Common cases: images > 5 MB, non-image > 25 MB, declared application/pdf but magic bytes say PNG.",
833
+ skillAnchor: "attachments#limits",
834
+ retryable: false,
835
+ },
836
+ "attachments.ingest_failed": {
837
+ expected: "successful blob write",
838
+ hint: "Internal error storing the attachment. Check daemon logs for the stack; not retryable from the agent side. Notify the user and skip the attach step rather than looping.",
839
+ skillAnchor: "attachments#errors",
840
+ legacyErrorCode: "ingest_failed",
841
+ retryable: false,
842
+ },
843
+ "attachments.not_found": {
844
+ expected: "an existing attachment id from the recent upload response",
845
+ hint: "GET / DELETE /chat/attachments/:id for an unknown id. Attachment ids are returned from POST /chat/attachments — store the id immediately and quote it verbatim. Attachments may also have been pruned (TTL).",
846
+ skillAnchor: "attachments#crud",
847
+ legacyErrorCode: "not_found",
848
+ retryable: false,
849
+ },
850
+ "attachments.already_bound": {
851
+ expected: "attachment not yet bound to a message",
852
+ hint: "DELETE on an attachment that's already attached to a sent message is rejected — deletion would break the message thread. Detach via the chat history flow instead, or leave it in place.",
853
+ skillAnchor: "attachments#crud",
854
+ legacyErrorCode: "already_bound",
855
+ retryable: false,
856
+ },
857
+ // ── /api/github/* and /webhook/github — webhook receiver + API proxy.
858
+ "github.rate_limited": {
859
+ expected: "fewer than 60 requests per minute per peer (300 global)",
860
+ hint: "Webhook receiver rate limit hit (per-peer 60 req/min, global 300 req/min). GitHub auto-retries webhook deliveries with backoff, so this is harmless for real deliveries. If the agent itself is calling here (replay/testing), do NOT tight-loop — drop the GitHub branch of this routine and continue with the rest. Retry the same call only after >=60s have elapsed.",
861
+ skillAnchor: "github#webhook",
862
+ legacyErrorCode: "rate_limited",
863
+ retryable: true,
864
+ },
865
+ "github.webhook_not_configured": {
866
+ expected: "PA_GITHUB_WEBHOOK_SECRET set in the keychain",
867
+ hint: "The webhook secret is unset, so signature verification cannot run. The user must set it in /settings/integrations → GitHub. Skip webhook-related work until the secret is configured.",
868
+ skillAnchor: "github#webhook-setup",
869
+ legacyErrorCode: "webhook_not_configured",
870
+ retryable: false,
871
+ },
872
+ "github.invalid_signature": {
873
+ expected: "X-Hub-Signature-256 matching HMAC-SHA256(body, secret)",
874
+ hint: "Signature mismatch — either the wrong secret was configured or the request was tampered with. Verify the GitHub webhook secret in /settings/integrations matches the one configured on github.com side.",
875
+ skillAnchor: "github#webhook-auth",
876
+ legacyErrorCode: "invalid_signature",
877
+ retryable: false,
878
+ },
879
+ "github.missing_event_type": {
880
+ expected: "X-GitHub-Event header set",
881
+ hint: "Webhook payload received without the X-GitHub-Event header. Real GitHub deliveries always set it; if you are replaying for testing, add it to the request.",
882
+ skillAnchor: "github#webhook",
883
+ legacyErrorCode: "missing_event_type",
884
+ retryable: false,
885
+ },
886
+ "github.invalid_json": {
887
+ expected: "syntactically valid JSON webhook body",
888
+ hint: "The webhook body failed JSON.parse(). Genuine GitHub webhooks are always valid JSON; if you are replaying, check the body wasn't double-encoded or truncated.",
889
+ skillAnchor: "github#webhook",
890
+ legacyErrorCode: "invalid_json",
891
+ retryable: false,
892
+ },
893
+ "github.not_configured": {
894
+ expected: "GitHub token in the OS keychain",
895
+ hint: "No GitHub token is stored. The user must add one in /settings/integrations → GitHub → 'Add token'. Until then, /api/github/* proxy routes 503 — skip the GitHub branch of any routine.",
896
+ skillAnchor: "github#token",
897
+ legacyErrorCode: "github_not_configured",
898
+ retryable: false,
899
+ },
900
+ "github.repository_not_found": {
901
+ expected: "repository id or slug registered in the unified repositories table",
902
+ hint: "The supplied `repo=`/`repositoryId=` does not match any row. Use GET /api/repositories to list registered repos; the value can be a row id, a local absolute path, or `github:owner/repo`. To register a new repo, POST /api/repositories.",
903
+ skillAnchor: "repositories#identifiers",
904
+ legacyErrorCode: "repository_not_found",
905
+ retryable: false,
906
+ },
907
+ "github.repository_not_registered": {
908
+ expected: "(owner, repo) pair registered in the unified repositories table",
909
+ hint: "The combination of owner/repo does not match a registered repository. Register the repo first via POST /api/repositories, then retry. Owner+repo must be the canonical GitHub slug — branches and forks need their own rows.",
910
+ skillAnchor: "repositories#github-side",
911
+ legacyErrorCode: "repository_not_registered",
912
+ retryable: false,
913
+ },
914
+ "github.side_required": {
915
+ expected: "repository row with non-null githubOwner + githubRepo",
916
+ hint: "The repository is registered locally but has no GitHub side (it's a `local:*` row). GitHub API calls only work on rows with a GitHub slug; either register the GitHub side via PATCH /api/repositories/:id or pick a different repo.",
917
+ skillAnchor: "repositories#dual-sided",
918
+ legacyErrorCode: "github_side_required",
919
+ retryable: false,
920
+ },
921
+ "github.validation_error": {
922
+ expected: "owner+repo or repositoryId in the request",
923
+ hint: "Specify the target repo either as `?owner=<owner>&repo=<repo>` or `?repositoryId=<id-or-slug>`. The dual form exists for legacy callers; new code should pass `repositoryId` only.",
924
+ skillAnchor: "repositories#identifiers",
925
+ legacyErrorCode: "validation_error",
926
+ retryable: false,
927
+ },
928
+ "github.pull_number_and_comment_required": {
929
+ expected: "'pull_number' (number) and 'comment' (string) in the JSON body",
930
+ hint: "POST /api/github/pulls/comment requires `{ owner, repo, pull_number, comment }` (or `repositoryId` instead of owner+repo). pull_number is the PR number (not the global issue id). comment is the markdown body of the new comment.",
931
+ skillAnchor: "github#pull-comment",
932
+ legacyErrorCode: "pull_number and comment are required",
933
+ constraint: { type: "object", required: true },
934
+ },
935
+ "github.api_error": {
936
+ expected: "successful Octokit response",
937
+ hint: "GitHub API returned an error. Inspect `message` (Octokit's verbatim error text) AND `status` (the HTTP code) to disambiguate. Common cases: 401 (token expired — notify the user to refresh in /settings/integrations → GitHub, then skip; the agent cannot self-recover); 403 (secondary rate limit — wait 60s and retry once, then skip if it persists); 404 (repo/PR deleted or token lost visibility — verify the slug, do NOT loop); 422 (validation — `message` names the offending field; typical causes: closing an already-closed PR, commenting on a locked PR, merge with conflicts; do NOT retry the same body); 5xx (GitHub maintenance — retry once after 60s, then skip).",
938
+ skillAnchor: "github#errors",
939
+ legacyErrorCode: "github_api_error",
940
+ retryable: true,
941
+ },
942
+ // ── /api/git/* — local clone proxy (log/diff/show).
943
+ "git.invalid_repo": {
944
+ expected: "repo identifier resolving to a clone in the unified repositories table",
945
+ hint: "?repo= must be either an absolute local path matching a row's `local_path`, or a repository id (e.g. `github:acme/widgets`, `local:abc123…`). GitHub-only rows without a clone 404 here. The response's `allowed` array (on /git/log) lists currently-registered local paths.",
946
+ skillAnchor: "repositories#local-path",
947
+ legacyErrorCode: "invalid or missing repo",
948
+ retryable: false,
949
+ },
950
+ "git.invalid_ref": {
951
+ expected: "ref free of shell metachars; not starting with '-'",
952
+ hint: "The `?ref=` query parameter is sanitized — it cannot contain `;&|`$` or start with `-` (which would be interpreted as a git flag). Use plain refs like `HEAD~1..HEAD`, `main..feature`, or a 40-char sha range.",
953
+ skillAnchor: "git#diff",
954
+ legacyErrorCode: "invalid ref format",
955
+ retryable: false,
956
+ },
957
+ "git.invalid_hash": {
958
+ expected: "hash free of shell metachars; not starting with '-'",
959
+ hint: "The `?hash=` query parameter is sanitized — same rules as ref. Use a 7-40 char hex sha, or `HEAD`/`HEAD~N`. No flag-style strings.",
960
+ skillAnchor: "git#show",
961
+ legacyErrorCode: "invalid hash format",
962
+ retryable: false,
963
+ },
964
+ "git.exec_failed": {
965
+ expected: "successful git invocation on the resolved clone",
966
+ hint: "git exited non-zero. `message` is git's verbatim stderr — read it first. Branches: 'unknown revision' / 'bad revision' → the ref doesn't exist locally; cannot retry, instead either narrow to a known sha range (HEAD~10..HEAD) or skip this repo for the turn. 'fatal: not a git repository' → the localPath is no longer a clone (deleted or moved); update the row via PATCH /api/repositories/:id. 'fatal: bad object' / 'object file is empty' → repository corruption; surface to the user with 'Run `git fsck` in <localPath>' and skip. 'unable to read tree' on porcelain queries → uncommitted state; either stash or skip the file-tree query. Do NOT auto-retry — git failures are deterministic given the inputs.",
967
+ skillAnchor: "git#errors",
968
+ retryable: false,
969
+ },
970
+ // ── /api/notion/* — Notion CRUD + search/query.
971
+ "notion.not_configured": {
972
+ expected: "Notion integration token in the OS keychain",
973
+ hint: "Notion is not configured. The user must add an integration token in /settings/integrations → Notion and share at least one database with the integration. Until then, /api/notion/* 503s — skip Notion work.",
974
+ skillAnchor: "notion#configuration",
975
+ legacyErrorCode: "notion_not_configured",
976
+ retryable: false,
977
+ },
978
+ "notion.database_not_found": {
979
+ expected: "database label registered in notion_databases config",
980
+ hint: "The `?database=<label>` query did not match a registered alias. The 404 response's `available` array lists configured labels (e.g. 'tasks', 'projects'). To register a new database, edit notion_databases in /settings/integrations → Notion.",
981
+ skillAnchor: "notion#database-labels",
982
+ legacyErrorCode: "database_not_found",
983
+ retryable: false,
984
+ },
985
+ "notion.invalid_json_parameter": {
986
+ expected: "URL-encoded JSON in the 'filter' or 'sorts' query parameters",
987
+ hint: "GET /api/notion/query accepts `?filter=<json>` and `?sorts=<json>`. The JSON must be URL-encoded — use `encodeURIComponent(JSON.stringify(filterObj))`. Notion filter shape: `{ property: '<name>', <type>: { <op>: <value> } }`. Empty filter: omit the param.",
988
+ skillAnchor: "notion#query",
989
+ legacyErrorCode: "invalid_json_parameter",
990
+ retryable: false,
991
+ },
992
+ "notion.invalid_type": {
993
+ expected: "type query parameter = 'page' | 'data_source'",
994
+ hint: "Notion search supports two filter types. Omit the parameter to search both, or pass `type=page` (titles) / `type=data_source` (databases). Anything else is rejected.",
995
+ skillAnchor: "notion#search",
996
+ legacyErrorCode: "invalid_type",
997
+ constraint: { type: "enum", enum: ["page", "data_source"] },
998
+ },
999
+ "notion.invalid_page_id": {
1000
+ expected: "32-hex UUID or 8-4-4-4-12 hyphenated UUID",
1001
+ hint: "Notion page ids are UUIDs — either 32 hex chars without hyphens or canonical 8-4-4-4-12 form. URL-style ids ('My-Page-abc123…') must be stripped of the title prefix; copy the id from the page's Share menu → Copy link → trailing 32-hex string.",
1002
+ skillAnchor: "notion#page-id",
1003
+ legacyErrorCode: "invalid_page_id",
1004
+ constraint: { type: "string", pattern: "^([0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$" },
1005
+ },
1006
+ "notion.not_found": {
1007
+ expected: "an existing Notion page accessible to the configured integration",
1008
+ hint: "Notion returned 404 / object_not_found. Either the id is wrong, the page is in trash, or the integration token doesn't have access to it. Share the parent page/database with the integration from the Notion UI, then retry.",
1009
+ skillAnchor: "notion#permissions",
1010
+ legacyErrorCode: "not_found",
1011
+ retryable: false,
1012
+ },
1013
+ "notion.invalid_parent": {
1014
+ expected: "parent as database label, { database: '<label>' }, { data_source_id: '<uuid>' }, or { page_id: '<uuid>' }",
1015
+ hint: "Page create needs an explicit parent. Three accepted shapes: string database label ('tasks'), `{ database: 'tasks' }`, `{ data_source_id: '<uuid>' }`, or `{ page_id: '<uuid>' }`. Database labels must be pre-registered (404 above) before using them here.",
1016
+ skillAnchor: "notion#create-page",
1017
+ legacyErrorCode: "invalid_parent",
1018
+ constraint: { required: true },
1019
+ },
1020
+ "notion.empty_body": {
1021
+ expected: "at least one field to update in the PATCH body",
1022
+ hint: "PATCH /api/notion/pages/:id rejects empty bodies — supply at least one of properties / icon / cover / in_trash. To rename a page, send `{ properties: { title: [{ text: { content: '<new title>' } }] } }`.",
1023
+ skillAnchor: "notion#update-page",
1024
+ legacyErrorCode: "empty_body",
1025
+ retryable: false,
1026
+ },
1027
+ "notion.content_required": {
1028
+ expected: "'content' string in the PATCH body for append/replace_all/replace_range modes",
1029
+ hint: "PATCH /api/notion/pages/:id/content modes 'append', 'replace_all', 'replace_range' all need `content: '<markdown>'`. 'update' mode uses `updates: [{ oldStr, newStr }]` instead. Pass an empty string for content to clear; do not omit the field.",
1030
+ skillAnchor: "notion#content-modes",
1031
+ constraint: { type: "string", required: true },
1032
+ },
1033
+ "notion.content_range_required": {
1034
+ expected: "'content' AND 'contentRange' strings in 'replace_range' mode",
1035
+ hint: "`mode: 'replace_range'` also needs `contentRange` — the substring of the current page content to replace with `content`. Use 'update' mode if you have many small edits; replace_range is for one contiguous swap.",
1036
+ skillAnchor: "notion#content-modes",
1037
+ constraint: { type: "object", required: true },
1038
+ },
1039
+ "notion.updates_required": {
1040
+ expected: "non-empty 'updates' array in 'update' mode",
1041
+ hint: "`mode: 'update'` needs `updates: [{ oldStr: '<find>', newStr: '<replace>', replaceAll?: false }, …]` with at least one entry. Use this for multi-spot find-and-replace; for a single large edit use 'replace_range' or 'replace_all'.",
1042
+ skillAnchor: "notion#content-modes",
1043
+ constraint: { type: "array", required: true, minimum: 1 },
1044
+ },
1045
+ "notion.updates_element_invalid": {
1046
+ expected: "every updates element has non-empty 'oldStr' (string) and 'newStr' (string)",
1047
+ hint: "Each entry of `updates[]` must carry both fields. oldStr cannot be empty — that would match everywhere. The response's `message` carries the offending index.",
1048
+ skillAnchor: "notion#content-modes",
1049
+ constraint: { type: "object" },
1050
+ },
1051
+ "notion.invalid_mode": {
1052
+ expected: "mode = 'append' | 'replace_all' | 'replace_range' | 'update'",
1053
+ hint: "PATCH content has exactly four modes. Pick: 'append' (add to end), 'replace_all' (rewrite), 'replace_range' (swap one contiguous chunk), 'update' (multi-spot find-replace). Anything else is rejected.",
1054
+ skillAnchor: "notion#content-modes",
1055
+ legacyErrorCode: "invalid_mode",
1056
+ constraint: { type: "enum", enum: ["append", "replace_all", "replace_range", "update"] },
1057
+ },
1058
+ "notion.upstream_error": {
1059
+ expected: "successful Notion API response",
1060
+ hint: "Notion API call failed. The `error` field tells you which operation tripped (query/search/get/create/update/archive/content_update); the `message` is Notion's verbatim API response. Common cases: 401 (token revoked or rotated — notify the user to re-add in /settings/integrations → Notion, then skip Notion work this turn); 403 (page not shared with the integration — guide the user to open the page in Notion → ··· menu → Add connections → pick the integration, then retry once); 404 (page in trash or id wrong — verify with a search call before retrying); 409 (conflict_error on concurrent edits — re-fetch the page and replay the edit once); 429 (rate limit — Notion caps at 3 req/sec/integration; back off 60s then retry once). Do NOT loop without changing the call shape.",
1061
+ skillAnchor: "notion#errors",
1062
+ retryable: true,
1063
+ },
1064
+ // ── /api/mail/* — multi-provider mail proxy (Gmail, Outlook, IMAP, …).
1065
+ "mail.not_configured": {
1066
+ expected: "at least one active mail account in the account registry",
1067
+ hint: "No mail accounts are set up. The user must add one in /settings/mail (Gmail OAuth, Outlook MSAL, IMAP credentials, etc.). Until then, /api/mail/* 503s — skip the mail branch.",
1068
+ skillAnchor: "mail#accounts",
1069
+ legacyErrorCode: "mail_not_configured",
1070
+ retryable: false,
1071
+ },
1072
+ "mail.blob_store_unavailable": {
1073
+ expected: "encrypted blob store wired into the daemon",
1074
+ hint: "Internal config error — the per-account secrets store is missing. The user must run `aitne doctor` to diagnose; agent cannot recover. Skip mail work this turn.",
1075
+ skillAnchor: "mail#secrets",
1076
+ legacyErrorCode: "blob_store_unavailable",
1077
+ retryable: false,
1078
+ },
1079
+ "mail.unsupported_kind": {
1080
+ expected: "kind = 'gmail' | 'outlook' | 'yahoo' | 'icloud' | 'imap'",
1081
+ hint: "Mail account creation needs an explicit provider kind. Pick from the supported list — the request body's `kind` is the dispatch switch. Use 'imap' for any custom IMAP server (Fastmail, ProtonMail bridge, etc.).",
1082
+ skillAnchor: "mail#account-create",
1083
+ legacyErrorCode: "unsupported_kind",
1084
+ constraint: { type: "enum", enum: ["gmail", "outlook", "yahoo", "icloud", "imap"] },
1085
+ },
1086
+ "mail.outlook_client_config_missing": {
1087
+ expected: "OUTLOOK_CLIENT_ID env var or pre-registered tenant config",
1088
+ hint: "Outlook MSAL needs a registered app's client_id. The user must register an app in Azure AD and set OUTLOOK_CLIENT_ID before /api/mail/accounts can create Outlook rows. Skip Outlook setup; notify the user.",
1089
+ skillAnchor: "mail#outlook-setup",
1090
+ legacyErrorCode: "outlook_client_config_missing",
1091
+ retryable: false,
1092
+ },
1093
+ "mail.account_not_found": {
1094
+ expected: "account id from POST /api/mail/accounts",
1095
+ hint: "No mail account row matches this id. GET /api/mail/accounts to list current ids. Note that DELETE returns 204 + the row drops; reusing the same id afterward will 404.",
1096
+ skillAnchor: "mail#account-crud",
1097
+ legacyErrorCode: "not_found",
1098
+ retryable: false,
1099
+ },
1100
+ "mail.oauth_timeout": {
1101
+ expected: "user completes the OAuth consent flow within the timeout",
1102
+ hint: "OAuth pairing timed out waiting for the user to grant consent in the browser. Notify the user that they need to finish the consent flow (open the link the dashboard surfaced) and retry the pair step.",
1103
+ skillAnchor: "mail#oauth",
1104
+ legacyErrorCode: "oauth_timeout",
1105
+ retryable: true,
1106
+ },
1107
+ "mail.invalid_body": {
1108
+ expected: "JSON body matching the per-endpoint shape",
1109
+ hint: "Mail endpoint body missing or malformed — `message` names the specific offender (e.g. 'clientId required', 'read: boolean required', 'add/remove labels required'). Common shapes by endpoint: POST /mail/accounts — `{ kind, displayName?, ... }` (provider-specific extras); PATCH /mail/messages/:id/read — `{ read: boolean }`; PATCH /mail/messages/:id/labels — `{ add?: string[], remove?: string[] }` (at least one); POST /mail/send — see mail.validation_failed for the send contract. Read `message` verbatim, supply the missing field, and resubmit; do NOT retry the same body.",
1110
+ skillAnchor: "mail#request-shapes",
1111
+ legacyErrorCode: "invalid_body",
1112
+ constraint: { type: "object", required: true },
1113
+ },
1114
+ "mail.not_implemented": {
1115
+ expected: "operation supported by the resolved provider",
1116
+ hint: "This operation is not available on the resolved provider. `message` names the gap (e.g. 'untrash not supported on imap', 'listDrafts not supported on imap'). Known gaps: IMAP cannot manage drafts (no DRAFTS folder semantics — compose via SMTP send instead); IMAP cannot untrash once a message hits `\\Deleted` + EXPUNGE; Yahoo/iCloud have no thread API (use search by subject as a fallback); only Gmail exposes label semantics, others use folder moves. If the agent's intent requires this provider's data, skip the operation and surface to the user — do NOT retry the same call.",
1117
+ skillAnchor: "mail#provider-matrix",
1118
+ legacyErrorCode: "not_implemented",
1119
+ retryable: false,
1120
+ },
1121
+ "mail.provider_auth_error": {
1122
+ expected: "valid OAuth/IMAP credentials for the resolved account",
1123
+ hint: "Provider returned 401/403 — credentials are revoked, expired, or insufficient. Notify the user to re-link the account in /settings/mail; the agent cannot refresh OAuth interactively. Skip mail operations until the user confirms.",
1124
+ skillAnchor: "mail#errors",
1125
+ legacyErrorCode: "provider_auth_error",
1126
+ retryable: false,
1127
+ },
1128
+ "mail.validation_failed": {
1129
+ expected: "send body matching the validateSendInput contract",
1130
+ hint: "Send-mail body failed validation. The `errors` array carries per-field issues; fix every listed entry before retrying. Required fields: `to` (non-empty array of string emails or `{ email, name? }` objects), at least one of `subject`/`body`/`draftId`. Optional: `cc`/`bcc` (same shape as `to`), `replyTo` (single recipient), `inReplyTo` (RFC 822 message-id of the parent for thread continuation), `attachments` (array of `{ filename, mimeType, contentBase64 }` — all three are required per attachment). Address fields reject bare names without `@`; encode display names via the `{ email, name }` form, not RFC-2822 `Name <addr>` strings.",
1131
+ skillAnchor: "mail#send",
1132
+ legacyErrorCode: "validation_failed",
1133
+ },
1134
+ "mail.upstream_error": {
1135
+ expected: "successful provider API response",
1136
+ hint: "Mail provider returned an unexpected error. `error` is the per-route tag (`mail_list_failed`, `mail_send_failed`, …); `message` is the provider's verbatim text. Transient = 5xx, ECONNRESET, ETIMEDOUT, IMAP NO/BAD with text mentioning 'temporary' — retry ONCE after 30s, then skip. Permanent = 4xx, IMAP NO with mailbox/permission text, SMTP 5xx — do NOT retry; notify the user with the verbatim `message` and skip the mail branch. After two consecutive retries on the same account, treat as permanent regardless of code.",
1137
+ skillAnchor: "mail#errors",
1138
+ retryable: true,
1139
+ },
1140
+ // ── /api/knowledge/* — knowledge bus inputs.
1141
+ "knowledge.event_bus_unavailable": {
1142
+ expected: "EventBus wired into the API server",
1143
+ hint: "The event bus is not constructed — daemon is starting up or in degraded mode. Wait ~5s and retry; if it persists, the agent should skip and notify the user. Do not loop tight.",
1144
+ skillAnchor: "knowledge#bus",
1145
+ legacyErrorCode: "event_bus_unavailable",
1146
+ retryable: true,
1147
+ },
1148
+ "knowledge.empty_file": {
1149
+ expected: "non-empty file payload",
1150
+ hint: "The uploaded file was 0 bytes after multipart parse — likely a curl typo (`-F 'file=@'` with empty path) or a download race. Re-fetch the source and resubmit.",
1151
+ skillAnchor: "knowledge#upload",
1152
+ legacyErrorCode: "empty_file",
1153
+ constraint: { type: "string", minLength: 1 },
1154
+ },
1155
+ // ── /api/observations/* — Phase 9 pending/consume.
1156
+ "observations.invalid_actor": {
1157
+ expected: "actor = 'user' | 'agent' | 'system'",
1158
+ hint: "The `?actor=` filter accepts only `user`, `agent`, or `system`. `user` is the hourly-check default (excludes agent's own writes). Omit to get all observations.",
1159
+ skillAnchor: "observations#actor-filter",
1160
+ legacyErrorCode: "invalid_actor",
1161
+ constraint: { type: "enum", enum: ["user", "agent", "system"] },
1162
+ },
1163
+ "observations.invalid_json_body": {
1164
+ expected: "syntactically valid JSON consume body",
1165
+ hint: "POST /api/observations/consume body failed JSON parse. The body shape is `{ ids: number[], cursor?: string }`. Verify quoting and array shape before re-sending.",
1166
+ skillAnchor: "observations#consume",
1167
+ legacyErrorCode: "invalid_json_body",
1168
+ retryable: false,
1169
+ },
1170
+ // ── /api/recurring-schedules/* — recurring schedule CRUD.
1171
+ "recurring_schedules.invalid_id": {
1172
+ expected: "positive integer id from GET /api/recurring-schedules",
1173
+ hint: "The `:id` route param must be a positive integer. GET /api/recurring-schedules to list ids. URL-encoded ids and decimal ids are rejected.",
1174
+ skillAnchor: "recurring-schedules#crud",
1175
+ legacyErrorCode: "invalid_id",
1176
+ constraint: { type: "integer", minimum: 1 },
1177
+ },
1178
+ "recurring_schedules.not_found": {
1179
+ expected: "an existing recurring_schedules row",
1180
+ hint: "PATCH/DELETE on an id that doesn't exist. GET /api/recurring-schedules to discover current ids; rows are pruned when the user disables them via the dashboard.",
1181
+ skillAnchor: "recurring-schedules#crud",
1182
+ legacyErrorCode: "not_found",
1183
+ retryable: false,
1184
+ },
1185
+ "recurring_schedules.validation_error": {
1186
+ expected: "body matching recurringScheduleSchema (cron-like + payload)",
1187
+ hint: "Zod validation failed. The `details` field lists per-field issues. Required shape: `{ cron: '<cron expr>', taskType: 'wake'|'dm_session'|…, description: '<20+ chars>', taskContext: {...} }`. Use a cron expression validator (e.g. crontab.guru) before submitting.",
1188
+ skillAnchor: "recurring-schedules#schema",
1189
+ legacyErrorCode: "validation_error",
1190
+ },
1191
+ // ── /api/travel-bookings/* — travel booking CRUD (read-only for agent
1192
+ // except PATCH status / destination). Ingested by the mail observer
1193
+ // via gmail-classifier reservation extraction; the agent rarely
1194
+ // creates these directly.
1195
+ "travel_bookings.invalid_type": {
1196
+ expected: "one of 'flight' | 'hotel' | 'restaurant' | 'train' | 'bus' | 'other'",
1197
+ hint: "The `?type=` filter must be a registered travel kind. The 400 response carries the `validTypes` array verbatim — pick from it or omit the filter to get all kinds. Spelling matters: 'plane' / 'flights' are rejected; use 'flight'.",
1198
+ skillAnchor: "gmail-lifestyle#travel-bookings-filters",
1199
+ legacyErrorCode: "invalid_type",
1200
+ constraint: { type: "enum", enum: ["flight", "hotel", "restaurant", "train", "bus", "other"] },
1201
+ },
1202
+ "travel_bookings.invalid_status": {
1203
+ expected: "one of 'upcoming' | 'completed' | 'cancelled' | 'all' (filter) or 'upcoming' | 'completed' | 'cancelled' (PATCH)",
1204
+ hint: "Status values are constrained. Filter (?status=) accepts 'all' as the no-op; PATCH body { status } does NOT — pick one of the three concrete states. The 400 response's `validStatuses` / `valid` array carries the live list.",
1205
+ skillAnchor: "gmail-lifestyle#travel-bookings-status",
1206
+ legacyErrorCode: "invalid_status",
1207
+ },
1208
+ "travel_bookings.invalid_id": {
1209
+ expected: "positive integer id from GET /api/travel-bookings",
1210
+ hint: "PATCH /api/travel-bookings/:id needs a numeric row id. GET /api/travel-bookings to discover live ids; the response items carry `.id`. URL-encoded and decimal ids are rejected.",
1211
+ skillAnchor: "gmail-lifestyle#travel-bookings-crud",
1212
+ legacyErrorCode: "invalid_id",
1213
+ constraint: { type: "integer", minimum: 1 },
1214
+ },
1215
+ "travel_bookings.invalid_json": {
1216
+ expected: "syntactically valid JSON body",
1217
+ hint: "PATCH body failed JSON.parse(). Check for trailing commas, single quotes, or unescaped newlines. Use jq -nc to validate before piping into curl --data-binary.",
1218
+ skillAnchor: "gmail-lifestyle#travel-bookings-request-shape",
1219
+ legacyErrorCode: "invalid_json",
1220
+ retryable: false,
1221
+ },
1222
+ "travel_bookings.no_updates": {
1223
+ expected: "PATCH body containing at least one of { status, destination }",
1224
+ hint: "PATCH /api/travel-bookings/:id rejects empty bodies. Supply at least one mutable field — currently `status` ('upcoming'|'completed'|'cancelled') or `destination` (string). Other columns are immutable; re-ingest from mail to change them.",
1225
+ skillAnchor: "gmail-lifestyle#travel-bookings-patch",
1226
+ legacyErrorCode: "no_updates",
1227
+ retryable: false,
1228
+ },
1229
+ "travel_bookings.not_found": {
1230
+ expected: "an existing travel_bookings row matching :id",
1231
+ hint: "PATCH targeted a row id that does not exist. GET /api/travel-bookings to discover current ids; rows can be pruned when a cancellation mail is reconciled. Do not retry the same id.",
1232
+ skillAnchor: "gmail-lifestyle#travel-bookings-crud",
1233
+ legacyErrorCode: "not_found",
1234
+ retryable: false,
1235
+ },
1236
+ // ── /api/travel-time/* — Google Maps proxy used by morning/evening
1237
+ // routines to compute leave-by times for calendar events.
1238
+ "travel_time.google_maps_not_configured": {
1239
+ expected: "Google Maps API key in the OS keychain",
1240
+ hint: "Google Maps is not configured. The user must add `PA_GOOGLE_MAPS_API_KEY` via /settings/integrations → Google Maps. Until then, skip the travel-time branch of the routine and notify the user once.",
1241
+ skillAnchor: "gmail-lifestyle#travel-time-configuration",
1242
+ legacyErrorCode: "google_maps_not_configured",
1243
+ retryable: false,
1244
+ },
1245
+ "travel_time.calendar_not_configured": {
1246
+ expected: "calendar integration available (direct mode)",
1247
+ hint: "GET /travel-time/for-event needs the calendar service to fetch the event. Either no calendar is configured, or it's in delegated/native mode. Use GET /api/travel-time?origin=…&destination=… directly with pre-fetched event location instead.",
1248
+ skillAnchor: "gmail-lifestyle#travel-time-for-event",
1249
+ legacyErrorCode: "calendar_not_configured",
1250
+ retryable: false,
1251
+ },
1252
+ "travel_time.origin_and_destination_required": {
1253
+ expected: "non-empty 'origin' AND 'destination' query parameters",
1254
+ hint: "GET /api/travel-time?origin=<addr>&destination=<addr>. Both fields are mandatory and must be non-empty. Geocodable inputs accepted: street address, plus-code, or `lat,lng`. Empty strings are rejected.",
1255
+ skillAnchor: "gmail-lifestyle#travel-time-query",
1256
+ legacyErrorCode: "origin_and_destination_required",
1257
+ constraint: { type: "string", required: true, minLength: 1 },
1258
+ },
1259
+ "travel_time.invalid_mode": {
1260
+ expected: "one of 'driving' | 'transit' | 'walking' | 'bicycling'",
1261
+ hint: "Google Maps Directions API mode whitelist. Use 'transit' for public transport (default), 'driving' for car/taxi, 'walking' for foot, 'bicycling' for bike. The 400 response carries `validModes` verbatim.",
1262
+ skillAnchor: "gmail-lifestyle#travel-time-mode",
1263
+ legacyErrorCode: "invalid_mode",
1264
+ constraint: { type: "enum", enum: ["driving", "transit", "walking", "bicycling"] },
1265
+ },
1266
+ "travel_time.no_route_found": {
1267
+ expected: "Google Maps Directions API returned a route",
1268
+ hint: "No route could be computed between origin and destination. Common causes: addresses don't geocode (typo, no country/region), mode='transit' but no transit exists between the points, or destination is on a different continent. Try `mode=driving` as a fallback, or omit the call and notify the user.",
1269
+ skillAnchor: "gmail-lifestyle#travel-time-errors",
1270
+ legacyErrorCode: "no_route_found",
1271
+ retryable: false,
1272
+ },
1273
+ "travel_time.event_not_found": {
1274
+ expected: "an existing calendar event matching :eventId",
1275
+ hint: "Calendar.getEvent threw — the eventId is invalid or the event has been deleted. Re-list with GET /api/calendar/events?date=<day> to find the current id, or skip if the event no longer exists.",
1276
+ skillAnchor: "gmail-lifestyle#travel-time-for-event",
1277
+ legacyErrorCode: "event_not_found",
1278
+ retryable: false,
1279
+ },
1280
+ "travel_time.event_has_no_location": {
1281
+ expected: "calendar event with a non-empty `location` field",
1282
+ hint: "The event has no `location` set, so there's nowhere to compute travel time to. Either the event is virtual (Zoom/Meet) or the user forgot to add a location. Skip travel-time for this event; mention in the routine summary that location is missing.",
1283
+ skillAnchor: "gmail-lifestyle#travel-time-for-event",
1284
+ legacyErrorCode: "event_has_no_location",
1285
+ retryable: false,
1286
+ },
1287
+ "travel_time.origin_required": {
1288
+ expected: "non-empty 'origin' query parameter",
1289
+ hint: "GET /travel-time/for-event/:eventId needs `?origin=<addr>` explicitly — there is no implicit home/office default in this build. Look up the user's home address from /api/context/user/profile if needed.",
1290
+ skillAnchor: "gmail-lifestyle#travel-time-for-event",
1291
+ legacyErrorCode: "origin_required",
1292
+ constraint: { type: "string", required: true, minLength: 1 },
1293
+ },
1294
+ // ── /api/receipts/* — receipt attachment registry. Mostly read-only; the
1295
+ // agent PATCHes obsidianPath/category when the morning routine files a
1296
+ // receipt under the Obsidian vault. Download proxies to the mail
1297
+ // provider for the originating account.
1298
+ "receipts.invalid_id": {
1299
+ expected: "positive integer id from GET /api/receipts",
1300
+ hint: "The `:id` route param must be a positive integer. GET /api/receipts to list current ids; receipts are pruned when the source mail is deleted. URL-encoded and decimal ids are rejected.",
1301
+ skillAnchor: "gmail-lifestyle#receipts-crud",
1302
+ legacyErrorCode: "invalid_id",
1303
+ constraint: { type: "integer", minimum: 1 },
1304
+ },
1305
+ "receipts.not_found": {
1306
+ expected: "an existing receipts row matching :id",
1307
+ hint: "No receipt row exists with that id. Re-list with GET /api/receipts (filters: ?category=, ?saved=true|false) to find the right id. Rows are removed when the source mail message is purged by IMAP reconciliation.",
1308
+ skillAnchor: "gmail-lifestyle#receipts-crud",
1309
+ legacyErrorCode: "not_found",
1310
+ retryable: false,
1311
+ },
1312
+ "receipts.no_updates": {
1313
+ expected: "PATCH body containing at least one of { obsidianPath, category }",
1314
+ hint: "PATCH /api/receipts/:id needs at least one mutable field. Currently `obsidianPath` (sets saved_at to now) or `category` ('document'|'travel'). Pass both to file a receipt under Obsidian and re-tag in one shot.",
1315
+ skillAnchor: "gmail-lifestyle#receipts-patch",
1316
+ legacyErrorCode: "no_updates",
1317
+ retryable: false,
1318
+ },
1319
+ "receipts.attachment_too_large": {
1320
+ expected: "receipt attachment <= 100 MB",
1321
+ hint: "Download blocked by the 100 MB ceiling to avoid OOM. The `maxBytes` field carries the limit. For attachments above this, ask the user to download via the mail provider's web UI instead. Do not retry — the size won't shrink.",
1322
+ skillAnchor: "gmail-lifestyle#receipts-download-limits",
1323
+ legacyErrorCode: "attachment_too_large",
1324
+ retryable: false,
1325
+ },
1326
+ "receipts.mail_not_configured": {
1327
+ expected: "mail registry available with at least one active account",
1328
+ hint: "Receipt download proxies through the originating mail account. The mail registry is not wired in — either no accounts are configured yet, or the daemon is in degraded mode. Notify the user; skip receipt downloads this turn.",
1329
+ skillAnchor: "gmail-lifestyle#receipts-download",
1330
+ legacyErrorCode: "mail_not_configured",
1331
+ retryable: false,
1332
+ },
1333
+ "receipts.orphaned_receipt": {
1334
+ expected: "receipt row with a non-null account_id linked to an active mail account",
1335
+ hint: "The receipt has account_id=NULL, so the daemon can't pick a provider to fetch the attachment. This typically means the originating mail account was deleted. Either re-ingest the receipt from the new account, or skip the download — there is no recovery from this state without re-ingest.",
1336
+ skillAnchor: "gmail-lifestyle#receipts-account-link",
1337
+ legacyErrorCode: "orphaned_receipt",
1338
+ retryable: false,
1339
+ },
1340
+ "receipts.account_not_found": {
1341
+ expected: "account_id matching an active row in the mail account registry",
1342
+ hint: "The receipt's account_id does not resolve to any registered mail account. The account was likely removed in /settings/mail. Either re-add it (the user must re-link) or skip the download.",
1343
+ skillAnchor: "gmail-lifestyle#receipts-account-link",
1344
+ legacyErrorCode: "account_not_found",
1345
+ retryable: false,
1346
+ },
1347
+ "receipts.attachment_download_not_supported": {
1348
+ expected: "originating provider supports per-attachment download",
1349
+ hint: "This provider (e.g. an IMAP server without UID FETCH attachment selectors) cannot download individual attachments. The `provider` field names the kind. Ask the user to fetch the attachment via the provider's native UI; do not retry.",
1350
+ skillAnchor: "gmail-lifestyle#receipts-download",
1351
+ legacyErrorCode: "attachment_download_not_supported",
1352
+ retryable: false,
1353
+ },
1354
+ "receipts.attachment_not_found": {
1355
+ expected: "provider returns a non-null attachment payload for the recorded (msg, attachment) pair",
1356
+ hint: "The mail provider returned null when asked for the attachment — typically the source message was deleted or moved out of the All Mail label. Skip the download and offer to remove the orphan row by PATCHing category='deleted' or by user action.",
1357
+ skillAnchor: "gmail-lifestyle#receipts-download",
1358
+ legacyErrorCode: "attachment_not_found",
1359
+ retryable: false,
1360
+ },
1361
+ // ── /api/books/* — reading list + highlights. Source of truth for the
1362
+ // `/wiki reading` skill and the morning routine's "what did I read"
1363
+ // block. Read-only except PATCH metadata + import Kindle clippings.
1364
+ "books.invalid_id": {
1365
+ expected: "positive integer id from GET /api/books",
1366
+ hint: "Book id must be a positive integer. GET /api/books?limit=200 to list current ids; URL-encoded and decimal ids are rejected.",
1367
+ skillAnchor: "books#crud",
1368
+ legacyErrorCode: "invalid_id",
1369
+ constraint: { type: "integer", minimum: 1 },
1370
+ },
1371
+ "books.not_found": {
1372
+ expected: "an existing books row matching :id",
1373
+ hint: "No book row exists with that id. GET /api/books to discover current ids, or POST /api/books/import-clippings / import-notebook-html to create rows from Kindle exports.",
1374
+ skillAnchor: "books#crud",
1375
+ legacyErrorCode: "not_found",
1376
+ retryable: false,
1377
+ },
1378
+ "books.invalid_json": {
1379
+ expected: "syntactically valid JSON body",
1380
+ hint: "Body failed JSON.parse(). Check for trailing commas, single quotes, or unescaped newlines in the clipping/HTML text. Use jq -nc to validate before piping into curl --data-binary.",
1381
+ skillAnchor: "books#request-shape",
1382
+ legacyErrorCode: "invalid_json",
1383
+ retryable: false,
1384
+ },
1385
+ "books.invalid_status": {
1386
+ expected: "one of 'reading' | 'completed' | 'abandoned'",
1387
+ hint: "Book status whitelist. The 400 response carries the `valid` array verbatim. To delete a book, use 'abandoned' (no DELETE endpoint by design — keeps the highlights linkable).",
1388
+ skillAnchor: "books#status",
1389
+ legacyErrorCode: "invalid_status",
1390
+ constraint: { type: "enum", enum: ["reading", "completed", "abandoned"] },
1391
+ },
1392
+ "books.rating_must_be_1_to_5": {
1393
+ expected: "integer between 1 and 5 (inclusive) for the `rating` field",
1394
+ hint: "Rating uses a 5-star scale. Pass an integer 1–5; decimals and out-of-range values are rejected. Omit the field if you don't want to rate the book.",
1395
+ skillAnchor: "books#rating",
1396
+ legacyErrorCode: "rating_must_be_1_to_5",
1397
+ constraint: { type: "integer", minimum: 1, maximum: 5 },
1398
+ },
1399
+ "books.no_updates": {
1400
+ expected: "PATCH body containing at least one of { status, rating, notes }",
1401
+ hint: "PATCH /api/books/:id needs at least one mutable field. Currently `status` ('reading'|'completed'|'abandoned'), `rating` (1–5), or `notes` (free string). Setting status='completed' also stamps completed_at automatically.",
1402
+ skillAnchor: "books#patch",
1403
+ legacyErrorCode: "no_updates",
1404
+ retryable: false,
1405
+ },
1406
+ "books.payload_too_large": {
1407
+ expected: "request body <= 10 MB",
1408
+ hint: "Clipping/notebook-HTML imports cap at 10 MB. Typical Kindle My Clippings.txt is under 5 MB; a 10 MB+ payload suggests duplicate concatenation or stray binary data. Split the file or ask the user to re-export.",
1409
+ skillAnchor: "books#import-limits",
1410
+ legacyErrorCode: "payload_too_large",
1411
+ retryable: false,
1412
+ },
1413
+ "books.data_required": {
1414
+ expected: "non-empty 'data' string in the import-clippings body",
1415
+ hint: "POST /api/books/import-clippings requires `{ data: '<clippings.txt content>' }`. Pass the raw text content (the parser handles ==== separators and the standard Kindle header lines).",
1416
+ skillAnchor: "books#import-clippings",
1417
+ legacyErrorCode: "data_required",
1418
+ constraint: { type: "string", required: true, minLength: 1 },
1419
+ },
1420
+ "books.html_required": {
1421
+ expected: "non-empty 'html' string in the import-notebook-html body",
1422
+ hint: "POST /api/books/import-notebook-html requires `{ html: '<email HTML>', subject?, date? }`. Pass the raw HTML payload from the Kindle 'Export Notebook' email; subject/date are used as fallbacks when the HTML lacks an explicit title or timestamp.",
1423
+ skillAnchor: "books#import-notebook-html",
1424
+ legacyErrorCode: "html_required",
1425
+ constraint: { type: "string", required: true, minLength: 1 },
1426
+ },
1427
+ "books.unrecognized_format": {
1428
+ expected: "Kindle notebook HTML matching the parser's known shapes",
1429
+ hint: "HTML didn't match any known Kindle notebook export shape. Three likely causes: (a) Amazon changed the export template — the agent cannot adapt, surface to the user and ask for a re-export; (b) the HTML was already converted to plain text before upload (e.g. via a 'Save as PDF'/'Print to plain text' detour) — request the original .html attachment from the Kindle 'Export Notebook' email; (c) wrong source (Goodreads, Apple Books, etc.) — only Kindle is supported here. Recovery for the user: open https://read.amazon.com → pick the book → Notebook (right rail) → Export → 'Send as email' → forward the .html attachment without modification. Do NOT retry without a different payload.",
1430
+ skillAnchor: "books#import-notebook-html",
1431
+ legacyErrorCode: "unrecognized_format",
1432
+ retryable: false,
1433
+ },
1434
+ // ── /api/repositories/* — unified Git + GitHub repository CRUD,
1435
+ // triggers, and daily management. The agent touches these from
1436
+ // the `repository.*` skills, `git.project.refresh_architecture`,
1437
+ // and the morning routine's "what changed in my repos?" block.
1438
+ // See docs/design/appendices/unified-repositories.md.
1439
+ "repositories.not_found": {
1440
+ expected: "an existing repositories row matching :id, slug, or `github:owner/repo`",
1441
+ hint: "GET /api/repositories to list current rows. The `:id` route param accepts the row id, the `localPath` absolute path, or a `github:<owner>/<repo>` slug. Rows are removed via DELETE — the agent cannot recreate one without explicit user intent.",
1442
+ skillAnchor: "repositories#crud",
1443
+ legacyErrorCode: "not_found",
1444
+ retryable: false,
1445
+ },
1446
+ "repositories.event_bus_unavailable": {
1447
+ expected: "EventBus wired into the API server",
1448
+ hint: "The daemon is starting up or in degraded mode — the EventBus that schedules repository runs is not yet available. Wait ~5s and retry once; if it persists, skip the repository run and notify the user.",
1449
+ skillAnchor: "repositories#run",
1450
+ legacyErrorCode: "event_bus_unavailable",
1451
+ retryable: true,
1452
+ },
1453
+ "repositories.validation_error": {
1454
+ expected: "request body fields satisfying the run/trigger contract",
1455
+ hint: "A required field is missing or has the wrong shape — `message` names the offender (e.g. 'name is required', 'backend must be claude/codex/gemini', 'prompt is required'). Read the message verbatim and supply the field. Most repository POST bodies need at minimum `{ backend, model, workdirMode, prompt }` (run) or `{ name, eventType, backend, model, workdirMode, prompt }` (trigger).",
1456
+ skillAnchor: "repositories#schemas",
1457
+ legacyErrorCode: "validation_error",
1458
+ },
1459
+ "repositories.model_invalid": {
1460
+ expected: "model registered for the chosen backend in process_backend_config",
1461
+ hint: "The (backend, model) pair is rejected because the model isn't listed in the backend's model registry. GET /api/backends/models?backend=<backend> to list valid models for that backend. Common: passing 'sonnet' alias to codex/gemini — they need full ids like 'claude-sonnet-4-6' or 'gemini-2.5-pro'.",
1462
+ skillAnchor: "repositories#run",
1463
+ legacyErrorCode: "model_invalid",
1464
+ retryable: false,
1465
+ },
1466
+ "repositories.local_clone_required": {
1467
+ expected: "repositories row with a non-null localPath",
1468
+ hint: "The operation needs a local git clone — management init/scan/refresh-architecture and `workdirMode='local-clone'` runs all require it. Either POST /api/repositories/:id/link-local first with a clone path, or pick `workdirMode='temp'` for run-in-temp.",
1469
+ skillAnchor: "repositories#dual-sided",
1470
+ legacyErrorCode: "local_clone_required",
1471
+ retryable: false,
1472
+ },
1473
+ "repositories.instruction_required": {
1474
+ expected: "`instructionMd` string when workdirMode='temp'",
1475
+ hint: "run-in-temp needs an explicit instruction markdown — the temp clone has no project context to ground the agent. Either provide `instructionMd` (markdown the agent will read first) or switch to `workdirMode='local-clone'` if a clone is registered.",
1476
+ skillAnchor: "repositories#run",
1477
+ legacyErrorCode: "instruction_required",
1478
+ retryable: false,
1479
+ },
1480
+ "repositories.already_in_flight": {
1481
+ expected: "no in-flight git.project.refresh_architecture row for this repo",
1482
+ hint: "An architecture refresh is already pending or running. Two concurrent runs would race on the overview.md chokepoint write and burn model quota. Poll GET /api/repositories/:id/management → architectureRefresh.status until it leaves 'pending'/'running', or just wait for the next dashboard refresh.",
1483
+ skillAnchor: "repositories#refresh-architecture",
1484
+ legacyErrorCode: "already_in_flight",
1485
+ retryable: true,
1486
+ },
1487
+ "repositories.no_overview": {
1488
+ expected: "overview.md exists for this repository",
1489
+ hint: "PUT /architecture-section can only replace the Architecture block of an existing overview.md. Run POST /api/repositories/:id/management/init first to create the skeleton, then retry the PUT.",
1490
+ skillAnchor: "repositories#architecture-section",
1491
+ legacyErrorCode: "no_overview",
1492
+ retryable: false,
1493
+ },
1494
+ "repositories.payload_too_large": {
1495
+ expected: "architecture-section markdown body within the size limit",
1496
+ hint: "The markdown body exceeds the per-section byte cap. Shorten the section — focus on stable architecture (modules, dataflow, key invariants), not change history. Split overlapping content out into other overview sections via init/scan instead.",
1497
+ skillAnchor: "repositories#architecture-section",
1498
+ legacyErrorCode: "payload_too_large",
1499
+ retryable: false,
1500
+ },
1501
+ "repositories.management_init_failed": {
1502
+ expected: "successful init of git/<slug>/overview.md",
1503
+ hint: "Init failed mid-pipeline (skeleton write OR architecture-refresh enqueue). `message` carries the underlying error verbatim. Diagnose by branch: 'ENOENT' / 'EACCES' on the localPath → user must remount the clone or fix permissions; 'fatal:' from git → the clone is corrupt (suggest `git fsck`); 'UNIQUE constraint failed' → an in-flight init/refresh already exists, wait for it to terminal then re-call; any other text → schema drift, do not loop. Skip the repository for this turn and surface ONE notification with the verbatim `message`.",
1504
+ skillAnchor: "repositories#management-init",
1505
+ legacyErrorCode: "management_init_failed",
1506
+ retryable: false,
1507
+ },
1508
+ "repositories.architecture_refresh_enqueue_failed": {
1509
+ expected: "successful insert of a git.project.refresh_architecture schedule row",
1510
+ hint: "Could not enqueue the architecture-refresh agent run. The underlying error is in `message`. Don't auto-retry — typically a schema/migration drift or an EventBus shutdown. Surface to the user.",
1511
+ skillAnchor: "repositories#refresh-architecture",
1512
+ legacyErrorCode: "architecture_refresh_enqueue_failed",
1513
+ retryable: false,
1514
+ },
1515
+ "repositories.architecture_section_write_failed": {
1516
+ expected: "successful in-process write of overview.md's Architecture block",
1517
+ hint: "The Architecture section write to overview.md failed. `message` carries the underlying file/IO error. Inspect `git/<slug>/overview.md` manually if writable; otherwise surface to the user.",
1518
+ skillAnchor: "repositories#architecture-section",
1519
+ legacyErrorCode: "architecture_section_write_failed",
1520
+ retryable: false,
1521
+ },
1522
+ "repositories.management_scan_failed": {
1523
+ expected: "successful management scan run",
1524
+ hint: "Daily management scan failed mid-run. `message` carries the underlying error. Common causes and the right next step: 'git fetch failed' / network text → clone is offline; skip until network returns, do not retry this turn. 'journal write failed' (EACCES/ENOSPC) → vault is read-only or full; surface to the user with the verbatim path and stop scanning. GitHub rate-limited → the secondary limit hit (see github.rate_limited); next cron tick (1h+) will succeed naturally. The scan row is marked 'failed' for audit even though the row itself is unrecoverable — do NOT enqueue a manual retry, let the daily cron fire.",
1525
+ skillAnchor: "repositories#management-scan",
1526
+ legacyErrorCode: "management_scan_failed",
1527
+ retryable: false,
1528
+ },
1529
+ "repositories.internal_error": {
1530
+ expected: "no unexpected exception inside the store",
1531
+ hint: "RepositoryStore threw an unrecognised error. `message` carries the verbatim exception. This is typically a schema/migration drift; do not retry without operator action.",
1532
+ skillAnchor: "repositories#errors",
1533
+ legacyErrorCode: "internal_error",
1534
+ retryable: false,
1535
+ },
1536
+ // ── /api/mcp/* — MCP server CRUD + probe (B-003). The agent rarely
1537
+ // calls these directly; most are dashboard mutations. Included for
1538
+ // completeness so any agent-initiated probe / status read gets a
1539
+ // structured envelope rather than a bare 4xx.
1540
+ "mcp.not_found": {
1541
+ expected: "an existing mcp_servers row matching :id",
1542
+ hint: "GET /api/mcp/servers to list current ids. MCP server rows are added via POST /api/mcp/servers; the agent cannot create them — surface to the user if a referenced server is missing.",
1543
+ skillAnchor: "mcp#crud",
1544
+ legacyErrorCode: "not_found",
1545
+ retryable: false,
1546
+ },
1547
+ "mcp.invalid_input": {
1548
+ expected: "request body matching the MCP server / patch / secret schema",
1549
+ hint: "Body failed Zod validation. The `issues` array carries per-field paths — fix each one and resubmit. Common: transport must be 'stdio'|'sse'|'http'|'docker'; backends must be a non-empty array of registered backend ids; envKeys/headerKeys must be non-empty strings.",
1550
+ skillAnchor: "mcp#schemas",
1551
+ legacyErrorCode: "invalid_input",
1552
+ },
1553
+ "mcp.duplicate": {
1554
+ expected: "no existing mcp_servers row with the same id",
1555
+ hint: "An MCP server with this id already exists. Either PATCH /api/mcp/servers/:id to update the existing row, or pick a different id (ids are user-visible — e.g. 'my-notion', 'acme-mcp/coda').",
1556
+ skillAnchor: "mcp#duplicate",
1557
+ legacyErrorCode: "duplicate",
1558
+ retryable: false,
1559
+ },
1560
+ "mcp.internal_error": {
1561
+ expected: "no unexpected exception inside the MCP registry",
1562
+ hint: "Internal error during MCP CRUD. Inspect daemon logs for the stack; the route does not surface implementation detail to the agent. Do not retry — typically a schema/migration drift or a corrupted secrets blob.",
1563
+ skillAnchor: "mcp#errors",
1564
+ legacyErrorCode: "internal_error",
1565
+ retryable: false,
1566
+ },
1567
+ "mcp.server_disabled": {
1568
+ expected: "MCP server row with enabled=1",
1569
+ hint: "Probe is rejected because the server is currently disabled. Enable via POST /api/mcp/servers/:id/enable (Approve tier — the dashboard owns this), or notify the user to enable it before retrying.",
1570
+ skillAnchor: "mcp#probe",
1571
+ legacyErrorCode: "server_disabled",
1572
+ retryable: false,
1573
+ },
1574
+ "mcp.probe_failed": {
1575
+ expected: "MCP probe completes without throwing at the transport layer",
1576
+ hint: "The probe transport threw an unexpected error (not a normal `{ ok: false }` probe result, which is persisted normally). `message` carries the verbatim failure. Common: the configured command/binary is missing, the URL is unreachable, or an env-key secret is unset.",
1577
+ skillAnchor: "mcp#probe",
1578
+ legacyErrorCode: "probe_failed",
1579
+ retryable: true,
1580
+ },
1581
+ "mcp.unknown_key": {
1582
+ expected: "keyName declared in the server's envKeys or headerKeys",
1583
+ hint: "Secret keyName must match one of the strings configured on the server. PATCH /api/mcp/servers/:id with the new key in envKeys (for stdio servers) or headerKeys (for HTTP/SSE) before writing the secret value.",
1584
+ skillAnchor: "mcp#secrets",
1585
+ legacyErrorCode: "unknown_key",
1586
+ retryable: false,
1587
+ },
1588
+ "mcp.gemini_cli_not_found": {
1589
+ expected: "`gemini` CLI binary resolvable on the daemon's PATH",
1590
+ hint: "POST /api/mcp/gemini-install needs the Gemini CLI installed and on PATH. Notify the user to install it from https://github.com/google-gemini/gemini-cli and re-run `aitne doctor` to confirm resolution.",
1591
+ skillAnchor: "mcp#gemini-install",
1592
+ legacyErrorCode: "gemini_cli_not_found",
1593
+ retryable: false,
1594
+ },
1595
+ "mcp.install_failed": {
1596
+ expected: "successful `gemini extensions install` / `gemini mcp add` invocation",
1597
+ hint: "Gemini CLI exited non-zero. `stdout`/`stderr` on the response are verbatim — read both. Common branches: 'not authenticated' / 'login required' → user must run `gemini auth login` first, the agent cannot complete OAuth interactively; 'ECONNREFUSED' / 'ENOTFOUND' → npm registry/network blocked, surface and skip; 'already installed' at a different version → DELETE /api/mcp/servers/:id then re-POST with the desired version, OR install via the dashboard's /settings/integrations → MCP card which handles version pinning. Idempotent re-runs short-circuit with `alreadyInstalled: true`, so reaching this code means a real failure — do NOT retry the same call.",
1598
+ skillAnchor: "mcp#gemini-install",
1599
+ legacyErrorCode: "install_failed",
1600
+ retryable: false,
1601
+ },
1602
+ // ── /api/skills/* — user skill CRUD + multipart upload. The agent
1603
+ // doesn't typically write skills (the user owns the curated set),
1604
+ // but skill discovery is read-tier from the dashboard.
1605
+ "skills.invalid_form": {
1606
+ expected: "multipart/form-data body with at least a `file` field",
1607
+ hint: "Skill upload requires multipart/form-data. application/json and x-www-form-urlencoded are rejected. Use `curl -F 'file=@SKILL.md' -F 'name=<slug>'` for shell uploads.",
1608
+ skillAnchor: "skills#upload",
1609
+ legacyErrorCode: "invalid_form",
1610
+ retryable: false,
1611
+ },
1612
+ "skills.file_field_required": {
1613
+ expected: "multipart 'file' part containing the SKILL.md content",
1614
+ hint: "The multipart parsed but did not include a `file` field. Add `-F 'file=@<path>'` (or the equivalent in your client) — the file part is required.",
1615
+ skillAnchor: "skills#upload",
1616
+ legacyErrorCode: "file field required",
1617
+ retryable: false,
1618
+ },
1619
+ "skills.builtin_protected": {
1620
+ expected: "skill name not matching a built-in slug",
1621
+ hint: "Built-in skills are read-only — they ship with the daemon and are owned by `agent-assets/skills/`. Pick a different slug for user skills (e.g. add a prefix like `user-` or `<your-handle>-`). GET /skills/sources to see built-in slugs.",
1622
+ skillAnchor: "skills#builtin-protection",
1623
+ legacyErrorCode: "builtin_protected",
1624
+ retryable: false,
1625
+ },
1626
+ "skills.file_too_large": {
1627
+ expected: "uploaded SKILL.md file <= 256 KB",
1628
+ hint: "SKILL.md uploads cap at 256 KB. A larger file usually means embedded binary content or accidentally-included generated output. Trim the file and re-upload; consider linking out to longer prose rather than inlining.",
1629
+ skillAnchor: "skills#upload-limits",
1630
+ legacyErrorCode: "file_too_large",
1631
+ retryable: false,
1632
+ },
1633
+ "skills.invalid_encoding": {
1634
+ expected: "valid UTF-8 file content",
1635
+ hint: "The uploaded file is not valid UTF-8 — likely a binary file renamed to .md. Re-export as plain UTF-8 text. macOS Notes / Pages exports particularly often include BOMs and surrogate pairs that trip the validator.",
1636
+ skillAnchor: "skills#upload",
1637
+ legacyErrorCode: "invalid_encoding",
1638
+ retryable: false,
1639
+ },
1640
+ "skills.empty_content": {
1641
+ expected: "non-empty SKILL.md body after parsing the frontmatter",
1642
+ hint: "The skill file parsed successfully but has no body content after the YAML frontmatter. Add at least one prose paragraph after the `---` block; that's the actual skill instructions the agent will read.",
1643
+ skillAnchor: "skills#shape",
1644
+ legacyErrorCode: "empty_content",
1645
+ retryable: false,
1646
+ },
1647
+ "skills.invalid_name": {
1648
+ expected: "skill name matching `[a-z0-9][a-z0-9-]*` (kebab-case)",
1649
+ hint: "Skill slugs are restricted to lowercase letters, digits, and hyphens (no underscores, dots, or uppercase). The first character must not be a hyphen. Pick a kebab-case slug such as `my-skill` or `obsidian-graduate-v2`.",
1650
+ skillAnchor: "skills#naming",
1651
+ legacyErrorCode: "invalid_name",
1652
+ retryable: false,
1653
+ },
1654
+ "skills.invalid_description": {
1655
+ expected: "single-line description field (no \\r or \\n)",
1656
+ hint: "Frontmatter description must be one line. Embedded newlines break the regex-based parser the daemon uses to update skills. Re-write the description as a single sentence; long prose goes in the body.",
1657
+ skillAnchor: "skills#frontmatter",
1658
+ legacyErrorCode: "invalid_description",
1659
+ retryable: false,
1660
+ },
1661
+ "skills.validation_error": {
1662
+ expected: "JSON body matching skillCreateSchema / skillUpdateSchema",
1663
+ hint: "Zod validation failed. The `details` array carries per-field paths — fix and resubmit. POST /skills needs `{ name, description, content, allowedTools? }`; PUT /skills/:name accepts a partial { description?, content?, allowedTools? }.",
1664
+ skillAnchor: "skills#schemas",
1665
+ legacyErrorCode: "validation_error",
1666
+ },
1667
+ "skills.not_found": {
1668
+ expected: "an existing skill at agent-assets/skills/<slug>/SKILL.md (built-in) or user-skills/<slug>/SKILL.md",
1669
+ hint: "GET /skills to list current slugs. Built-ins are read-only; user skills can be created via POST /skills or POST /skills/upload. Do not retry the same slug — verify spelling first.",
1670
+ skillAnchor: "skills#crud",
1671
+ legacyErrorCode: "not_found",
1672
+ retryable: false,
1673
+ },
1674
+ "skills.already_exists": {
1675
+ expected: "skill slug not yet present in user-skills/",
1676
+ hint: "A user skill with this slug already exists. Either PUT /skills/:name to update it, or DELETE /skills/:name first if you want a fresh create. POST /skills/upload overwrites by default.",
1677
+ skillAnchor: "skills#crud",
1678
+ legacyErrorCode: "already_exists",
1679
+ retryable: false,
1680
+ },
1681
+ "skills.write_failed": {
1682
+ expected: "successful disk write to the user-skills directory",
1683
+ hint: "Internal write error — the user-skills directory may be on a read-only mount or out of space. `message` carries the verbatim cause. Do not auto-retry; surface to the user.",
1684
+ skillAnchor: "skills#errors",
1685
+ legacyErrorCode: "write_failed",
1686
+ retryable: false,
1687
+ },
1688
+ "skills.delete_failed": {
1689
+ expected: "successful rmSync on the user-skills/<slug>/ directory",
1690
+ hint: "Internal delete error — `message` carries the verbatim cause. Common: file locked by an editor, permission denied. Do not auto-retry; surface to the user.",
1691
+ skillAnchor: "skills#errors",
1692
+ legacyErrorCode: "delete_failed",
1693
+ retryable: false,
1694
+ },
1695
+ // ── /api/managed-tasks/* — recurring management commitments
1696
+ // (`management.md` ↔ `managed_tasks` ↔ `recurring_schedules`).
1697
+ // Agent-callable via the management skill. Codes mirror existing
1698
+ // pre-envelope error strings so the dashboard's history card and
1699
+ // activity-view tests keep matching.
1700
+ "managed_tasks.invalid_id": {
1701
+ expected: "managed-task id matching `mt_<n>` (alphanumeric)",
1702
+ hint: "Managed-task ids are `mt_<n>` (e.g. `mt_1`, `mt_42`). GET /api/managed-tasks to list current ids. Pass the id verbatim — do NOT URL-encode the underscore.",
1703
+ skillAnchor: "managed-tasks#id-format",
1704
+ legacyErrorCode: "invalid_id",
1705
+ retryable: false,
1706
+ },
1707
+ "managed_tasks.not_found": {
1708
+ expected: "an existing managed_tasks row matching :id",
1709
+ hint: "Either the id is wrong or the row was already deleted (DELETE /api/managed-tasks/:id). GET /api/managed-tasks to list current rows. Stopped rows are pruned — `mt_<n>` is not reused; pick a different id rather than re-creating with the same one.",
1710
+ skillAnchor: "managed-tasks#crud",
1711
+ legacyErrorCode: "not_found",
1712
+ retryable: false,
1713
+ },
1714
+ "managed_tasks.validation_error": {
1715
+ expected: "JSON body matching managedTaskCreate/Patch/RunResult schema",
1716
+ hint: "Zod validation failed. `details` carries per-field issues. Common: cadence must match `daily|weekly|monthly|<RRULE>`; intent must be a non-empty string; recurrenceRule must include freq + interval. Read each `path`/`message` and resubmit.",
1717
+ skillAnchor: "managed-tasks#schemas",
1718
+ legacyErrorCode: "validation_error",
1719
+ },
1720
+ "managed_tasks.invalid_limit": {
1721
+ expected: "positive integer <= 200 in the `limit` query parameter",
1722
+ hint: "`?limit=` accepts an integer between 1 and 200. Pass `?limit=50` for the default page size; values above 200 are rejected to keep `agent_actions` scans bounded. URL-encoded or decimal values are rejected.",
1723
+ skillAnchor: "managed-tasks#pagination",
1724
+ legacyErrorCode: "invalid_limit",
1725
+ constraint: { type: "integer", minimum: 1, maximum: 200 },
1726
+ },
1727
+ "managed_tasks.invalid_cursor": {
1728
+ expected: "positive integer in the `before_id` query parameter",
1729
+ hint: "Pagination cursor is the smallest id in the previous page (returned as `nextCursor`). Pass it verbatim — URL-encoded and decimal values are rejected. Omit the parameter for the first page.",
1730
+ skillAnchor: "managed-tasks#pagination",
1731
+ legacyErrorCode: "invalid_cursor",
1732
+ constraint: { type: "integer", minimum: 1 },
1733
+ },
1734
+ "managed_tasks.cap_reached": {
1735
+ expected: "active managed-tasks count below the configured cap",
1736
+ hint: "The active-tasks cap (default 20; configured via `managedTasksMaxActive` in /settings) is hit. Either DELETE one before registering a new task, or raise the cap via /settings → Management. Surface to the user; the cap is intentional.",
1737
+ skillAnchor: "managed-tasks#capacity",
1738
+ legacyErrorCode: "cap_reached",
1739
+ retryable: false,
1740
+ },
1741
+ "managed_tasks.duplicate": {
1742
+ expected: "no managed_tasks row with the same (app_normalized, cadence) pair",
1743
+ hint: "A managed task already covers this (app, cadence) — the response's `item` carries the existing row. To replace it, DELETE the existing id first. To extend it (e.g. change cadence), PATCH /api/managed-tasks/:id on the existing row.",
1744
+ skillAnchor: "managed-tasks#duplicate-handling",
1745
+ legacyErrorCode: "duplicate",
1746
+ retryable: false,
1747
+ },
1748
+ "managed_tasks.internal_error": {
1749
+ expected: "no unexpected exception inside the transaction",
1750
+ hint: "Internal error inside a managed-tasks CRUD/run-now/rename-app/run-result transaction. The error is logged server-side; the route does not surface implementation detail. Do not auto-retry — typically a schema/migration drift.",
1751
+ skillAnchor: "managed-tasks#errors",
1752
+ legacyErrorCode: "internal_error",
1753
+ retryable: false,
1754
+ },
1755
+ // ── /api/apple-calendar/* — iCloud CalDAV provider sibling of
1756
+ // /api/calendar/* (Google). Use this branch when management.md's
1757
+ // SOT for schedule is `apple_calendar`; do NOT cross-call /api/calendar/*.
1758
+ "apple_calendar.not_configured": {
1759
+ expected: "Apple Calendar credentials saved + service initialised",
1760
+ hint: "Apple Calendar is unavailable — either no credentials are saved (POST /apple-calendar/credentials with `{ email, appPassword }`) or the cached service rejected the iCloud handshake. Notify the user to add an app-specific password from https://appleid.apple.com → App-Specific Passwords. Skip Apple Calendar branches of routines until reconfigured.",
1761
+ skillAnchor: "apple-calendar#configuration",
1762
+ legacyErrorCode: "apple_calendar_not_configured",
1763
+ retryable: false,
1764
+ },
1765
+ "apple_calendar.auth_failed": {
1766
+ expected: "iCloud CalDAV accepts the Apple ID + app-specific password",
1767
+ hint: "Apple ID auth failed. Common causes: regular password used instead of an app-specific one; password was revoked from appleid.apple.com; account requires reauth after iCloud security update. Re-issue an app-specific password and POST /apple-calendar/credentials again — there's no silent recovery.",
1768
+ skillAnchor: "apple-calendar#auth",
1769
+ legacyErrorCode: "auth_failed",
1770
+ retryable: false,
1771
+ },
1772
+ "apple_calendar.validation_error": {
1773
+ expected: "request body matching the Apple Calendar schema",
1774
+ hint: "Zod validation failed (`.strict()`) — Apple Calendar deliberately rejects Google-shaped extras like `attendees`, `reminders`, `recurrence`, `visibility`. Drop those fields. The `details` array carries per-field paths. iCloud event create body: `{ summary, start, end, description?, location? }`.",
1775
+ skillAnchor: "apple-calendar#event-shape",
1776
+ legacyErrorCode: "validation_error",
1777
+ },
1778
+ "apple_calendar.invalid_date": {
1779
+ expected: "'today' or YYYY-MM-DD `date` query parameter",
1780
+ hint: "The `?date=` query rejects shapes other than the literal 'today' or a calendar date like '2026-05-15'. Use localDateStr(now, <timezone>) shape — RFC-3339 timestamps are rejected.",
1781
+ skillAnchor: "apple-calendar#list-events",
1782
+ constraint: { type: "string", pattern: "^(today|\\d{4}-\\d{2}-\\d{2})$" },
1783
+ },
1784
+ "apple_calendar.not_found": {
1785
+ expected: "an existing iCloud event matching :id",
1786
+ hint: "The event id does not resolve to any iCloud event. Recurring-instance ids cannot be modified directly (use the master event id). Re-list with GET /apple-calendar/events?date=<day> to find the master id.",
1787
+ skillAnchor: "apple-calendar#event-id",
1788
+ legacyErrorCode: "not_found",
1789
+ retryable: false,
1790
+ },
1791
+ "apple_calendar.recurring_instance_unsupported": {
1792
+ expected: "master event id (not a per-instance recurrence override)",
1793
+ hint: "iCloud CalDAV cannot mutate single instances of a recurring event in this build. Either edit the master (changes apply to the whole series) or skip the modification and notify the user. The `message` field carries the offending instance id verbatim.",
1794
+ skillAnchor: "apple-calendar#recurring",
1795
+ legacyErrorCode: "recurring_instance_unsupported",
1796
+ retryable: false,
1797
+ },
1798
+ "apple_calendar.upstream_error": {
1799
+ expected: "successful iCloud CalDAV round-trip",
1800
+ hint: "iCloud CalDAV returned an error (surfaced as 502 to the agent). `message` is iCloud's verbatim text. Branches: 5xx → Apple maintenance window or transient; retry once after 60s, then skip. 401 → app-specific password revoked or expired; notify the user with this exact prompt: 'Apple Calendar access expired — please go to https://appleid.apple.com → App-Specific Passwords, issue a new password, and paste it into /settings/integrations → Apple Calendar.' Then skip. 412 → etag mismatch from concurrent edit; GET the event again to refresh the etag and replay the change ONCE. Do NOT chain more than one retry per error code in a single turn.",
1801
+ skillAnchor: "apple-calendar#errors",
1802
+ legacyErrorCode: "apple_calendar_error",
1803
+ retryable: true,
1804
+ },
1805
+ // ── /api/agent/*, /api/schedule (legacy), /api/schedule/dm, /api/notify,
1806
+ // /api/agent/regenerate, /api/action/log — leftover agent-route codes
1807
+ // that pre-date the schedule.* batch envelope. Still agent-callable
1808
+ // from skills (manual schedule edits, run-now, regenerate).
1809
+ "agent.daemon_starting": {
1810
+ expected: "daemon startup complete",
1811
+ hint: "Daemon still initialising (typical window: 1-3 s after process spawn). Wait ~3s and retry ONCE; if the second attempt also returns daemon_starting, abandon this turn — startup that takes >30s indicates a stuck migration or a port conflict the agent cannot resolve. Surface ONE notification suggesting `aitne status` / `aitne doctor`, then exit. Never tight-loop on this code.",
1812
+ skillAnchor: "agent#startup-gate",
1813
+ legacyErrorCode: "daemon_starting",
1814
+ retryable: true,
1815
+ },
1816
+ "agent.hourly_check_unavailable": {
1817
+ expected: "triggerHourlyCheck wired into the API server",
1818
+ hint: "POST /agent/run-now/hourly was called before the hourly-check engine was wired (typically only the first ~1s of boot). Wait ~3s and retry.",
1819
+ skillAnchor: "agent#run-now",
1820
+ legacyErrorCode: "hourly_check_unavailable",
1821
+ retryable: true,
1822
+ },
1823
+ "agent.roadmap_maintenance_unavailable": {
1824
+ expected: "triggerRoadmapMaintenance wired into the API server",
1825
+ hint: "Roadmap maintenance dispatcher not yet wired. Wait ~3s and retry; this fires only during boot.",
1826
+ skillAnchor: "agent#roadmap-maintenance",
1827
+ legacyErrorCode: "roadmap_maintenance_unavailable",
1828
+ retryable: true,
1829
+ },
1830
+ "agent.roadmap_maintenance_failed": {
1831
+ expected: "successful roadmap maintenance pass",
1832
+ hint: "Maintenance threw mid-pass; `message` carries the underlying error. Common: roadmap.md lock contention, schema drift, or malformed roadmap row. Do not auto-retry — inspect daemon logs.",
1833
+ skillAnchor: "agent#roadmap-maintenance",
1834
+ legacyErrorCode: "roadmap_maintenance_failed",
1835
+ retryable: false,
1836
+ },
1837
+ "agent.invalid_requested_model": {
1838
+ expected: "'sonnet' | 'opus' | omitted",
1839
+ hint: "`requestedModel` accepts the alias 'sonnet' or 'opus'. Omit to let process_backend_config decide; pin 'opus' for high-complexity multi-file analysis.",
1840
+ skillAnchor: "agent#requestedModel",
1841
+ legacyErrorCode: "invalid_requestedModel",
1842
+ constraint: { type: "enum", enum: ["sonnet", "opus"] },
1843
+ },
1844
+ "agent.notify_validation_error": {
1845
+ expected: "request body matching notifyRequestSchema",
1846
+ hint: "Body failed Zod validation. `details` carries per-field paths. Required: `message` (string). Optional: `platform`/`platforms`, `priority` ('critical'|'high'|'normal'|'low').",
1847
+ skillAnchor: "agent#notify",
1848
+ legacyErrorCode: "validation_error",
1849
+ },
1850
+ "agent.schedule_dm_validation_error": {
1851
+ expected: "request body matching scheduleDmRequestSchema",
1852
+ hint: "POST /schedule/dm body shape: `{ time: ISO8601, message: string, platform?, platforms?, importance? }`. `details` lists per-field paths. Use POST /api/schedule (batch) for LLM runs — /schedule/dm is the no-LLM precomposed-DM path.",
1853
+ skillAnchor: "schedule#dm-shape",
1854
+ legacyErrorCode: "validation_error",
1855
+ },
1856
+ "agent.invalid_time": {
1857
+ expected: "ISO8601 string parseable by Date() and >= now - 60s",
1858
+ hint: "`time` failed Date.parse() or was more than 1 minute in the past. `details` disambiguates which check failed. Use `2026-05-15T14:30:00-04:00` shape with explicit timezone offset.",
1859
+ skillAnchor: "schedule#scheduledFor-bounds",
1860
+ legacyErrorCode: "invalid_time",
1861
+ constraint: { type: "iso8601", required: true },
1862
+ },
1863
+ "agent.invalid_status": {
1864
+ expected: "comma-separated subset of 'pending,running,completed,failed,skipped'",
1865
+ hint: "GET /schedule `?status=` accepts a CSV of the five statuses. `details` names the offending token. Default is 'pending,running'; pass 'completed,failed,skipped' to see history.",
1866
+ skillAnchor: "schedule#status-filter",
1867
+ legacyErrorCode: "invalid_status",
1868
+ },
1869
+ "agent.invalid_id": {
1870
+ expected: "positive integer id from GET /schedule",
1871
+ hint: "`:id` must be a positive integer. GET /schedule for current pending ids. URL-encoded and decimal ids are rejected.",
1872
+ skillAnchor: "schedule#crud",
1873
+ legacyErrorCode: "invalid_id",
1874
+ constraint: { type: "integer", minimum: 1 },
1875
+ },
1876
+ "agent.not_found": {
1877
+ expected: "an existing agent_schedule row matching :id",
1878
+ hint: "PATCH/DELETE on a schedule id that doesn't exist. The row may have been picked up by the scheduler (status moved to running) or already cancelled. GET /schedule for current ids.",
1879
+ skillAnchor: "schedule#crud",
1880
+ legacyErrorCode: "not_found",
1881
+ retryable: false,
1882
+ },
1883
+ "agent.schedule_conflict": {
1884
+ expected: "row in 'pending' status (running/completed/failed are immutable)",
1885
+ hint: "Edits and cancels only work on `pending` schedules. `details` carries the row's current status. Per-state recovery: 'running' → the scheduler has already picked the row up; cannot cancel mid-flight (the running session must finish). If you want a different action, POST /api/schedule with a NEW row scheduled for a later time. 'completed' / 'failed' / 'skipped' → terminal, the row is archive-only; do not retry — its outcome is final. If the conflict was for a cancel intent the user really needs to honour, send them a notification with the schedule id and current status rather than looping.",
1886
+ skillAnchor: "schedule#crud",
1887
+ legacyErrorCode: "conflict",
1888
+ retryable: false,
1889
+ },
1890
+ "agent.invalid_field": {
1891
+ expected: "field set respecting the task_type contract",
1892
+ hint: "PATCH /schedule/:id field/task_type mismatch — `details` names the offending field. Matrix: task_type='wake' or 'dm_session' or 'check' → set `description` and optionally `prompt`, NOT `message`. task_type='dm' (precomposed) → set `message`, NOT `description` and NOT `prompt` (dm rows do not run a model, so prompt would be ignored and is rejected). `time` and `taskContext` are accepted on all types; `model` is accepted on 'wake'/'dm_session'/'check' but ignored on 'dm'. To switch a row's shape, DELETE and re-create rather than PATCH across the dm boundary.",
1893
+ skillAnchor: "schedule#field-by-type",
1894
+ legacyErrorCode: "invalid_field",
1895
+ retryable: false,
1896
+ },
1897
+ "agent.no_changes": {
1898
+ expected: "PATCH body containing at least one mutable field",
1899
+ hint: "PATCH /schedule/:id needs at least one of { time, description, message, prompt, model, taskContext }. Omitting all is rejected.",
1900
+ skillAnchor: "schedule#crud",
1901
+ legacyErrorCode: "no_changes",
1902
+ retryable: false,
1903
+ },
1904
+ "agent.invalid_target": {
1905
+ expected: "target = 'today' | 'roadmap'",
1906
+ hint: "POST /agent/regenerate body needs `{ target: 'today' | 'roadmap' }`. Other values rejected. 'today' refreshes today.md via routine.today_refresh; 'roadmap' refreshes roadmap.md.",
1907
+ skillAnchor: "agent#regenerate",
1908
+ constraint: { type: "enum", enum: ["today", "roadmap"] },
1909
+ },
1910
+ "agent.roadmap_refresh_unavailable": {
1911
+ expected: "triggerRoadmapRefresh wired into the API server",
1912
+ hint: "Roadmap refresh dispatcher not currently wired (boot-time only). Wait ~3s and retry; if persistent, surface to user — daemon is in degraded mode.",
1913
+ skillAnchor: "agent#regenerate",
1914
+ retryable: true,
1915
+ },
1916
+ "agent.event_bus_unavailable": {
1917
+ expected: "EventBus wired into the API server",
1918
+ hint: "EventBus unavailable, so today.md regeneration cannot be dispatched. Wait ~3s and retry; if persistent, run `aitne status`.",
1919
+ skillAnchor: "agent#regenerate",
1920
+ retryable: true,
1921
+ },
1922
+ "agent.action_log_validation_error": {
1923
+ expected: "request body matching actionLogRequestSchema",
1924
+ hint: "Body failed Zod validation. `details` carries per-field paths. Required: `actionType` (string), `result` (string). Use `<resource>.<verb>` shape for actionType (e.g. `mail.archive`).",
1925
+ skillAnchor: "agent#action-log",
1926
+ legacyErrorCode: "validation_error",
1927
+ },
1928
+ // ── /api/profile-questions/* — slot-filled probe for the interview queue.
1929
+ // Read-only helper used by the profile-interview skill to ask "is this
1930
+ // slot already populated in user/profile.md?" before generating a
1931
+ // follow-up question. Bare codes prior to this entry left the agent
1932
+ // looping with no clue what shape the path was supposed to take.
1933
+ "profile_questions.path_required": {
1934
+ expected: "non-empty 'path' query parameter (context-relative)",
1935
+ hint: "GET /api/profile-questions/slot-filled needs `?path=<relative>` — e.g. `?path=user/profile`. Trailing `.md` is optional; `.base` files are rejected (use a prose markdown file). Add `&section=<heading>` to scope to one heading and `&anchor=<bullet-key>` to scope to one bullet.",
1936
+ skillAnchor: "profile-questions#slot-filled",
1937
+ legacyErrorCode: "missing_path",
1938
+ constraint: { type: "string", required: true },
1939
+ },
1940
+ "profile_questions.path_invalid": {
1941
+ expected: "context-relative path inside the vault (no '..', no absolute)",
1942
+ hint: "The path failed traversal validation. Use forward-slash, vault-relative paths only — e.g. `user/profile`, `user/people/colleague-jane`. Reject `..` segments, absolute paths, and `.base` files (those are Obsidian view configs, not prose).",
1943
+ skillAnchor: "profile-questions#slot-filled",
1944
+ legacyErrorCode: "invalid_path",
1945
+ constraint: { type: "string" },
1946
+ retryable: true,
1947
+ },
1948
+ "profile_questions.read_failed": {
1949
+ expected: "readable file at the resolved path",
1950
+ hint: "Filesystem read failed mid-probe (EACCES / EIO). The file exists but the daemon couldn't open it. Likely cause: vault was moved or permissions changed while the daemon was running. Re-check the externalObsidianVaultPath config and retry; if persistent, surface to user — daemon needs vault access.",
1951
+ skillAnchor: "profile-questions#slot-filled",
1952
+ legacyErrorCode: "read_failed",
1953
+ retryable: false,
1954
+ },
1955
+ // ── /api/entities/* — entity registry lookup (tier-1 + tier-2 queries).
1956
+ // Existing `validation_error` / `missing_query` / `ambiguous_query`
1957
+ // paths carry a `message` field already; this registers them so they
1958
+ // flow through the envelope with retryable + hint metadata.
1959
+ "entities.missing_query": {
1960
+ expected: "either tier-1 (source[, external_id]) or tier-2 (domain, type, date) query parameters",
1961
+ hint: "GET /api/entities needs a query shape. Tier-1 exact: `?source=<app>&external_id=<eid>`. Tier-1 bias: `?source=<app>` alone (list all entities for the source so you can pick a dominant domain/type). Tier-2: `?domain=<domain>&type=<singular>&date=YYYY-MM-DD`. Don't mix tiers in one call.",
1962
+ skillAnchor: "entities#query-shape",
1963
+ legacyErrorCode: "missing_query",
1964
+ retryable: true,
1965
+ },
1966
+ "entities.ambiguous_query": {
1967
+ expected: "exactly one tier — not both at the same time",
1968
+ hint: "The request mixed tier-1 (source/external_id) and tier-2 (domain/type/date) parameters. Drop one set: tier-1 is for known external ids (e.g. gmail thread id), tier-2 is for discovery by category. Pick the side that matches your input.",
1969
+ skillAnchor: "entities#query-shape",
1970
+ legacyErrorCode: "ambiguous_query",
1971
+ retryable: true,
1972
+ },
1973
+ "entities.validation_error": {
1974
+ expected: "well-formed query per the tier rules",
1975
+ hint: "A field violated its constraint. Common causes: `external_id` set without `source` (tier-1 needs both for exact match); tier-2 missing one of domain/type/date; `date` not ISO YYYY-MM-DD; `limit` not a positive integer; unknown `domain` / `type` enum value. The `message` field carries the specific violation — read it verbatim and fix that field only.",
1976
+ skillAnchor: "entities#query-shape",
1977
+ legacyErrorCode: "validation_error",
1978
+ retryable: true,
1979
+ },
1980
+ "entities.not_found": {
1981
+ expected: "an entity row matching the supplied path",
1982
+ hint: "GET /api/entities/by-path?path=<...> returned no row. The path is a vault-relative markdown path (e.g. `work/meetings/2026-05-15-launch-review.md`). Check the slug spelling, or use GET /api/entities?source=<app> to discover the canonical path the registrar wrote.",
1983
+ skillAnchor: "entities#by-path",
1984
+ legacyErrorCode: "not_found",
1985
+ retryable: false,
1986
+ },
1987
+ // ── /api/sot-bindings/* — SoT (Source-of-Truth) bindings list
1988
+ // (rules/management.md §A). The PUT replaces the entire list.
1989
+ "sot_bindings.invalid_category": {
1990
+ expected: "non-empty category slug in the URL",
1991
+ hint: "GET /api/sot-bindings/:category needs a trimmed category like `email`, `calendar`, `task`. Empty strings (e.g. `/sot-bindings/%20`) are rejected. List all categories first via GET /api/sot-bindings.",
1992
+ skillAnchor: "sot-bindings#categories",
1993
+ legacyErrorCode: "invalid_category",
1994
+ retryable: true,
1995
+ },
1996
+ "sot_bindings.not_found": {
1997
+ expected: "an existing binding row for the supplied category",
1998
+ hint: "The category has no binding registered yet. Use GET /api/sot-bindings to enumerate the live rows, or PUT /api/sot-bindings to install a new binding (body shape: an array or `{items: [...]}`).",
1999
+ skillAnchor: "sot-bindings#categories",
2000
+ legacyErrorCode: "not_found",
2001
+ retryable: false,
2002
+ },
2003
+ "sot_bindings.validation_error": {
2004
+ expected: "array of binding rows, or `{items: [...]}`",
2005
+ hint: "PUT body must be a JSON array or `{items: [{category, source, ...}]}`. Each row needs `category` (string) and at least one source descriptor. Duplicates by category are rejected as defense-in-depth — the file render is keyed on category position and silent overwrites would corrupt the user's mental model. Read `details`/`message` for the exact field that failed.",
2006
+ skillAnchor: "sot-bindings#put-shape",
2007
+ legacyErrorCode: "validation_error",
2008
+ constraint: { type: "array", required: true },
2009
+ retryable: true,
2010
+ },
2011
+ "sot_bindings.duplicate_category": {
2012
+ expected: "at most one row per category in the PUT body",
2013
+ hint: "Two or more rows shared the same `category` value. De-dupe in the caller — pick the row you want (most specific) and drop the rest. The duplicate's category value is carried on the response as `category` for log triage.",
2014
+ skillAnchor: "sot-bindings#put-shape",
2015
+ legacyErrorCode: "duplicate_category",
2016
+ retryable: true,
2017
+ },
2018
+ "sot_bindings.internal_error": {
2019
+ expected: "successful DB write",
2020
+ hint: "The SoT-bindings table write failed at the SQLite layer (disk full, lock contention, or corruption). Retry once; if persistent, run `aitne doctor` to verify the DB and surface to user.",
2021
+ skillAnchor: "sot-bindings#put-shape",
2022
+ legacyErrorCode: "internal_error",
2023
+ retryable: true,
2024
+ },
2025
+ // ── /api/docs/* — docs corpus search + Q&A bus. The Q&A path is dashboard-
2026
+ // facing but `/docs/by-slug/:slug` is agent-callable for the docs-ask
2027
+ // skill (when the agent is composing answers from indexed prose).
2028
+ "docs.doc_not_found": {
2029
+ expected: "an existing doc with the supplied slug in fts_docs",
2030
+ hint: "No doc indexed under `slug`. Slugs come from doc frontmatter and look like `appendices/voice-transcription` or `design/02-event-pipeline`. Search first via GET /api/docs/search?q=<term> to find the canonical slug, or GET /api/docs to list everything.",
2031
+ skillAnchor: "docs#by-slug",
2032
+ legacyErrorCode: "doc_not_found",
2033
+ retryable: false,
2034
+ },
2035
+ "docs.qa_adapter_unavailable": {
2036
+ expected: "DocsQAAdapter wired into the API server",
2037
+ hint: "The docs-QA bus is not initialized in this daemon instance. Most likely an in-test boot or a degraded startup; the dashboard's QA panel cannot operate. Wait a few seconds and retry; if persistent, run `aitne status` and re-launch the daemon.",
2038
+ skillAnchor: "docs#qa-bus",
2039
+ legacyErrorCode: "qa_adapter_unavailable",
2040
+ retryable: true,
2041
+ },
2042
+ "docs.channel_not_connected": {
2043
+ expected: "channelId minted by GET /docs/qa/stream and still open",
2044
+ hint: "POST /docs/qa/messages was called with a channelId the adapter does not recognise. The channel must be minted by the SSE stream's first event (D5 SSE-first contract). Open GET /docs/qa/stream first, capture the channelId, then POST messages against it. The channel auto-closes when the SSE EventSource disconnects.",
2045
+ skillAnchor: "docs#qa-channel",
2046
+ legacyErrorCode: "channel_not_connected",
2047
+ retryable: false,
2048
+ },
2049
+ "docs.validation_error": {
2050
+ expected: "request body matching qaMessageSchema (channelId, content, scope, …)",
2051
+ hint: "QA message body failed Zod validation. Required fields: `channelId` (UUID from the SSE stream), `content` (non-empty string). Optional: `scope`, `context`, `modelId`. Read `details` for the per-field path.",
2052
+ skillAnchor: "docs#qa-message-shape",
2053
+ legacyErrorCode: "validation_error",
2054
+ constraint: { type: "object", required: true },
2055
+ retryable: true,
2056
+ },
2057
+ "docs.model_not_registered": {
2058
+ expected: "modelId registered for the active QA backend",
2059
+ hint: "The picker-supplied `modelId` is not in the registered list for the resolved QA backend. Drop the `modelId` field (the daemon picks the canonical medium-tier model), or pick a registered id from GET /api/docs/qa/config.",
2060
+ skillAnchor: "docs#qa-model-pick",
2061
+ legacyErrorCode: "model_not_registered",
2062
+ retryable: true,
2063
+ },
2064
+ "docs.model_tier_locked": {
2065
+ expected: "modelId at the medium tier (not lite, not high)",
2066
+ hint: "The QA path is hard-wired to the medium tier to avoid silently draining Opus quota. The supplied `modelId` is registered but at the wrong tier. Pick a medium-tier model from GET /api/docs/qa/config or drop the field entirely.",
2067
+ skillAnchor: "docs#qa-model-pick",
2068
+ legacyErrorCode: "model_tier_locked",
2069
+ retryable: true,
2070
+ },
2071
+ // ── /api/git/templates/* — project-doc template editor + per-file
2072
+ // re-template reporter (Decision 8). The reporter is called by an
2073
+ // autonomous task flow, so good hints here directly shorten the
2074
+ // re-template loop.
2075
+ "git_templates.invalid_kind": {
2076
+ expected: "'project' or 'git-repo' in the :kind URL segment",
2077
+ hint: "The `:kind` segment must be exactly `project` (full project doc template) or `git-repo` (repo-only doc template). Other values (e.g. `repo`, `Git-Repo`) are rejected — these are the only two on-disk templates the daemon ships.",
2078
+ skillAnchor: "git-templates#kinds",
2079
+ legacyErrorCode: "invalid_kind",
2080
+ constraint: { type: "enum", enum: ["project", "git-repo"], required: true },
2081
+ retryable: true,
2082
+ },
2083
+ "git_templates.content_required": {
2084
+ expected: "string body at `content` (the template markdown source)",
2085
+ hint: "PUT /api/git/templates/:kind needs `{ \"content\": \"# Template…\\n…\" }`. The value must be a string — sending a number, array, or omitting the key returns 400. Read the active body first via GET /api/git/templates/:kind to seed your edit.",
2086
+ skillAnchor: "git-templates#editor",
2087
+ legacyErrorCode: "content_required",
2088
+ constraint: { type: "string", required: true },
2089
+ retryable: true,
2090
+ },
2091
+ "git_templates.read_failed": {
2092
+ expected: "readable template file (or absent override = bundled fallback)",
2093
+ hint: "Filesystem read of the template override failed (EACCES / EIO). The bundled template is still usable — drop the override file or PUT a fresh body. `message` carries the underlying fs error verbatim.",
2094
+ skillAnchor: "git-templates#editor",
2095
+ legacyErrorCode: "read_failed",
2096
+ retryable: true,
2097
+ },
2098
+ "git_templates.write_failed": {
2099
+ expected: "successful write to the override path",
2100
+ hint: "Failed to write the template override (disk full, permissions, or path collision). Verify dataDir is writable (`aitne doctor`), then retry. `message` carries the underlying fs error.",
2101
+ skillAnchor: "git-templates#editor",
2102
+ legacyErrorCode: "write_failed",
2103
+ retryable: true,
2104
+ },
2105
+ "git_templates.body_too_large": {
2106
+ expected: "template body ≤ 64 KB",
2107
+ hint: "Template payload exceeded the 64 KB cap. Trim boilerplate, or split into two template kinds. Skill / agent-profile prose belongs in agent-assets/ — only the rendered project doc template lives here.",
2108
+ skillAnchor: "git-templates#editor",
2109
+ legacyErrorCode: "body_too_large",
2110
+ retryable: false,
2111
+ },
2112
+ "git_templates.in_progress": {
2113
+ expected: "no in-flight retemplate run",
2114
+ hint: "A retemplate run is already scheduled / executing (409). Wait for the existing run to finish, or check status via GET /api/git/templates/retemplate/status. The response carries `scheduleId` and `correlationId` for the in-flight run so the dashboard can render 'already running' state.",
2115
+ skillAnchor: "git-templates#apply",
2116
+ legacyErrorCode: "in_progress",
2117
+ retryable: true,
2118
+ },
2119
+ "git_templates.missing_template": {
2120
+ expected: "template file present for the supplied kind",
2121
+ hint: "The retemplate apply call cannot proceed because the template file isn't readable yet (no override AND the bundled fallback is missing — possibly a corrupt install). Run `aitne doctor` or reinstall. The `kind` field on the response identifies which template was missing.",
2122
+ skillAnchor: "git-templates#apply",
2123
+ legacyErrorCode: "missing_template",
2124
+ retryable: false,
2125
+ },
2126
+ "git_templates.no_targets": {
2127
+ expected: "at least one watched git repository for the apply call",
2128
+ hint: "No git repositories are registered for the retemplate scope (422). Add a watched repo via POST /api/repositories first, or pick a different kind. GET /api/repositories?filter=git lists the live set.",
2129
+ skillAnchor: "git-templates#apply",
2130
+ legacyErrorCode: "no_targets",
2131
+ retryable: false,
2132
+ },
2133
+ "git_templates.invalid_slug": {
2134
+ expected: "lowercase kebab-case slug ([a-z0-9]([a-z0-9-]*[a-z0-9])?)",
2135
+ hint: "POST /api/git/templates/retemplate/file body needs `slug` matching `^[a-z0-9][a-z0-9-]*[a-z0-9]$` (or a single `[a-z0-9]` char). Underscores, uppercase, and leading/trailing hyphens are rejected. Slugs come from the apply call's `targets[].slug` — echo that value verbatim.",
2136
+ skillAnchor: "git-templates#per-file-report",
2137
+ legacyErrorCode: "invalid_slug",
2138
+ constraint: { type: "string", pattern: "^[a-z0-9][a-z0-9-]*[a-z0-9]$", required: true },
2139
+ retryable: true,
2140
+ },
2141
+ "git_templates.invalid_status": {
2142
+ expected: "one of 'started' | 'completed' | 'skipped' | 'failed'",
2143
+ hint: "Per-file `status` must be exactly one of the four lifecycle values. `started` is a work-begin marker (no audit row). `completed`/`skipped`/`failed` are terminal — each emits one `agent_actions` row tagged `git.project.retemplate`. Don't invent intermediate states.",
2144
+ skillAnchor: "git-templates#per-file-report",
2145
+ legacyErrorCode: "invalid_status",
2146
+ constraint: { type: "enum", enum: ["started", "completed", "skipped", "failed"], required: true },
2147
+ retryable: true,
2148
+ },
2149
+ "git_templates.no_active_run": {
2150
+ expected: "an active retemplate run before posting per-file status",
2151
+ hint: "No retemplate run is in flight; per-file reports are only accepted while the parent run holds its slot in `runtime_state` (409). The apply call (POST /api/git/templates/:kind/apply) must succeed first and seed the status grid.",
2152
+ skillAnchor: "git-templates#per-file-report",
2153
+ legacyErrorCode: "no_active_run",
2154
+ retryable: false,
2155
+ },
2156
+ "git_templates.correlation_mismatch": {
2157
+ expected: "correlationId matching the in-flight retemplate run",
2158
+ hint: "The supplied `correlationId` does not match the active run's id (409). Another retemplate run started between your apply and this report. Re-read GET /api/git/templates/retemplate/status to confirm the live run's id, then echo that value in subsequent reports.",
2159
+ skillAnchor: "git-templates#per-file-report",
2160
+ legacyErrorCode: "correlation_mismatch",
2161
+ retryable: false,
2162
+ },
2163
+ "git_templates.slug_not_in_run": {
2164
+ expected: "slug present in the active run's target list",
2165
+ hint: "The supplied `slug` is not one of the active run's targets (404). Slugs are pinned at apply time — adding a new target mid-run is not supported. Re-check the slug spelling against the apply response's `targets[].slug` array.",
2166
+ skillAnchor: "git-templates#per-file-report",
2167
+ legacyErrorCode: "slug_not_in_run",
2168
+ retryable: false,
2169
+ },
2170
+ // ── /api/integrations/* — integration mode CRUD + delegated /exec.
2171
+ // Most error paths already carry rich structured fields (key, mode,
2172
+ // supportedModes, …). These entries register the bare 404/500 paths
2173
+ // plus the /exec invalid_json_body that an agent retry could fix.
2174
+ "integrations.unknown_integration": {
2175
+ expected: "integration key registered in the daemon (e.g. gmail, google_calendar, notion)",
2176
+ hint: "The `:key` URL segment is not a registered integration. Get the live list from GET /api/integrations (returns every key the daemon knows about, with mode + backend). Keys are case-sensitive snake_case: `gmail`, `google_calendar`, `outlook_mail`, `outlook_calendar`, `notion` — not `Gmail` / `outlook-mail`.",
2177
+ skillAnchor: "integrations#keys",
2178
+ legacyErrorCode: "unknown_integration",
2179
+ retryable: false,
2180
+ },
2181
+ "integrations.invalid_json_body": {
2182
+ expected: "valid JSON object body (parse-able)",
2183
+ hint: "POST /api/integrations/:key/probe expects either `null`/empty (= cached read), or `{ backend?, tools?, liveProbe? }`. Parse-level failure means the body wasn't valid JSON at all — check Content-Type: application/json and that no trailing commas / unquoted keys snuck in.",
2184
+ skillAnchor: "integrations#probe",
2185
+ legacyErrorCode: "invalid_json_body",
2186
+ constraint: { type: "json", required: true },
2187
+ retryable: true,
2188
+ },
2189
+ "integrations.invalid_limit": {
2190
+ expected: "positive integer (default 50, max 200)",
2191
+ hint: "`?limit=` must parse as an integer ≥ 1. Anything else falls back to 50, but a non-numeric value returns 400. The hard cap is 200 — larger values are silently clamped, smaller positive ints honoured.",
2192
+ skillAnchor: "integrations#recent-proxy-calls",
2193
+ legacyErrorCode: "invalid_limit",
2194
+ constraint: { type: "integer", minimum: 1, maximum: 200 },
2195
+ retryable: true,
2196
+ },
2197
+ "integrations.internal_error": {
2198
+ expected: "successful integration state update / probe persistence",
2199
+ hint: "Server-side failure during integration mode change or probe persistence. Typical causes: a `runtime_state.integration_flip_lock:<key>` left by a crashed flip blocks the next change (resolves after the lock TTL expires, ~60s); descriptor vs DB-row drift (an integration was removed from `packages/shared/src/integrations.ts` but a `direct` row still exists); SQLite write contention during a long-running migration. Retry ONCE after 5s. If still failing, do not loop — stop touching this integration's mode/probe this turn, surface ONE notification quoting the response's `message`, and proceed with other work. The flip lock alone does not block read endpoints, so observation consumption can continue.",
2200
+ skillAnchor: "integrations#internal-errors",
2201
+ legacyErrorCode: "internal_error",
2202
+ retryable: true,
2203
+ },
2204
+ };
2205
+ // ── Helpers ──────────────────────────────────────────────────────────────────
2206
+ const PLACEHOLDER_HINT_PREFIX = "(unregistered code — please register in agent-errors.ts) ";
2207
+ /**
2208
+ * Compose an AgentErrorIssue from a code + call-site context. Pulls the
2209
+ * registry's `hint` / `expected` / `skillAnchor` / `constraint` defaults so
2210
+ * the call site can stay terse:
2211
+ *
2212
+ * ```ts
2213
+ * composeIssue("schedule.scheduled_for_in_past", {
2214
+ * field: "rows[4].scheduledFor",
2215
+ * received: "2026-05-15T03:45:00",
2216
+ * rowIndex: 4,
2217
+ * });
2218
+ * ```
2219
+ *
2220
+ * The call site can override any registry default by passing it explicitly.
2221
+ */
2222
+ export function composeIssue(code, overrides) {
2223
+ const entry = AGENT_ERROR_REGISTRY[code];
2224
+ if (!entry) {
2225
+ return {
2226
+ code,
2227
+ field: overrides.field,
2228
+ received: overrides.received,
2229
+ rowIndex: overrides.rowIndex ?? null,
2230
+ expected: overrides.expected ?? "(unspecified)",
2231
+ constraint: overrides.constraint,
2232
+ validValues: overrides.validValues,
2233
+ hint: overrides.hint ??
2234
+ PLACEHOLDER_HINT_PREFIX +
2235
+ "no registry entry for this code; the caller should register it before shipping.",
2236
+ skillAnchor: overrides.skillAnchor,
2237
+ docsUrl: overrides.docsUrl,
2238
+ severity: overrides.severity ?? "error",
2239
+ };
2240
+ }
2241
+ return {
2242
+ code,
2243
+ field: overrides.field,
2244
+ received: overrides.received,
2245
+ rowIndex: overrides.rowIndex ?? null,
2246
+ expected: overrides.expected ?? entry.expected,
2247
+ constraint: overrides.constraint ?? entry.constraint,
2248
+ validValues: overrides.validValues,
2249
+ hint: overrides.hint ?? entry.hint,
2250
+ skillAnchor: overrides.skillAnchor ?? entry.skillAnchor,
2251
+ docsUrl: overrides.docsUrl ?? entry.docsUrl,
2252
+ severity: overrides.severity ?? entry.severity ?? "error",
2253
+ };
2254
+ }
2255
+ /**
2256
+ * Decide whether the response is retryable. Pure: composed only from the
2257
+ * registry entries for each issue's code, so the decision is deterministic
2258
+ * given the input array.
2259
+ */
2260
+ function computeRetryable(issues) {
2261
+ if (issues.length === 0)
2262
+ return false;
2263
+ for (const issue of issues) {
2264
+ if (issue.severity === "warning")
2265
+ continue;
2266
+ const entry = AGENT_ERROR_REGISTRY[issue.code];
2267
+ if (entry?.retryable === false)
2268
+ return false;
2269
+ }
2270
+ return true;
2271
+ }
2272
+ /**
2273
+ * Resolve the top-level `error` legacy alias for an envelope. Precedence:
2274
+ * 1. explicit `options.legacyErrorCode` override at the call site
2275
+ * 2. the registry entry for the FIRST issue's code (single-issue
2276
+ * responses are the only place a top-level scalar makes sense —
2277
+ * dashboard branches like `body.error === "forbidden"` can only
2278
+ * match one value)
2279
+ * Multi-issue envelopes intentionally drop the legacy alias unless the
2280
+ * call site forces it: there's no sensible way to surface 4 different
2281
+ * legacy strings through one scalar field.
2282
+ */
2283
+ function resolveLegacyAlias(issues, options) {
2284
+ if (options && "legacyErrorCode" in options) {
2285
+ // Allow explicit `null` to opt out even when the registry would set one.
2286
+ return options.legacyErrorCode ?? undefined;
2287
+ }
2288
+ if (issues.length !== 1)
2289
+ return undefined;
2290
+ const entry = AGENT_ERROR_REGISTRY[issues[0].code];
2291
+ return entry?.legacyErrorCode;
2292
+ }
2293
+ /**
2294
+ * Build the response envelope from a list of issues. Exposed for testing;
2295
+ * call sites should use `respondWithAgentError` instead.
2296
+ */
2297
+ export function buildEnvelope(issues, options) {
2298
+ const retryable = options?.retryable ?? computeRetryable(issues);
2299
+ const summary = options?.summary ??
2300
+ (issues.length === 1
2301
+ ? `Request rejected: ${issues[0].code} on ${issues[0].field}.`
2302
+ : `${issues.length} validation errors. Fix the listed errors and retry.`);
2303
+ const envelope = {
2304
+ ok: false,
2305
+ summary,
2306
+ errors: issues,
2307
+ retryable,
2308
+ };
2309
+ const legacy = resolveLegacyAlias(issues, options);
2310
+ if (legacy !== undefined)
2311
+ envelope.error = legacy;
2312
+ if (options?.rowsAttempted !== undefined)
2313
+ envelope.rowsAttempted = options.rowsAttempted;
2314
+ if (options?.rowsCommitted !== undefined)
2315
+ envelope.rowsCommitted = options.rowsCommitted;
2316
+ if (options?.retryHint !== undefined)
2317
+ envelope.retryHint = options.retryHint;
2318
+ if (options?.warnings && options.warnings.length > 0) {
2319
+ envelope.warnings = options.warnings.map((w) => w.severity === "warning" ? w : { ...w, severity: "warning" });
2320
+ }
2321
+ if (options?.legacyFields) {
2322
+ // Reserved keys we never let the call site overwrite — these are the
2323
+ // structured envelope contract. `error` is allowed to pass through so
2324
+ // an explicit `legacyErrorCode` can coexist with a registry default.
2325
+ const reserved = new Set([
2326
+ "ok",
2327
+ "summary",
2328
+ "errors",
2329
+ "warnings",
2330
+ "retryable",
2331
+ "retryHint",
2332
+ "rowsAttempted",
2333
+ "rowsCommitted",
2334
+ ]);
2335
+ for (const [key, value] of Object.entries(options.legacyFields)) {
2336
+ if (reserved.has(key))
2337
+ continue;
2338
+ if (value === undefined)
2339
+ continue;
2340
+ envelope[key] = value;
2341
+ }
2342
+ }
2343
+ return envelope;
2344
+ }
2345
+ /**
2346
+ * Compose the envelope and emit it as a Hono JSON response. The status
2347
+ * code is required so the call site can pick 400 (single-row request
2348
+ * validation) / 422 (per-row batch validation) / 403 (authn) / 404
2349
+ * (row missing) without the helper guessing.
2350
+ */
2351
+ export function respondWithAgentError(c, status, issues, options) {
2352
+ const envelope = buildEnvelope(issues, options);
2353
+ return c.json(envelope, status);
2354
+ }
2355
+ /**
2356
+ * Compose a warning-severity issue. Thin wrapper around `composeIssue`
2357
+ * that pins `severity:"warning"` so call sites populating an
2358
+ * `envelope.warnings[]` or a 2xx success body's `warnings` channel
2359
+ * don't have to remember the severity argument every time.
2360
+ *
2361
+ * Use for inputs that are syntactically valid but suspicious enough to
2362
+ * flag (deprecated model, `daysOfMonth:[31]` with default policy, etc.).
2363
+ * The route still returns 2xx — see
2364
+ * SCHEDULE_API_REDESIGN_PLAN.md §5.0.5 for the contract.
2365
+ */
2366
+ export function composeWarning(code, overrides) {
2367
+ return composeIssue(code, { ...overrides, severity: "warning" });
2368
+ }
2369
+ // ── Zod issue translator ─────────────────────────────────────────────────────
2370
+ //
2371
+ // Maps Zod issues onto registry codes so endpoints that already use Zod
2372
+ // schemas (POST /api/schedule, POST /api/schedule/batch) don't have to
2373
+ // re-implement field-by-field validation prose. Endpoints that need
2374
+ // custom-typed errors (auth-header missing, row-not-found) compose issues
2375
+ // manually via `composeIssue`.
2376
+ //
2377
+ // The mapping is intentionally conservative — only Zod issue codes that
2378
+ // correspond cleanly to a registry code get translated; anything else
2379
+ // falls through to a generic `<namespace>.field_invalid` placeholder so
2380
+ // the call site notices and adds a registry entry.
2381
+ /**
2382
+ * Stringify a Zod issue path into a JSON-pointer-ish string.
2383
+ *
2384
+ * Zod 4 typed paths as `PropertyKey[]` (string | number | symbol). We
2385
+ * render strings/numbers verbatim and best-effort `String(seg)` symbols
2386
+ * — none of the schemas in this repo emit symbol keys, but accepting
2387
+ * the wider type keeps us compatible with the public API surface.
2388
+ *
2389
+ * Output: `rows[0].taskContext.background` (numbers become `[N]`).
2390
+ */
2391
+ export function formatZodPath(path) {
2392
+ let out = "";
2393
+ for (const seg of path) {
2394
+ if (typeof seg === "number") {
2395
+ out += `[${seg}]`;
2396
+ }
2397
+ else if (typeof seg === "symbol") {
2398
+ const s = seg.description ?? seg.toString();
2399
+ out += out.length === 0 ? s : `.${s}`;
2400
+ }
2401
+ else if (out.length === 0) {
2402
+ out = seg;
2403
+ }
2404
+ else {
2405
+ out += `.${seg}`;
2406
+ }
2407
+ }
2408
+ return out;
2409
+ }
2410
+ /**
2411
+ * Heuristic — given a Zod issue at path `rows[i].field.subfield`, return
2412
+ * the leading row index when applicable. Otherwise null.
2413
+ */
2414
+ function extractRowIndex(path) {
2415
+ if (path.length >= 2 && path[0] === "rows" && typeof path[1] === "number") {
2416
+ return path[1];
2417
+ }
2418
+ return null;
2419
+ }
2420
+ const MISSING_SENTINEL = "<missing>";
2421
+ function inferReceivedFromIssue(issue) {
2422
+ // Zod 4 dropped the `received` field on invalid_type issues — the
2423
+ // information leaks only through the issue's `message` ("…received
2424
+ // undefined" / "…received number"). For non-invalid_type codes the
2425
+ // received value is not exposed at all without the original input, so
2426
+ // the call site that needs verbatim received must compose manually.
2427
+ const msg = issue.message;
2428
+ if (typeof msg === "string") {
2429
+ const match = msg.match(/received (\w+)/);
2430
+ if (match) {
2431
+ return match[1] === "undefined" ? MISSING_SENTINEL : match[1];
2432
+ }
2433
+ }
2434
+ return MISSING_SENTINEL;
2435
+ }
2436
+ /**
2437
+ * Zod 4 dropped the `received` field on invalid_type issues, so detect
2438
+ * "missing field" by inspecting the issue's message. The match is
2439
+ * intentionally tight ("received undefined") so a string literal
2440
+ * `"undefined"` payload — which is a wrong-type bug, not a missing one
2441
+ * — is NOT classified as missing.
2442
+ */
2443
+ function isMissingFieldIssue(issue) {
2444
+ return (issue.code === "invalid_type" &&
2445
+ typeof issue.message === "string" &&
2446
+ /received undefined\b/.test(issue.message));
2447
+ }
2448
+ /**
2449
+ * Translate a single Zod issue into an AgentErrorIssue. The `namespace`
2450
+ * controls which code prefix is used for the fallback path.
2451
+ */
2452
+ export function translateZodIssue(issue, ctx) {
2453
+ const path = issue.path;
2454
+ const field = formatZodPath(path);
2455
+ const fieldTail = path.length > 0 ? String(path[path.length - 1]) : "";
2456
+ const rowIndex = extractRowIndex(path);
2457
+ // Field-name-keyed override wins. Pick by the longest matching suffix so
2458
+ // `taskContext.background` beats `background`.
2459
+ let code;
2460
+ if (ctx.fieldCodeMap) {
2461
+ let bestLen = -1;
2462
+ for (const key of Object.keys(ctx.fieldCodeMap)) {
2463
+ if (field === key || field.endsWith(`.${key}`) || field.endsWith(`]${key}`)) {
2464
+ if (key.length > bestLen) {
2465
+ bestLen = key.length;
2466
+ code = ctx.fieldCodeMap[key];
2467
+ }
2468
+ }
2469
+ }
2470
+ }
2471
+ if (!code) {
2472
+ // Generic Zod-code → registry-code mapping. Zod 4 changed several
2473
+ // issue shapes vs. Zod 3 — `received` dropped from invalid_type;
2474
+ // `invalid_enum_value` renamed to `invalid_value`.
2475
+ if (isMissingFieldIssue(issue)) {
2476
+ code = `${ctx.namespace}.${fieldTail}_missing`;
2477
+ }
2478
+ else if (issue.code === "too_small") {
2479
+ code = `${ctx.namespace}.${fieldTail}_too_short`;
2480
+ }
2481
+ else if (issue.code === "too_big") {
2482
+ code = `${ctx.namespace}.${fieldTail}_too_long`;
2483
+ }
2484
+ else if (issue.code === "invalid_value" ||
2485
+ // @ts-expect-error — Zod 3 compatibility for migrations in flight.
2486
+ issue.code === "invalid_enum_value") {
2487
+ code = `${ctx.namespace}.${fieldTail}_unknown`;
2488
+ }
2489
+ else {
2490
+ code = `${ctx.namespace}.${fieldTail}_invalid`;
2491
+ }
2492
+ }
2493
+ return composeIssue(code, {
2494
+ field,
2495
+ received: inferReceivedFromIssue(issue),
2496
+ rowIndex,
2497
+ });
2498
+ }
2499
+ /**
2500
+ * Translate every issue from a Zod safeParse error. Stable order — issues
2501
+ * are emitted in the order Zod produced them, which matches input traversal.
2502
+ */
2503
+ export function translateZodError(error, ctx) {
2504
+ return error.issues.map((issue) => translateZodIssue(issue, ctx));
2505
+ }
2506
+ //# sourceMappingURL=agent-errors.js.map