aimeat 1.33.2 → 1.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (328) hide show
  1. package/dist/locales/en.json +437 -15
  2. package/dist/locales/fi.json +437 -15
  3. package/dist/public/cortex-bundled/aimeat-charts.js +3 -3
  4. package/dist/public/cortex-bundled/aimeat-charts.yaml +3 -3
  5. package/dist/public/cortex-bundled/aimeat-i18n.js +123 -0
  6. package/dist/public/cortex-bundled/aimeat-i18n.yaml +64 -0
  7. package/dist/public/css/views/landing.css +2 -0
  8. package/dist/public/css/views/secretary.css +464 -2
  9. package/dist/public/js/i18n.js +9 -0
  10. package/dist/public/js/services/apps.js +4 -2
  11. package/dist/public/js/services/memory.js +9 -3
  12. package/dist/public/js/services/offers.js +1 -1
  13. package/dist/public/js/services/organisms.js +49 -17
  14. package/dist/public/js/services/secretary-helpers.js +266 -17
  15. package/dist/public/js/services/secretary-policy.js +28 -2
  16. package/dist/public/js/services/secretary-rules.js +70 -0
  17. package/dist/public/js/theme.js +20 -0
  18. package/dist/public/lib/VENDORED-STYLING.md +31 -0
  19. package/dist/public/lib/aimeat-daisyui-bridge.css +48 -0
  20. package/dist/public/lib/chartjs@4.js +14 -0
  21. package/dist/public/lib/daisyui@5.css +7 -0
  22. package/dist/public/lib/tailwindcss@4.js +8 -0
  23. package/dist/public/llms-template.txt +9 -6
  24. package/dist/public/showroom.html +104 -0
  25. package/dist/public/showroom.js +247 -0
  26. package/dist/public/spa.html +18 -16
  27. package/dist/public/views/aimeat-os.js +3 -2
  28. package/dist/public/views/landing.js +50 -6
  29. package/dist/public/views/profile/agents/agent-card.js +13 -4
  30. package/dist/public/views/profile/agents/state-detector.js +6 -0
  31. package/dist/public/views/profile/agents/tab-integration.js +11 -2
  32. package/dist/public/views/profile/apps-tab.js +46 -0
  33. package/dist/public/views/profile/extensions-tab.js +4 -0
  34. package/dist/public/views/profile/inbox-tab.js +1 -1
  35. package/dist/public/views/profile/landing-page.js +8 -3
  36. package/dist/public/views/profile/memory-tab.js +37 -6
  37. package/dist/public/views/profile/organisms/home.js +32 -0
  38. package/dist/public/views/profile/organisms/workspace-list.js +36 -5
  39. package/dist/public/views/profile/organisms/workspace.js +29 -4
  40. package/dist/public/views/profile/organisms-tab.js +61 -8
  41. package/dist/public/views/secretary/calendar.js +327 -0
  42. package/dist/public/views/secretary/cards-reach.js +16 -17
  43. package/dist/public/views/secretary/cards.js +217 -79
  44. package/dist/public/views/secretary/dashboard.js +617 -0
  45. package/dist/public/views/secretary/quality.js +156 -0
  46. package/dist/public/views/secretary/use-autonomy.js +17 -2
  47. package/dist/public/views/secretary/use-freshness.js +50 -0
  48. package/dist/public/views/secretary/use-intake.js +23 -4
  49. package/dist/public/views/secretary/use-layout.js +122 -0
  50. package/dist/public/views/secretary/use-quick-actions.js +98 -0
  51. package/dist/public/views/secretary/use-specialists.js +103 -0
  52. package/dist/public/views/secretary/use-triggers.js +159 -0
  53. package/dist/public/views/secretary/use-whats-next.js +340 -0
  54. package/dist/public/views/secretary/workflow-design.js +112 -0
  55. package/dist/public/views/secretary.js +836 -44
  56. package/dist/scripts/check-no-max-tokens.d.ts +2 -0
  57. package/dist/scripts/check-no-max-tokens.d.ts.map +1 -0
  58. package/dist/scripts/check-no-max-tokens.js +71 -0
  59. package/dist/scripts/check-no-max-tokens.js.map +1 -0
  60. package/dist/src/cli/connect/mcp/tools/organisms.d.ts +2 -0
  61. package/dist/src/cli/connect/mcp/tools/organisms.d.ts.map +1 -1
  62. package/dist/src/cli/connect/mcp/tools/organisms.js +9 -1
  63. package/dist/src/cli/connect/mcp/tools/organisms.js.map +1 -1
  64. package/dist/src/cli/connect/onboarding-prompt.d.ts +3 -0
  65. package/dist/src/cli/connect/onboarding-prompt.d.ts.map +1 -1
  66. package/dist/src/cli/connect/onboarding-prompt.js +10 -3
  67. package/dist/src/cli/connect/onboarding-prompt.js.map +1 -1
  68. package/dist/src/data/aimeat-iam-package.d.ts +25 -0
  69. package/dist/src/data/aimeat-iam-package.d.ts.map +1 -0
  70. package/dist/src/data/aimeat-iam-package.js +312 -0
  71. package/dist/src/data/aimeat-iam-package.js.map +1 -0
  72. package/dist/src/data/aimeat-marketplace-package.d.ts +25 -0
  73. package/dist/src/data/aimeat-marketplace-package.d.ts.map +1 -0
  74. package/dist/src/data/aimeat-marketplace-package.js +231 -0
  75. package/dist/src/data/aimeat-marketplace-package.js.map +1 -0
  76. package/dist/src/data/app-templates.d.ts +48 -0
  77. package/dist/src/data/app-templates.d.ts.map +1 -0
  78. package/dist/src/data/app-templates.js +856 -0
  79. package/dist/src/data/app-templates.js.map +1 -0
  80. package/dist/src/data/example-packages.d.ts.map +1 -1
  81. package/dist/src/data/example-packages.js +3 -1
  82. package/dist/src/data/example-packages.js.map +1 -1
  83. package/dist/src/generated/api-types.d.ts +689 -7
  84. package/dist/src/generated/api-types.d.ts.map +1 -1
  85. package/dist/src/generated/prisma-postgres/edge.js +10 -3
  86. package/dist/src/generated/prisma-postgres/index-browser.js +7 -0
  87. package/dist/src/generated/prisma-postgres/index.d.ts +205 -2
  88. package/dist/src/generated/prisma-postgres/index.js +10 -3
  89. package/dist/src/generated/prisma-postgres/package.json +1 -1
  90. package/dist/src/generated/prisma-postgres/schema.prisma +23 -13
  91. package/dist/src/generated/prisma-postgres/wasm.js +10 -3
  92. package/dist/src/mcp/agent-onboarding.d.ts +4 -0
  93. package/dist/src/mcp/agent-onboarding.d.ts.map +1 -1
  94. package/dist/src/mcp/agent-onboarding.js +28 -2
  95. package/dist/src/mcp/agent-onboarding.js.map +1 -1
  96. package/dist/src/mcp/annotations.d.ts.map +1 -1
  97. package/dist/src/mcp/annotations.js +1 -0
  98. package/dist/src/mcp/annotations.js.map +1 -1
  99. package/dist/src/mcp/catalog/definitions.d.ts.map +1 -1
  100. package/dist/src/mcp/catalog/definitions.js +16 -1
  101. package/dist/src/mcp/catalog/definitions.js.map +1 -1
  102. package/dist/src/mcp/catalog/surfaces.js +3 -3
  103. package/dist/src/mcp/catalog/surfaces.js.map +1 -1
  104. package/dist/src/mcp/index.d.ts +3 -0
  105. package/dist/src/mcp/index.d.ts.map +1 -1
  106. package/dist/src/mcp/index.js +11 -0
  107. package/dist/src/mcp/index.js.map +1 -1
  108. package/dist/src/mcp/organisms.d.ts.map +1 -1
  109. package/dist/src/mcp/organisms.js +48 -8
  110. package/dist/src/mcp/organisms.js.map +1 -1
  111. package/dist/src/mcp/workspaces.d.ts.map +1 -1
  112. package/dist/src/mcp/workspaces.js +6 -4
  113. package/dist/src/mcp/workspaces.js.map +1 -1
  114. package/dist/src/models/agent-onboarding-schemas.d.ts +47 -0
  115. package/dist/src/models/agent-onboarding-schemas.d.ts.map +1 -1
  116. package/dist/src/models/agent-onboarding-schemas.js +117 -0
  117. package/dist/src/models/agent-onboarding-schemas.js.map +1 -1
  118. package/dist/src/models/offer-schemas.d.ts +8 -8
  119. package/dist/src/routes/agent-onboarding.d.ts +5 -0
  120. package/dist/src/routes/agent-onboarding.d.ts.map +1 -1
  121. package/dist/src/routes/agent-onboarding.js +21 -2
  122. package/dist/src/routes/agent-onboarding.js.map +1 -1
  123. package/dist/src/routes/ai.d.ts +8 -4
  124. package/dist/src/routes/ai.d.ts.map +1 -1
  125. package/dist/src/routes/ai.js +38 -14
  126. package/dist/src/routes/ai.js.map +1 -1
  127. package/dist/src/routes/app-grants.d.ts +2 -0
  128. package/dist/src/routes/app-grants.d.ts.map +1 -1
  129. package/dist/src/routes/app-grants.js +7 -1
  130. package/dist/src/routes/app-grants.js.map +1 -1
  131. package/dist/src/routes/app-templates.d.ts +16 -0
  132. package/dist/src/routes/app-templates.d.ts.map +1 -0
  133. package/dist/src/routes/app-templates.js +40 -0
  134. package/dist/src/routes/app-templates.js.map +1 -0
  135. package/dist/src/routes/apps.d.ts.map +1 -1
  136. package/dist/src/routes/apps.js +55 -5
  137. package/dist/src/routes/apps.js.map +1 -1
  138. package/dist/src/routes/auth.d.ts +3 -0
  139. package/dist/src/routes/auth.d.ts.map +1 -1
  140. package/dist/src/routes/auth.js +9 -4
  141. package/dist/src/routes/auth.js.map +1 -1
  142. package/dist/src/routes/bootstrap.d.ts.map +1 -1
  143. package/dist/src/routes/bootstrap.js +1 -0
  144. package/dist/src/routes/bootstrap.js.map +1 -1
  145. package/dist/src/routes/generator.d.ts.map +1 -1
  146. package/dist/src/routes/generator.js +3 -2
  147. package/dist/src/routes/generator.js.map +1 -1
  148. package/dist/src/routes/lib-ai.d.ts +0 -1
  149. package/dist/src/routes/lib-ai.d.ts.map +1 -1
  150. package/dist/src/routes/lib-ai.js +13 -6
  151. package/dist/src/routes/lib-ai.js.map +1 -1
  152. package/dist/src/routes/lib-header.d.ts +2 -0
  153. package/dist/src/routes/lib-header.d.ts.map +1 -1
  154. package/dist/src/routes/lib-header.js +7 -16
  155. package/dist/src/routes/lib-header.js.map +1 -1
  156. package/dist/src/routes/lib-markdown.d.ts +18 -0
  157. package/dist/src/routes/lib-markdown.d.ts.map +1 -0
  158. package/dist/src/routes/lib-markdown.js +236 -0
  159. package/dist/src/routes/lib-markdown.js.map +1 -0
  160. package/dist/src/routes/libs.d.ts +22 -0
  161. package/dist/src/routes/libs.d.ts.map +1 -1
  162. package/dist/src/routes/libs.js +133 -10
  163. package/dist/src/routes/libs.js.map +1 -1
  164. package/dist/src/routes/memory.d.ts.map +1 -1
  165. package/dist/src/routes/memory.js +21 -2
  166. package/dist/src/routes/memory.js.map +1 -1
  167. package/dist/src/routes/openrouter.d.ts +4 -0
  168. package/dist/src/routes/openrouter.d.ts.map +1 -1
  169. package/dist/src/routes/openrouter.js +17 -0
  170. package/dist/src/routes/openrouter.js.map +1 -1
  171. package/dist/src/routes/organisms.d.ts.map +1 -1
  172. package/dist/src/routes/organisms.js +107 -7
  173. package/dist/src/routes/organisms.js.map +1 -1
  174. package/dist/src/routes/portal.d.ts +9 -0
  175. package/dist/src/routes/portal.d.ts.map +1 -1
  176. package/dist/src/routes/portal.js +1 -1
  177. package/dist/src/routes/portal.js.map +1 -1
  178. package/dist/src/routes/public-stats.d.ts +3 -0
  179. package/dist/src/routes/public-stats.d.ts.map +1 -1
  180. package/dist/src/routes/public-stats.js +9 -2
  181. package/dist/src/routes/public-stats.js.map +1 -1
  182. package/dist/src/routes/secretary.d.ts +34 -0
  183. package/dist/src/routes/secretary.d.ts.map +1 -0
  184. package/dist/src/routes/secretary.js +311 -0
  185. package/dist/src/routes/secretary.js.map +1 -0
  186. package/dist/src/routes/specialists.d.ts.map +1 -1
  187. package/dist/src/routes/specialists.js +21 -0
  188. package/dist/src/routes/specialists.js.map +1 -1
  189. package/dist/src/routes/subdomains.js +1 -1
  190. package/dist/src/routes/subdomains.js.map +1 -1
  191. package/dist/src/routes/workflows.d.ts +9 -3
  192. package/dist/src/routes/workflows.d.ts.map +1 -1
  193. package/dist/src/routes/workflows.js +9 -3
  194. package/dist/src/routes/workflows.js.map +1 -1
  195. package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
  196. package/dist/src/server-bootstrap/routes-loader.js +4 -0
  197. package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
  198. package/dist/src/server-bootstrap/service-init.d.ts.map +1 -1
  199. package/dist/src/server-bootstrap/service-init.js +7 -0
  200. package/dist/src/server-bootstrap/service-init.js.map +1 -1
  201. package/dist/src/server-bootstrap/static-files.d.ts +4 -0
  202. package/dist/src/server-bootstrap/static-files.d.ts.map +1 -1
  203. package/dist/src/server-bootstrap/static-files.js +18 -0
  204. package/dist/src/server-bootstrap/static-files.js.map +1 -1
  205. package/dist/src/services/archive.d.ts +74 -0
  206. package/dist/src/services/archive.d.ts.map +1 -0
  207. package/dist/src/services/archive.js +132 -0
  208. package/dist/src/services/archive.js.map +1 -0
  209. package/dist/src/services/cortex-seeder.d.ts +4 -1
  210. package/dist/src/services/cortex-seeder.d.ts.map +1 -1
  211. package/dist/src/services/cortex-seeder.js +25 -6
  212. package/dist/src/services/cortex-seeder.js.map +1 -1
  213. package/dist/src/services/directory.d.ts.map +1 -1
  214. package/dist/src/services/directory.js +3 -2
  215. package/dist/src/services/directory.js.map +1 -1
  216. package/dist/src/services/generator-prompt-seeds.d.ts.map +1 -1
  217. package/dist/src/services/generator-prompt-seeds.js +5 -4
  218. package/dist/src/services/generator-prompt-seeds.js.map +1 -1
  219. package/dist/src/services/message-send.d.ts.map +1 -1
  220. package/dist/src/services/message-send.js +14 -0
  221. package/dist/src/services/message-send.js.map +1 -1
  222. package/dist/src/services/onboarding-guide.d.ts +53 -0
  223. package/dist/src/services/onboarding-guide.d.ts.map +1 -0
  224. package/dist/src/services/onboarding-guide.js +81 -0
  225. package/dist/src/services/onboarding-guide.js.map +1 -0
  226. package/dist/src/services/organism-search.d.ts +6 -2
  227. package/dist/src/services/organism-search.d.ts.map +1 -1
  228. package/dist/src/services/organism-search.js +2 -2
  229. package/dist/src/services/organism-search.js.map +1 -1
  230. package/dist/src/services/owner-memory.d.ts +1 -0
  231. package/dist/src/services/owner-memory.d.ts.map +1 -1
  232. package/dist/src/services/owner-memory.js.map +1 -1
  233. package/dist/src/services/owner-session.d.ts +1 -0
  234. package/dist/src/services/owner-session.d.ts.map +1 -1
  235. package/dist/src/services/owner-session.js +10 -2
  236. package/dist/src/services/owner-session.js.map +1 -1
  237. package/dist/src/services/package-seeder.d.ts +22 -0
  238. package/dist/src/services/package-seeder.d.ts.map +1 -0
  239. package/dist/src/services/package-seeder.js +30 -0
  240. package/dist/src/services/package-seeder.js.map +1 -0
  241. package/dist/src/services/presence.d.ts +3 -0
  242. package/dist/src/services/presence.d.ts.map +1 -1
  243. package/dist/src/services/presence.js +10 -0
  244. package/dist/src/services/presence.js.map +1 -1
  245. package/dist/src/services/prompt-defaults.d.ts.map +1 -1
  246. package/dist/src/services/prompt-defaults.js +8 -3
  247. package/dist/src/services/prompt-defaults.js.map +1 -1
  248. package/dist/src/services/scheduler.d.ts +50 -2
  249. package/dist/src/services/scheduler.d.ts.map +1 -1
  250. package/dist/src/services/scheduler.js +454 -26
  251. package/dist/src/services/scheduler.js.map +1 -1
  252. package/dist/src/services/secretary-authoring.d.ts +71 -0
  253. package/dist/src/services/secretary-authoring.d.ts.map +1 -0
  254. package/dist/src/services/secretary-authoring.js +336 -0
  255. package/dist/src/services/secretary-authoring.js.map +1 -0
  256. package/dist/src/services/secretary-clarify.d.ts +57 -0
  257. package/dist/src/services/secretary-clarify.d.ts.map +1 -0
  258. package/dist/src/services/secretary-clarify.js +104 -0
  259. package/dist/src/services/secretary-clarify.js.map +1 -0
  260. package/dist/src/services/secretary-feed.d.ts +11 -0
  261. package/dist/src/services/secretary-feed.d.ts.map +1 -0
  262. package/dist/src/services/secretary-feed.js +28 -0
  263. package/dist/src/services/secretary-feed.js.map +1 -0
  264. package/dist/src/services/secretary-tick.d.ts +243 -3
  265. package/dist/src/services/secretary-tick.d.ts.map +1 -1
  266. package/dist/src/services/secretary-tick.js +273 -3
  267. package/dist/src/services/secretary-tick.js.map +1 -1
  268. package/dist/src/services/secretary-workbrief.d.ts +144 -0
  269. package/dist/src/services/secretary-workbrief.d.ts.map +1 -0
  270. package/dist/src/services/secretary-workbrief.js +315 -0
  271. package/dist/src/services/secretary-workbrief.js.map +1 -0
  272. package/dist/src/services/secretary-worktier.d.ts +118 -0
  273. package/dist/src/services/secretary-worktier.d.ts.map +1 -0
  274. package/dist/src/services/secretary-worktier.js +157 -0
  275. package/dist/src/services/secretary-worktier.js.map +1 -0
  276. package/dist/src/services/skill-bundle/generator.d.ts +3 -0
  277. package/dist/src/services/skill-bundle/generator.d.ts.map +1 -1
  278. package/dist/src/services/skill-bundle/generator.js +19 -0
  279. package/dist/src/services/skill-bundle/generator.js.map +1 -1
  280. package/dist/src/services/specialist-runner.d.ts +36 -0
  281. package/dist/src/services/specialist-runner.d.ts.map +1 -0
  282. package/dist/src/services/specialist-runner.js +91 -0
  283. package/dist/src/services/specialist-runner.js.map +1 -0
  284. package/dist/src/services/specialist.d.ts +9 -0
  285. package/dist/src/services/specialist.d.ts.map +1 -1
  286. package/dist/src/services/specialist.js +42 -0
  287. package/dist/src/services/specialist.js.map +1 -1
  288. package/dist/src/services/structure-overview.d.ts +13 -0
  289. package/dist/src/services/structure-overview.d.ts.map +1 -1
  290. package/dist/src/services/structure-overview.js +34 -11
  291. package/dist/src/services/structure-overview.js.map +1 -1
  292. package/dist/src/services/system-agent-responder.d.ts +36 -0
  293. package/dist/src/services/system-agent-responder.d.ts.map +1 -0
  294. package/dist/src/services/system-agent-responder.js +107 -0
  295. package/dist/src/services/system-agent-responder.js.map +1 -0
  296. package/dist/src/services/system-agent.d.ts +34 -1
  297. package/dist/src/services/system-agent.d.ts.map +1 -1
  298. package/dist/src/services/system-agent.js +73 -0
  299. package/dist/src/services/system-agent.js.map +1 -1
  300. package/dist/src/storage/interface.d.ts +33 -0
  301. package/dist/src/storage/interface.d.ts.map +1 -1
  302. package/dist/src/storage/interface.js.map +1 -1
  303. package/dist/src/storage/providers/mongodb/index.d.ts +37 -0
  304. package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
  305. package/dist/src/storage/providers/mongodb/index.js +132 -12
  306. package/dist/src/storage/providers/mongodb/index.js.map +1 -1
  307. package/dist/src/storage/providers/sqlite/index.d.ts +23 -1
  308. package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
  309. package/dist/src/storage/providers/sqlite/index.js +60 -8
  310. package/dist/src/storage/providers/sqlite/index.js.map +1 -1
  311. package/dist/src/storage/providers/sqlite/repos/memory.d.ts +29 -1
  312. package/dist/src/storage/providers/sqlite/repos/memory.d.ts.map +1 -1
  313. package/dist/src/storage/providers/sqlite/repos/memory.js +109 -10
  314. package/dist/src/storage/providers/sqlite/repos/memory.js.map +1 -1
  315. package/dist/src/storage/providers/sqlite/schema.d.ts +3 -0
  316. package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
  317. package/dist/src/storage/providers/sqlite/schema.js +64 -9
  318. package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
  319. package/dist/src/storage/repositories/app.repository.d.ts +15 -0
  320. package/dist/src/storage/repositories/app.repository.d.ts.map +1 -1
  321. package/dist/src/storage/repositories/memory.repository.d.ts +41 -1
  322. package/dist/src/storage/repositories/memory.repository.d.ts.map +1 -1
  323. package/dist/src/storage/repositories/organism.repository.d.ts +2 -1
  324. package/dist/src/storage/repositories/organism.repository.d.ts.map +1 -1
  325. package/dist/static/app-catalog.html +469 -122
  326. package/package.json +2 -1
  327. package/prisma/schema.postgres.prisma +10 -0
  328. package/prisma/schema.prisma +12 -0
@@ -8,9 +8,71 @@
8
8
  * Presentational cards live in ./secretary/cards.js; pure helpers in /js/services/secretary-helpers.js.
9
9
  * Pure frontend orchestration over generic endpoints (directives + organisms + memory + ai/complete +
10
10
  * agent-messages + discover). Full design: docs/plans/2026-06-23-secretary-feature.md.
11
- * @structure SECRETARY_ICON · SecretaryView (default) — state, effects, handlers, layout (cards in ./secretary/cards.js + ./secretary/cards-reach.js)
11
+ * @structure SECRETARY_ICON · SecretaryView (default) — state, effects, handlers, layout (cards in ./secretary/cards.js + ./secretary/cards-reach.js + dashboard chrome in ./secretary/dashboard.js)
12
12
  * @usage routed at /v1/secretary by spa.html (+ portal.ts spaRoutes).
13
13
  * @version-history
14
+ * v0.25.0 — 2026-07-01 — Light-vs-heavy seam: setup no longer cheap-authors content (that hallucinated).
15
+ * applyResult now CREATES the workspace structure and then proposes grounded Work Briefs (POST
16
+ * /v1/secretary/brief) the user hands to a Builder (a connected big AI via the appdev MCP tools). Drops
17
+ * the per-workspace author-now fill loop + the author_content band. The dashboard brief tray (cards)
18
+ * renders the briefs with "Open in Claude Desktop" / "Send to a connected Builder".
19
+ * v0.24.0 — 2026-06-30 — Never-empty + honest fill result. normalizeInlineManifest now GUARANTEES every
20
+ * workspace has a document space (appends a "Muistiinpanot" doc space when the design gave only records
21
+ * spaces) so the setup fill always has a thinking-home, and is genuinely document-first for unspecified
22
+ * spaces. The per-workspace fill loop counts a workspace as filled ONLY on a real 🖊️ authored line (not
23
+ * a ⚠️ summary), retries the HTTP call once (flaky owl-alpha), and the toast names workspaces it
24
+ * couldn't fill instead of a bare count.
25
+ * v0.23.0 — 2026-06-30 — Autonomy chosen up front: "Set up my Secretary" first asks HOW autonomous it
26
+ * should be (Prepare/Draft · Run/Act · Only-when-I-ask/Off), each explained in plain words incl. the
27
+ * AI-credit cost, BEFORE anything runs — so nobody stumbles into Act bands later. The choice sets the
28
+ * new context's author_content band and, for Draft/Act, turns the daily check-in on. Browser-verified.
29
+ * v0.22.0 — 2026-06-30 — Live setup progress: the fill runs ONE workspace at a time (author-now?ws=)
30
+ * and a top-level overlay (survives the hire→dashboard switch) shows "Filling workspace N/total ·
31
+ * <name>" + a progress bar through the whole slow fill, instead of a couple of fleeting toasts.
32
+ * Browser-verified: overlay cycled 1/2 → 2/2 with the bar advancing.
33
+ * v0.21.0 — 2026-06-30 — Deterministic workspace structure (NO AI): genAndApplyManifest normalizes any
34
+ * design-provided inline manifest into a valid one (fixes namespace form + backfills schemaRef/backing/
35
+ * writeRole/mode, rekeys schemas, derives a records schema when missing) and falls back to a plain
36
+ * document space — instead of a flaky per-workspace AI generator call that left some workspaces as
37
+ * "set up when opened". Every workspace now gets its structure reliably, every time. Verified on the
38
+ * dev server: a 6-workspace design (with non-conforming komento.* namespaces) all came out structured.
39
+ * v0.20.0 — 2026-06-30 — Content at handshake: a fresh hire now fills its workspaces with REAL content
40
+ * immediately (POST /v1/secretary/author-now, long timeout) and drops any left empty; the workspace
41
+ * generator runs with skipExamples so no fake "example-N" placeholder records are written.
42
+ * v0.19.0 — 2026-06-30 — Strategy: setup now auto-links milestones to goals. seedGoals returns the
43
+ * assigned goal ids in order; applyResult seeds goals before building the strategy and resolves each
44
+ * milestone's index-based goalRefs to real ids (linkMilestoneGoals); reshape strips dangling index
45
+ * refs; AI re-plan passes existing goals and validates returned id refs (keepKnownGoalRefs). Removes
46
+ * the manual "tick the linked goals" step after setup.
47
+ * v0.18.0 — 2026-06-28 — Strategy: optional per-context steering frame (vision/mission/principles/risks
48
+ * + current → target + ordered milestone gates). strategyCard slot + handlers (edit fields, milestone
49
+ * CRUD/reorder/status, link goals, AI re-plan), enable from Manage & setup, carried in applyResult.
50
+ * v0.17.0 — 2026-06-28 — "Reset Secretary": guarded full-wipe card in Manage & setup (doReset →
51
+ * POST /v1/secretary/reset → reload to the clean setup screen).
52
+ * v0.16.0 — 2026-06-28 — Bound triggers: pass specialists + workflows into triggersCard and the
53
+ * calendar event dialog so a trigger's fire can be bound to a workflow/specialist run.
54
+ * v0.15.0 — 2026-06-28 — B5 (secretary-view-redesign): action-items. The autonomous tick now advances
55
+ * active routines band-driven (act-band file steps auto-run; draft/ask/delegate → action-items) and
56
+ * writes contexts[i].actionItems[]; the dashboard renders them (actionItemsCard) with a one-click
57
+ * handle (open the routine / check a delegated result) or dismiss. Handlers in useWhatsNext.
58
+ * v0.14.0 — 2026-06-27 — B3 (secretary-view-redesign): dynamic quick actions. useQuickActions manages
59
+ * per-context quickActions[] — 2–3 brain-seeded shortcuts (active on hire) + secretary-PROPOSED ones
60
+ * the owner pins. Active dynamic actions render in the quick row (prompt → canned chat message, compose
61
+ * → focus an input); a ✎ panel (quickActionsManager) pins/dismisses proposals + renames/reorders/removes.
62
+ * Security: the Secretary may seed/propose only prompt|compose, never a run verb (sanitizeQuickActions).
63
+ * v0.13.0 — 2026-06-27 — B2 (secretary-view-redesign): Routines + "What's next". The guided plan is
64
+ * generalised into useWhatsNext — propose a new Routine from a goal OR advance an active one, approve
65
+ * each step band-gated (act → run · draft|ask → approve + log a decision · off → skip), execute the
66
+ * automatable part (discover/file; delegation deferred to B4), and persist under
67
+ * contexts[i].routines[]. Active routines render on the dashboard (routinesCard).
68
+ * v0.12.0 — 2026-06-27 — B1 (secretary-view-redesign): dashboard-first IA. A core quick-action row
69
+ * ("Where do things stand?" read-only orientation · Plan/Find/Note focus · Review decisions when
70
+ * due), a "Today" status strip (reliability · budget · next run · last-scan + stale + refresh) above
71
+ * the feed/decision-log, chat below, and the set-up-once config cards (brain/operating/crew/knowledge/
72
+ * access/history/permissions) moved into a collapsed "Manage & setup" disclosure. Chrome in dashboard.js.
73
+ * v0.11.0 — 2026-06-25 — load() reads OpenRouter settings first (which backfills the Secretary agent
74
+ * when a key exists) and tracks hasOpenRouterKey, so a pre-configured owner is provisioned on view
75
+ * load and the "not set up" message correctly distinguishes "no key" from "provisioning".
14
76
  * v0.10.0 — 2026-06-24 — P3: doc/image intake into the chat (chatCard 📎 → intake.handleAttach:
15
77
  * upload → vision summary → file into the active context's self-organism) and auto-logged decision
16
78
  * contracts from answered Ask cards + approved guided plans (learning loop now sees real choices).
@@ -32,18 +94,27 @@ import { h } from 'preact';
32
94
  import htm from 'htm';
33
95
  const html = htm.bind(h);
34
96
  import { useState, useEffect, useCallback, useMemo } from 'preact/hooks';
35
- import { t } from '/js/i18n.js';
36
- import { api, apiGet, apiPost, apiPut } from '/js/api.js';
97
+ import { t, getLocale } from '/js/i18n.js';
98
+ import { api, apiGet, apiPost, apiPut, apiDelete } from '/js/api.js';
37
99
  import { useViewCSS } from '/components/useViewCSS.js';
38
100
  import { useToast } from '/components/Toast.js';
39
- import { createOrganism, createWorkspace } from '/js/services/organisms.js';
40
- import { defaultPolicy, mergePolicy } from '/js/services/secretary-policy.js';
41
- import { buildDesignPrompt, extractJson, snapshotOf, genCtxId, migrateConfig, suggestContextId, computeBudgetInfo, computeReliability, SECRETARY_AIMEAT_PRIMER } from '/js/services/secretary-helpers.js';
42
- import { contextSwitcher, hirePanel, chatCard, findCard, noteCard, decisionsCard, brainCard, operatingCard, historyCard, metaCard, guidedPlanCard, feedCard, automationCard, goalsCard, decisionLogCard } from '/views/secretary/cards.js';
101
+ import { createOrganism, createWorkspace, validateGenerated, applyGeneratedWorkspace } from '/js/services/organisms.js';
102
+ import { defaultPolicy, mergePolicy, autonomyPreset } from '/js/services/secretary-policy.js';
103
+ import { buildDesignPrompt, extractJson, snapshotOf, genCtxId, migrateConfig, suggestContextId, computeBudgetInfo, computeReliability, sanitizeQuickActions, normalizeStrategy, MILESTONE_STATUSES, buildStrategyReplanPrompt, linkMilestoneGoals, keepKnownGoalRefs, SECRETARY_AIMEAT_PRIMER } from '/js/services/secretary-helpers.js';
104
+ import { contextSwitcher, hirePanel, chatCard, findCard, noteCard, decisionsCard, brainCard, operatingCard, historyCard, metaCard, whatsNextCard, feedCard, automationCard, goalsCard, decisionLogCard } from '/views/secretary/cards.js';
43
105
  import { createResourceCard, knowledgeCard, accessCard, crewCard } from '/views/secretary/cards-reach.js';
106
+ import { quickActionRow, dashStatus, standPanel, briefTrayCard, whatsNextPanel, actionItemsCard, routinesCard, triggersCard, quickActionsManager, manageHeader, workflowDesignPanel, secretaryDangerCard, strategyCard } from '/views/secretary/dashboard.js';
107
+ import { fetchWorkflowOffers, designWorkflow, saveDesignedWorkflow, slugifyWorkflowId } from '/views/secretary/workflow-design.js';
108
+ import { useSpecialists, specialistsCard } from '/views/secretary/use-specialists.js';
44
109
  import { useIntake } from '/views/secretary/use-intake.js';
45
- import { useGuidedPlan } from '/views/secretary/use-guided-plan.js';
110
+ import { useWhatsNext } from '/views/secretary/use-whats-next.js';
111
+ import { useQuickActions } from '/views/secretary/use-quick-actions.js';
112
+ import { useFreshness } from '/views/secretary/use-freshness.js';
46
113
  import { useAutonomy } from '/views/secretary/use-autonomy.js';
114
+ import { useCalendar, calendarCard, CalendarEventDialog } from '/views/secretary/calendar.js';
115
+ import { useTriggers } from '/views/secretary/use-triggers.js';
116
+ import { useLayout, LayoutCard } from '/views/secretary/use-layout.js';
117
+ import { produceDeliverable, getAiCapability, reasonJson } from '/views/secretary/quality.js';
47
118
  import { useLearning } from '/views/secretary/use-learning.js';
48
119
  import { useCreateResource } from '/views/secretary/use-create-resource.js';
49
120
  import { useKnowledge } from '/views/secretary/use-knowledge.js';
@@ -62,10 +133,17 @@ export default function SecretaryView() {
62
133
  useViewCSS('/css/views/secretary.css');
63
134
  const { showToast, ToastContainer } = useToast();
64
135
  const [secretary, setSecretary] = useState(undefined); // undefined=loading, null=not provisioned
136
+ const [hasOpenRouterKey, setHasOpenRouterKey] = useState(false); // owner has an OpenRouter key configured
65
137
  const [config, setConfig] = useState(null); // { contexts:[...], activeContextId }
138
+ const [briefs, setBriefs] = useState([]); // open Work Briefs (heavy work to hand a Builder)
139
+ const [builders, setBuilders] = useState([]); // connected big-AI agents (not Secretary / specialists)
140
+ const [dispatching, setDispatching] = useState(''); // briefId currently being dispatched
66
141
  const [result, setResult] = useState('');
67
142
  const [needs, setNeeds] = useState('');
68
143
  const [applying, setApplying] = useState(false);
144
+ const [setupStep, setSetupStep] = useState(null); // live setup progress: { phase, done, total, name }
145
+ const [chooseMode, setChooseMode] = useState(false); // showing the "how should it work?" autonomy step
146
+ const [setupMode, setSetupMode] = useState('draft'); // chosen autonomy: 'off' | 'draft' | 'act'
69
147
  const [generating, setGenerating] = useState(false);
70
148
  const [showHire, setShowHire] = useState(false);
71
149
  const [hireMode, setHireMode] = useState('new'); // 'new' (add context) | 'edit' (re-run active)
@@ -76,6 +154,18 @@ export default function SecretaryView() {
76
154
  const [findScope, setFindScope] = useState('public');
77
155
  const [findResults, setFindResults] = useState(null); // null=not searched, []=none, [...]
78
156
  const [finding, setFinding] = useState(false);
157
+ const [manageOpen, setManageOpen] = useState(false); // B1: collapsed "Manage & setup" disclosure
158
+ const [stand, setStand] = useState(null); // B1: read-only "where things stand" summary { loading } | { text }
159
+ const [nextAns, setNextAns] = useState(null); // "What's next?" — the Secretary's forward answer { loading } | { text }
160
+ const [pasteDrafts, setPasteDrafts] = useState({}); // prompt-driven path: pasted-back result text, keyed by action id
161
+ const [wfDesign, setWfDesign] = useState(null); // "Design a workflow" flow: null | { outcome, designing, draft, trigger, error, saving, saved, errors }
162
+ const [calEvent, setCalEvent] = useState(null); // clicked calendar entry → inline type-specific editor dialog
163
+ const [resetState, setResetState] = useState({ show: false, text: '', busy: false }); // "Reset Secretary" guard
164
+ const [stratEdit, setStratEdit] = useState(null); // Strategy text/list edit draft | null
165
+ const [msInput, setMsInput] = useState(''); // Strategy "add milestone" input
166
+ const [replan, setReplan] = useState(null); // Strategy re-plan flow: { note } | { busy } | { draft } | null
167
+ const [brainDraft, setBrainDraft] = useState(null); // C2: direct brain editor draft { purpose, rules[] } | null
168
+ const [savingBrain, setSavingBrain] = useState(false);
79
169
 
80
170
  const owner = (secretary && secretary.owner)
81
171
  || (window.AIMEAT && window.AIMEAT.auth && window.AIMEAT.auth.getSession && window.AIMEAT.auth.getSession()?.owner)
@@ -92,22 +182,52 @@ export default function SecretaryView() {
92
182
  }, []);
93
183
 
94
184
  const load = useCallback(async () => {
185
+ // Read OpenRouter settings FIRST: this GET also provisions the Secretary agent when a key exists
186
+ // (backfill for owners who configured OpenRouter before the feature shipped), so by the time we
187
+ // list agents below the agent is present. Also tells us whether to show "configure OpenRouter".
188
+ const settingsResp = await apiGet('/v1/openrouter/settings').catch(() => null);
189
+ setHasOpenRouterKey(!!(settingsResp && settingsResp.data && settingsResp.data.hasApiKey));
95
190
  const agentsResp = await apiGet('/v1/agents').catch(() => null);
96
191
  const agents = (agentsResp && agentsResp.data && agentsResp.data.agents) || [];
97
192
  const sec = agents.find((a) => (a.tags || []).includes('system:secretary'))
98
193
  || agents.find((a) => a.name === 'secretary') || null;
99
194
  setSecretary(sec);
195
+ // Builders = connected big-AI agents the heavy work is dispatched to — anything that is NOT the
196
+ // Secretary (the watcher) and NOT a node-local specialist. These are visually distinct in the brief tray.
197
+ setBuilders(agents.filter((a) => a && a.name !== 'secretary'
198
+ && !(a.tags || []).includes('system:secretary') && !(a.tags || []).includes('system:specialist')));
100
199
  if (!sec) return;
101
- const [dir, cfg] = await Promise.all([
200
+ const [dir, cfg, briefResp] = await Promise.all([
102
201
  apiGet('/v1/agents/secretary/directives').catch(() => null),
103
202
  apiGet('/v1/memory/secretary.config').catch(() => null),
203
+ apiGet('/v1/secretary/briefs').catch(() => null),
104
204
  ]);
205
+ setBriefs((briefResp && briefResp.data && briefResp.data.briefs) || []);
105
206
  const rawCfg = (cfg && cfg.data && cfg.data.value) || null;
106
207
  const { config: normalized, changed } = migrateConfig(rawCfg, dir && dir.data);
107
208
  if (changed) await apiPost('/v1/memory', { key: 'secretary.config', value: normalized, visibility: 'private' }).catch(() => {});
108
209
  setConfig(normalized);
109
210
  }, []);
110
211
 
212
+ // Dispatch a Work Brief to a connected Builder (lane b): queues a task the Builder picks up + runs via
213
+ // the appdev MCP tools. Marks the brief 'dispatched' so the tray reflects "waiting on the Builder".
214
+ const dispatchBrief = useCallback(async (briefId, agentGaii) => {
215
+ if (!briefId || !agentGaii) return;
216
+ setDispatching(briefId);
217
+ try {
218
+ const r = await apiPost(`/v1/secretary/brief/${encodeURIComponent(briefId)}/dispatch`, { targetAgent: agentGaii }).catch(() => null);
219
+ if (r && r.data) { showToast(t('secretary.brief.dispatched')); load(); }
220
+ else showToast(t('secretary.brief.dispatchFailed'), true);
221
+ } finally { setDispatching(''); }
222
+ }, [load, showToast]);
223
+
224
+ // Resolve a Work Brief (the owner handled it themselves, or it's no longer relevant). Marks it 'done'.
225
+ const resolveBrief = useCallback(async (briefId) => {
226
+ if (!briefId) return;
227
+ await apiPost(`/v1/secretary/brief/${encodeURIComponent(briefId)}/done`, {}).catch(() => null);
228
+ load();
229
+ }, [load]);
230
+
111
231
  useEffect(() => { load(); }, [load]);
112
232
 
113
233
  // Re-fetch config on server-reported changes: the autonomous tick writes pending decisions + the
@@ -127,11 +247,36 @@ export default function SecretaryView() {
127
247
  // Cheap, AI-free routing suggestion: does what you're typing belong to another context? (§22)
128
248
  const routeSuggestion = useMemo(() => suggestContextId(chatInput, contexts, activeId), [chatInput, contexts, activeId]);
129
249
 
250
+ // "Reset Secretary" — full wipe (brain + all secretary.* + self-organisms/workspaces + specialists +
251
+ // tick schedule), keeping the OpenRouter key. Guarded by a typed RESET confirm; reloads to the clean
252
+ // setup screen afterward (state across many hooks is fully torn down server-side).
253
+ const doReset = useCallback(async () => {
254
+ setResetState((s) => ({ ...s, busy: true }));
255
+ try {
256
+ await apiPost('/v1/secretary/reset', {});
257
+ showToast(t('secretary.reset.done'));
258
+ setTimeout(() => window.location.reload(), 700);
259
+ } catch (e) {
260
+ showToast(`${t('secretary.reset.error')}: ${e.message}`, true);
261
+ setResetState((s) => ({ ...s, busy: false }));
262
+ }
263
+ }, [showToast]);
264
+
130
265
  // Load the active context's conversation (chat is per-context).
131
266
  useEffect(() => {
267
+ setStand(null);
268
+ setNextAns(null);
132
269
  if (!activeId) { setChat([]); return; }
133
270
  let cancelled = false;
134
271
  setChat([]);
272
+ // Restore the last "where things stand" so it persists across reloads (with its timestamp).
273
+ apiGet(`/v1/memory/${encodeURIComponent('secretary.stand.' + activeId)}`)
274
+ .then((r) => { const v = r && r.data && r.data.value; if (!cancelled && v && v.text) setStand({ text: v.text, generatedAt: v.generatedAt }); })
275
+ .catch(() => {});
276
+ // Restore the last "What's next" list (with its do/skip state) so leaving and coming back keeps it.
277
+ apiGet(`/v1/memory/${encodeURIComponent('secretary.next.' + activeId)}`)
278
+ .then((r) => { const v = r && r.data && r.data.value; if (!cancelled && v && Array.isArray(v.actions) && v.actions.length) setNextAns(v); })
279
+ .catch(() => {});
135
280
  apiGet(`/v1/memory/${encodeURIComponent('secretary.chat.' + activeId)}`)
136
281
  .then((r) => {
137
282
  if (cancelled) return;
@@ -142,14 +287,29 @@ export default function SecretaryView() {
142
287
  return () => { cancelled = true; };
143
288
  }, [activeId]);
144
289
 
290
+ // Persist the "What's next" list (and its do/skip state) so navigating away and back keeps it.
291
+ // Stamped with ctxId so a context switch never writes the old list under the new context's key.
292
+ useEffect(() => {
293
+ if (!activeId || !nextAns || !Array.isArray(nextAns.actions) || nextAns.ctxId !== activeId) return;
294
+ apiPost('/v1/memory', { key: `secretary.next.${activeId}`, value: nextAns, visibility: 'private' }).catch(() => {});
295
+ }, [nextAns, activeId]);
296
+
145
297
  // Intake (note composer + Ask cards + P2-E cross-context auto-routing) lives in its own hook.
146
- const intake = useIntake({ active, contexts, config, persistConfig, owner, showToast });
298
+ // Triggers (Slice 3/4) — declared before intake so a filed note can ask the Secretary to auto-propose a trigger.
299
+ const trig = useTriggers({ active, showToast });
300
+ const intake = useIntake({ active, contexts, config, persistConfig, owner, showToast, onNoteFiled: trig.proposeTriggerFromText });
147
301
  const { wsList, suggestedWsId } = intake;
148
302
 
149
- // Phase 3c — guided playbook (propose steps → approve → execute). Logic lives in its own hook.
150
- const plan = useGuidedPlan({ active, suggestedWsId, wsList, showToast });
303
+ // B2 — Routines + "What's next" (generalises the guided plan): propose/advance band-gated routines.
304
+ const next = useWhatsNext({ active, config, persistConfig, policy, wsList, suggestedWsId, showToast });
305
+ // B3 — dynamic quick actions (brain-seeded + secretary-proposed, owner-pinned).
306
+ const qa = useQuickActions({ active, config, persistConfig, owner, showToast });
151
307
  // Phase 4 — autonomous tick + Home feed + calendar (own hook).
152
308
  const auto = useAutonomy({ showToast });
309
+ // Decision B — real calendar (month/week/day) of feed (past) + tick & routine cadence (future).
310
+ const cal = useCalendar();
311
+ const lay = useLayout(); // customizable dashboard layout (pin to column / reorder / hide; persisted)
312
+ const spec = useSpecialists({ showToast }); // create/list/run the owner's specialists (Phase 3 building blocks)
153
313
  // Phase 5 — learning loop: goals + decision-log contracts + review trigger (own hook).
154
314
  const learn = useLearning({ active, auto, showToast });
155
315
  // P2 reach: create-don't-just-find (A), knowledge custodian (B), access gatekeeper (C), crew setup (D).
@@ -162,7 +322,108 @@ export default function SecretaryView() {
162
322
  const budgetInfo = useMemo(() => computeBudgetInfo(policy.dailyMorselBudget, config, activeId), [policy.dailyMorselBudget, config, activeId]);
163
323
  const reliability = useMemo(() => computeReliability(learn.decisions), [learn.decisions]);
164
324
 
165
- const applyResult = useCallback(async () => {
325
+ /** C3: seed the hire-proposed goals as standalone goal records (so the learning loop isn't empty). */
326
+ // Seed the proposed goals and RETURN their assigned ids in input order (null for a skipped/failed one),
327
+ // so milestone goalRefs (the AI links them by 1-based goal index) can be resolved to real goal ids.
328
+ const seedGoals = useCallback(async (goals, contextId, contextName) => {
329
+ const arr = Array.isArray(goals) ? goals.slice(0, 4) : [];
330
+ const ids = [];
331
+ for (const g of arr) {
332
+ const title = String((g && g.title) || '').trim();
333
+ if (!title) { ids.push(null); continue; }
334
+ const gid = 'g' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
335
+ const ok = await apiPost('/v1/memory', { key: `secretary.goal.${gid}`, value: { id: gid, title: title.slice(0, 200), why: String((g && g.why) || '').trim().slice(0, 300), status: 'open', contextId, contextName, createdAt: new Date().toISOString() }, visibility: 'private', tags: ['secretary', 'goal', 'open', contextId] }).then(() => true).catch(() => false);
336
+ ids.push(ok ? gid : null);
337
+ }
338
+ return ids;
339
+ }, []);
340
+
341
+ // ── Deterministic workspace structure (NO AI) ──
342
+ // The design JSON's inline manifests routinely fail the workspace validator (wrong namespace form, or
343
+ // missing schemaRef/backing/writeRole), which used to push EACH workspace into a flaky per-workspace AI
344
+ // generator call — owl-alpha failing some of those left workspaces as "set up when opened". Instead we
345
+ // NORMALIZE any inline manifest into a valid one ourselves, and fall back to a plain document space.
346
+ // Zero AI calls → every workspace gets its structure reliably, every time.
347
+ const WS_TYPE_MAP = { text: 'string', number: 'number', date: 'string', select: 'string', boolean: 'boolean', reference: 'string' };
348
+ const wsSlug = (s) => String(s || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'tila';
349
+ const backfillEnvelope = (mf, wsId, name) => {
350
+ if (!mf) return;
351
+ mf.manifestVersion = mf.manifestVersion || '1.0';
352
+ mf.id = mf.id || wsId;
353
+ mf.name = mf.name || name;
354
+ mf.kind = mf.kind || 'project';
355
+ mf.status = ['active', 'paused', 'done', 'archived'].includes(mf.status) ? mf.status : 'active';
356
+ };
357
+ // Make ANY inline manifest valid: backfill the envelope + each objectType's required fields, normalize
358
+ // the namespace to the required shared.* form (rekeying its schema), and derive a schema for a records
359
+ // type that lacks one. Mutates `manifest`; may add keys to `schemas`.
360
+ const normalizeInlineManifest = (manifest, schemas, wsId, name) => {
361
+ backfillEnvelope(manifest, wsId, name);
362
+ const ots = Array.isArray(manifest.objectTypes) ? manifest.objectTypes : [];
363
+ manifest.objectTypes = ots.filter(Boolean).map((ot, i) => {
364
+ const o = { ...ot };
365
+ o.name = String(o.name || ('tila' + (i + 1))).trim() || ('tila' + (i + 1));
366
+ let ns = String(o.namespace || ('shared.' + wsSlug(o.name)));
367
+ if (!/^(meta|shared|member)\./.test(ns)) {
368
+ const tail = ns.includes('.') ? ns.slice(ns.indexOf('.') + 1) : ns;
369
+ const newNs = 'shared.' + (wsSlug(tail) || wsSlug(o.name));
370
+ if (schemas[o.namespace] && !schemas[newNs]) { schemas[newNs] = schemas[o.namespace]; delete schemas[o.namespace]; }
371
+ ns = newNs;
372
+ }
373
+ o.namespace = ns;
374
+ o.backing = o.backing === 'tasks' ? 'tasks' : 'memory';
375
+ o.writeRole = ['owner', 'admin', 'member'].includes(o.writeRole) ? o.writeRole : 'member';
376
+ o.cardinality = o.cardinality === 'one' ? 'one' : 'many';
377
+ o.versioned = o.versioned !== false;
378
+ o.schemaRef = o.schemaRef || ('schema:' + wsSlug(o.name) + '@1');
379
+ // Document-first: keep an explicit mode as the design gave it; for an UNSPECIFIED space default to a
380
+ // DOCUMENT (flexible information — the common case) unless the design clearly describes a record list
381
+ // (real fields, or a schema already supplied for it). A records space is for same-shaped, list-like
382
+ // data (actions/tasks/bugs/a pipeline/a tracker) only.
383
+ if (o.mode !== 'document' && o.mode !== 'records') {
384
+ o.mode = ((Array.isArray(o.fields) && o.fields.length) || schemas[ns]) ? 'records' : 'document';
385
+ }
386
+ if (o.mode !== 'document' && o.backing === 'memory' && !schemas[ns]) {
387
+ const props = {}; (o.fields || []).forEach((f) => { if (f && f.name) props[f.name] = { type: WS_TYPE_MAP[f.type] || 'string' }; });
388
+ if (!props.id) props.id = { type: 'string' };
389
+ schemas[ns] = { type: 'object', properties: props, required: ['id'] };
390
+ }
391
+ return o;
392
+ });
393
+ // Never-empty guarantee: every workspace needs at least one DOCUMENT space — a free-markdown "thinking
394
+ // home" the setup fill can always author grounded content into, even when the design gave only records
395
+ // spaces (records are real-data-only and legitimately start empty). Append a default document space when
396
+ // the design provided none. Additive + idempotent: only when no document space exists.
397
+ if (!manifest.objectTypes.some((o) => o.mode === 'document')) {
398
+ let dns = 'shared.muistiinpanot';
399
+ if (manifest.objectTypes.some((o) => o.namespace === dns)) dns = 'shared.muistiinpanot-' + wsSlug(name);
400
+ manifest.objectTypes.push({ name: 'Muistiinpanot', namespace: dns, backing: 'memory', writeRole: 'member', cardinality: 'many', versioned: true, schemaRef: 'schema:doc@1', mode: 'document' });
401
+ }
402
+ };
403
+ // A plain document space (free markdown) — the deterministic fallback so a workspace with no usable
404
+ // manifest still gets a valid structure at setup, without any AI call.
405
+ const docSpaceManifest = (wsId, name) => ({
406
+ manifest: { manifestVersion: '1.0', id: wsId, name, kind: 'notes', status: 'active',
407
+ objectTypes: [{ name: 'Dokumentit', namespace: 'shared.dokumentit', backing: 'memory', writeRole: 'member', cardinality: 'many', versioned: true, schemaRef: 'schema:doc@1', mode: 'document' }] },
408
+ schemas: {},
409
+ });
410
+ const genAndApplyManifest = useCallback(async (orgId, wsId, ws) => {
411
+ const name = String(ws.name || '').trim() || 'Työtila';
412
+ try {
413
+ // skipExamples: the Secretary fills workspaces with REAL content at setup (author-now), never fakes.
414
+ if (ws.manifest && typeof ws.manifest === 'object') {
415
+ const schemas = { ...(ws.schemas || {}) };
416
+ const manifest = JSON.parse(JSON.stringify(ws.manifest));
417
+ normalizeInlineManifest(manifest, schemas, wsId, name);
418
+ const provided = { manifest, schemas };
419
+ if (!validateGenerated(provided).length) { await applyGeneratedWorkspace(orgId, wsId, provided, { skipExamples: true }); return; }
420
+ }
421
+ // No usable inline manifest → a plain document space, applied deterministically (no AI).
422
+ await applyGeneratedWorkspace(orgId, wsId, docSpaceManifest(wsId, name), { skipExamples: true });
423
+ } catch { /* leave it manifest-less; the owner can set it up manually */ }
424
+ }, []);
425
+
426
+ const applyResult = useCallback(async (mode = 'off') => {
166
427
  setApplying(true);
167
428
  try {
168
429
  const json = extractJson(result);
@@ -177,6 +438,8 @@ export default function SecretaryView() {
177
438
  }
178
439
  const newBrain = { purpose: b.purpose, rules };
179
440
  const isNew = hireMode === 'new' || !active;
441
+ setSetupStep({ phase: 'structure' }); // live progress: building the space + workspaces
442
+ let fillTargets = []; // [{ id, name }] of new workspaces to fill one at a time
180
443
 
181
444
  let next;
182
445
  if (isNew) {
@@ -185,6 +448,7 @@ export default function SecretaryView() {
185
448
  const orgId = created && created.data && created.data.organism && created.data.organism.id;
186
449
  const wsSummary = [];
187
450
  if (orgId) {
451
+ const created = [];
188
452
  for (const ws of org.workspaces.slice(0, 12)) {
189
453
  const name = String(ws.name || '').trim();
190
454
  if (!name) continue;
@@ -192,35 +456,133 @@ export default function SecretaryView() {
192
456
  if (entry && entry.id && ws.purpose) {
193
457
  await apiPost('/v1/memory', { key: `organism.${orgId}.w.${entry.id}.meta.readme`, value: `# ${name}\n\n${ws.purpose}`, visibility: 'private' });
194
458
  }
459
+ if (entry && entry.id) created.push({ wsId: entry.id, ws });
195
460
  wsSummary.push({ name, purpose: ws.purpose || '' });
196
461
  }
462
+ // Design + apply each workspace's manifest in parallel so they come out usable, not empty shells.
463
+ await Promise.all(created.map((c) => genAndApplyManifest(orgId, c.wsId, c.ws)));
464
+ fillTargets = created.map((c) => ({ id: c.wsId, name: String((c.ws && c.ws.name) || '').trim() || c.wsId }));
197
465
  }
198
- const ctx = { id: genCtxId(), name: org.name, brain: newBrain, organismId: orgId || null, organismName: org.name, workspaces: wsSummary, policy: defaultPolicy(), brainHistory: [] };
466
+ // B3: seed the brain-proposed quick actions — active on hire (run verbs are dropped by the sanitizer).
467
+ const quickActions = sanitizeQuickActions(json.quickActions, 'brain', 'active');
468
+ const ctxId = genCtxId();
469
+ // C3: seed the proposed goals FIRST (so the learning loop starts populated, not "no goals yet") and
470
+ // capture their ids, so each milestone's goalRefs (the AI links them by 1-based goal index) resolve
471
+ // to real goal ids — this is what lets a milestone auto-advance as its goals get done, instead of
472
+ // the owner ticking the linked goals by hand after setup.
473
+ const goalIds = await seedGoals(json.goals, ctxId, org.name);
474
+ const strategy = linkMilestoneGoals(normalizeStrategy(json.strategy), goalIds);
475
+ // Autonomy chosen in the setup step controls only how active the DAILY tick is (it watches, briefs,
476
+ // and proposes Work Briefs — it never authors content itself, so there is no "fill" band any more).
477
+ const policy = defaultPolicy();
478
+ const ctx = { id: ctxId, name: org.name, brain: newBrain, organismId: orgId || null, organismName: org.name, workspaces: wsSummary, policy, brainHistory: [], quickActions, strategy };
199
479
  next = { contexts: [...contexts, ctx], activeContextId: ctx.id };
200
480
  } else {
201
- // Re-run on the active context: update its brain, snapshot the old one. Keep its organism.
481
+ // Re-run on the active context (reshape): update its brain, snapshot the old one, KEEP its
482
+ // organism. C1: ADD any newly-proposed workspaces (additive — never rename/remove existing;
483
+ // removal stays a manual decision in the Organisms view, so no data is ever auto-deleted).
202
484
  const prev = snapshotOf(active.brain);
203
485
  const history = [prev, ...(active.brainHistory || [])].filter(Boolean).slice(0, 10);
204
- const updated = { ...active, brain: newBrain, brainHistory: history };
486
+ const existingNames = new Set((active.workspaces || []).map((w) => String(w.name).toLowerCase()));
487
+ const addedWs = [];
488
+ if (active.organismId && Array.isArray(org.workspaces)) {
489
+ const created = [];
490
+ for (const ws of org.workspaces.slice(0, 12)) {
491
+ const name = String(ws.name || '').trim();
492
+ if (!name || existingNames.has(name.toLowerCase())) continue;
493
+ const entry = await createWorkspace(active.organismId, name);
494
+ if (entry && entry.id && ws.purpose) {
495
+ await apiPost('/v1/memory', { key: `organism.${active.organismId}.w.${entry.id}.meta.readme`, value: `# ${name}\n\n${ws.purpose}`, visibility: 'private' });
496
+ }
497
+ if (entry && entry.id) created.push({ wsId: entry.id, ws });
498
+ addedWs.push({ name, purpose: ws.purpose || '' });
499
+ }
500
+ await Promise.all(created.map((c) => genAndApplyManifest(active.organismId, c.wsId, c.ws)));
501
+ }
502
+ // Strategy: if the reshape JSON proposes one, take it; otherwise keep what the context already had.
503
+ // Reshape doesn't re-seed goals, so a milestone's index-based goalRefs can't be resolved here —
504
+ // strip them (linkMilestoneGoals with no id list) rather than persist dangling references.
505
+ const strategy = json.strategy !== undefined ? linkMilestoneGoals(normalizeStrategy(json.strategy), []) : (active.strategy || normalizeStrategy(null));
506
+ const updated = { ...active, brain: newBrain, brainHistory: history, workspaces: [...(active.workspaces || []), ...addedWs], strategy };
205
507
  next = { ...config, contexts: contexts.map((c) => (c.id === active.id ? updated : c)) };
206
508
  }
207
509
  await persistConfig(next);
208
510
  await syncDirectives(newBrain);
209
- showToast(t('secretary.hireDone'));
511
+ window.dispatchEvent(new CustomEvent('aimeat-live-update')); // refresh goals/learning + cards
512
+ // The Secretary NO LONGER authors content one-shot at handshake (the cheap model hallucinated and went
513
+ // "onto the Builders' turf"). Instead it now proposes grounded Work Briefs for the new (empty)
514
+ // workspaces — heavy work a Builder (a connected big AI via the appdev MCP tools) executes. Fast +
515
+ // deterministic (no slow model call); the dashboard's brief tray then offers "Open in Claude Desktop"
516
+ // / "Send to a connected Builder". Only on a fresh hire, so it never touches existing content.
517
+ if (isNew && fillTargets.length) {
518
+ setSetupStep({ phase: 'fill', done: 0, total: fillTargets.length, name: fillTargets[0].name });
519
+ const r = await api('/v1/secretary/brief', { method: 'POST', body: '{}', timeoutMs: 120000, retries: 1 }).catch(() => null);
520
+ const proposed = (r && r.data && typeof r.data.proposed === 'number') ? r.data.proposed : 0;
521
+ setSetupStep({ phase: 'done', done: fillTargets.length, total: fillTargets.length });
522
+ window.dispatchEvent(new CustomEvent('aimeat-live-update')); // briefs show up in the tray live
523
+ showToast(proposed ? `${t('secretary.briefsReady')} (${proposed})` : t('secretary.hireDone'));
524
+ } else {
525
+ showToast(t('secretary.hireDone'));
526
+ }
527
+ // If the owner chose an autonomous mode, turn the daily check-in ON for them — so it actually keeps
528
+ // working on its own (the band alone does nothing without a schedule). 'off' stays fully manual.
529
+ if (isNew && mode !== 'off') {
530
+ await apiPost('/v1/schedules', { kind: 'secretary', cron: '0 8 * * *', display_name: t('secretary.auto.scheduleName') }).catch(() => {});
531
+ }
210
532
  setResult(''); setNeeds(''); setShowHire(false);
211
533
  } catch (e) {
212
534
  showToast(`${t('secretary.hireError')}: ${e.message}`, true);
213
535
  } finally {
214
- setApplying(false);
536
+ setApplying(false); setSetupStep(null);
537
+ }
538
+ }, [result, hireMode, active, contexts, config, persistConfig, syncDirectives, seedGoals, genAndApplyManifest, showToast]);
539
+
540
+ // "Set up my Secretary" no longer runs immediately — it first asks (one step, up front) HOW autonomous
541
+ // the owner wants it, explained in plain words, so nobody stumbles into Act bands later. The chosen mode
542
+ // is passed to applyResult. Only for a brand-new hire; re-running an existing context keeps its policy.
543
+ const startSetup = useCallback(() => {
544
+ if (!result.trim()) return;
545
+ try { extractJson(result); } catch { showToast(t('secretary.hireBadShape'), true); return; }
546
+ if (hireMode === 'new' || !active) { setSetupMode('draft'); setChooseMode(true); } else { applyResult('off'); }
547
+ }, [result, hireMode, active, applyResult, showToast]);
548
+ const confirmSetup = useCallback(() => { setChooseMode(false); applyResult(setupMode); }, [applyResult, setupMode]);
549
+
550
+ // C2 — direct brain editor: edit the active context's purpose + rules in place (no AI re-run).
551
+ const startBrainEdit = useCallback(() => {
552
+ if (!active) return;
553
+ setBrainDraft({ purpose: (active.brain && active.brain.purpose) || '', rules: ((active.brain && active.brain.rules) || []).map((r) => ({ id: r.id, description: r.description })) });
554
+ }, [active]);
555
+ const cancelBrainEdit = useCallback(() => setBrainDraft(null), []);
556
+ const saveBrain = useCallback(async () => {
557
+ if (!active || !brainDraft) return;
558
+ const purpose = String(brainDraft.purpose || '').trim();
559
+ if (!purpose) { showToast(t('secretary.brainEmptyPurpose'), true); return; }
560
+ const rules = (brainDraft.rules || [])
561
+ .map((r) => ({ id: r.id || ('r' + Math.random().toString(36).slice(2, 6)), description: String(r.description || '').trim() }))
562
+ .filter((r) => r.description);
563
+ setSavingBrain(true);
564
+ try {
565
+ const newBrain = { purpose, rules };
566
+ const prev = snapshotOf(active.brain);
567
+ const history = [prev, ...(active.brainHistory || [])].filter(Boolean).slice(0, 10);
568
+ const updated = { ...active, brain: newBrain, brainHistory: history };
569
+ await persistConfig({ ...config, contexts: contexts.map((c) => (c.id === active.id ? updated : c)) });
570
+ await syncDirectives(newBrain);
571
+ setBrainDraft(null);
572
+ showToast(t('secretary.brainSaved'));
573
+ } catch (e) {
574
+ showToast(`${t('secretary.hireError')}: ${e.message}`, true);
575
+ } finally {
576
+ setSavingBrain(false);
215
577
  }
216
- }, [result, hireMode, active, contexts, config, persistConfig, syncDirectives, showToast]);
578
+ }, [active, brainDraft, config, contexts, persistConfig, syncDirectives, showToast]);
217
579
 
218
580
  const generateInApp = useCallback(async () => {
219
581
  if (!needs.trim()) return;
220
582
  setGenerating(true);
221
583
  try {
222
584
  // Long AI completion: low-level api() with a large timeout + no retries (apiPost's 30s would abort a slow model).
223
- const r = await api('/v1/ai/complete', { method: 'POST', body: JSON.stringify({ prompt: buildDesignPrompt(owner, needs.trim()), app_id: 'secretary-setup' }), timeoutMs: 1_800_000, retries: 0 });
585
+ const r = await api('/v1/ai/complete', { method: 'POST', body: JSON.stringify({ prompt: buildDesignPrompt(owner, needs.trim(), hireMode === 'edit' ? active : null), app_id: 'secretary-setup' }), timeoutMs: 1_800_000, retries: 0 });
224
586
  const content = r && r.data && r.data.content;
225
587
  if (!content) throw new Error(t('secretary.inappEmpty'));
226
588
  setResult(content);
@@ -230,17 +592,18 @@ export default function SecretaryView() {
230
592
  } finally {
231
593
  setGenerating(false);
232
594
  }
233
- }, [needs, owner, showToast]);
595
+ }, [needs, owner, hireMode, active, showToast]);
234
596
 
235
597
  /** One chat turn: assemble the active context's brain as the system prompt + the transcript,
236
598
  * complete on the owner's key, append + persist. Per-context conversation (secretary.chat.{id}). */
237
- const sendChat = useCallback(async () => {
238
- const text = chatInput.trim();
599
+ const sendChat = useCallback(async (override) => {
600
+ // `override` lets a dynamic quick action (kind:'prompt') send a canned message without touching the input.
601
+ const text = (typeof override === 'string' ? override : chatInput).trim();
239
602
  if (!text || !active) return;
240
603
  setChatSending(true);
241
604
  const history = [...chat, { role: 'user', content: text }];
242
605
  setChat(history);
243
- setChatInput('');
606
+ if (typeof override !== 'string') setChatInput('');
244
607
  try {
245
608
  const rules = (active.brain.rules || []).map((r) => '- ' + r.description).join('\n');
246
609
  const wsNames = (active.workspaces || []).map((w) => w.name).join(', ');
@@ -286,6 +649,286 @@ ${SECRETARY_AIMEAT_PRIMER}`;
286
649
  }
287
650
  }, [findQ, findScope, showToast]);
288
651
 
652
+ // ── B1/G3 dashboard freshness (lastScan + stale + the real discover Reconcile) — in its own hook ──
653
+ const { lastScan, stale, scanning, reconcile } = useFreshness({ active, config, contexts, persistConfig, auto, showToast });
654
+
655
+ // Quick-action "focus" verbs: smooth-scroll a working card into view and focus its input.
656
+ const focusInto = useCallback((sel, inputSel) => {
657
+ const el = document.querySelector(sel);
658
+ if (!el) return;
659
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
660
+ if (inputSel) { const i = el.querySelector(inputSel); if (i) setTimeout(() => i.focus(), 250); }
661
+ }, []);
662
+
663
+ // "Review decisions" is only offered when an open decision is due for its revisit.
664
+ const decisionsDue = useMemo(() => {
665
+ const now = Date.now();
666
+ return (learn.decisions || []).some((d) => d && d.status === 'open' && d.revisitWhen && new Date(d.revisitWhen).getTime() <= now);
667
+ }, [learn.decisions]);
668
+
669
+ // B3: active dynamic quick actions → row items. prompt → send a canned chat message; compose → focus an input.
670
+ const dynamicQuickItems = useMemo(() => qa.activeActions.map((a) => ({
671
+ key: a.id,
672
+ label: a.label,
673
+ title: a.kind === 'prompt' ? a.prompt : `${t('secretary.qa.composeTarget')}: ${a.target}`,
674
+ onClick: a.kind === 'prompt'
675
+ ? () => { focusInto('.sec-chat', null); sendChat(a.prompt); }
676
+ : () => focusInto(a.target === 'find' ? '.sec-find' : a.target === 'note' ? '.sec-note' : '.sec-next', a.target === 'find' ? '.sec-find-in' : 'textarea'),
677
+ })), [qa.activeActions, focusInto, sendChat]);
678
+
679
+ // Read what's ACTUALLY in the active context's workspaces (recent items per workspace) so the
680
+ // Secretary's answers reflect real content — not just goals/decisions/feed. One memory-list call.
681
+ const loadSpaceSnapshot = useCallback(async () => {
682
+ if (!active || !active.organismId) return '';
683
+ const r = await apiGet(`/v1/memory?prefix=${encodeURIComponent('organism.' + active.organismId + '.w.')}&_=${Date.now()}`).catch(() => null);
684
+ const items = (r && r.data && r.data.items) || [];
685
+ const byWs = {};
686
+ for (const it of items) {
687
+ const m = String(it.key || '').match(/\.w\.([^.]+)\.([^.]+)\./);
688
+ if (!m || m[2] === 'meta') continue; // skip per-workspace readme/meta records
689
+ const v = it.value || {};
690
+ const title = String(v.title || v.name || (v.body ? String(v.body).slice(0, 60) : '') || '').replace(/\s+/g, ' ').trim();
691
+ if (!title) continue;
692
+ (byWs[m[1]] = byWs[m[1]] || []).push(title);
693
+ }
694
+ return (wsList || []).map((w) => `- ${w.name}: ${(byWs[w.id] || []).slice(0, 6).join('; ') || '(empty)'}`).join('\n');
695
+ }, [active, wsList]);
696
+
697
+ // "Where do things stand?" — read-only orientation grounded in the brain + open goals/decisions +
698
+ // recent activity AND the real workspace content. No actions taken.
699
+ const runStand = useCallback(async () => {
700
+ if (!active) return;
701
+ setStand({ loading: true });
702
+ try {
703
+ // Quality policy: orientation needs a ≥200k model too (it reads the whole space). No big model →
704
+ // tell the owner instead of running a degraded read.
705
+ const cap = await getAiCapability();
706
+ if (!cap.bigEnough) { setStand({ needModel: true }); return; }
707
+ const openGoals = (learn.goals || []).filter((g) => g.status !== 'done').map((g) => '- ' + g.title).join('\n');
708
+ const openDecs = (learn.decisions || []).filter((d) => d.status !== 'reviewed').map((d) => '- ' + d.decision).join('\n');
709
+ const recent = (auto.feed || []).slice(0, 6).map((f) => '- ' + String(f.text || '').replace(/\s+/g, ' ').slice(0, 160)).join('\n');
710
+ const space = await loadSpaceSnapshot();
711
+ const wsUrl = (wsId) => (active.organismId ? `/v1/profile?tab=organisms&org=${encodeURIComponent(active.organismId)}${wsId ? `&ws=${encodeURIComponent(wsId)}` : ''}` : '');
712
+ const wsLinks = active.organismId ? (wsList || []).map((w) => `- "${w.name}" → ${wsUrl(w.id)}`).join('\n') : '';
713
+ const sys = `You are ${owner || 'the user'}'s personal Secretary in the "${active.name}" context. Give a SHORT, ORGANIZED orientation of where things stand right now. Use Markdown with 2–4 short sections or bullet groups (e.g. In progress / Open items / Needs attention / Recent) — NOT one long paragraph. Ground it in what's ACTUALLY in the workspaces below; do NOT say the space is empty if a workspace has content. When you mention a workspace, write it as a Markdown link using the "Workspace links" below so the owner can jump to it. Do NOT propose actions or claim to have done anything. Reply in ${getLocale() === 'fi' ? 'Finnish' : 'English'}.`;
714
+ const snapshot = `Context purpose: ${active.brain.purpose}\nWorkspace contents:\n${space || '(no workspaces)'}\nWorkspace links:\n${wsLinks || '(none)'}\nOpen goals:\n${openGoals || '(none)'}\nOpen decisions:\n${openDecs || '(none)'}\nRecent autonomous activity:\n${recent || '(none)'}`;
715
+ // Fact-based read → execution model, low temperature.
716
+ const r = await api('/v1/ai/complete', { method: 'POST', body: JSON.stringify({ prompt: snapshot, systemPrompt: sys, modelRole: 'execution', temperature: 0.2, app_id: 'secretary-orient' }), timeoutMs: 1_800_000, retries: 0 });
717
+ const text = ((r && r.data && r.data.content) || '…').trim();
718
+ // Verify the orientation against the facts (reasoning, temp 0) — flag any invented progress/items.
719
+ const check = await reasonJson('You fact-check a status orientation against the facts it was given. Return ONLY JSON {"ok":boolean,"issues":[string]}. ok=false if it states any progress, item, number, or outcome NOT present in the facts. Bracketed [placeholders] are fine.', `Facts:\n${snapshot}\n\nOrientation:\n${text}`, 'secretary-orient-verify');
720
+ const issues = (check && check.ok === false && Array.isArray(check.issues)) ? check.issues.slice(0, 5) : [];
721
+ const generatedAt = new Date().toISOString();
722
+ setStand({ text, generatedAt, issues });
723
+ // Persist so it stays visible across reloads (with its timestamp) — no need to regenerate each time.
724
+ if (active.id) await apiPost('/v1/memory', { key: `secretary.stand.${active.id}`, value: { text, generatedAt, issues }, visibility: 'private' }).catch(() => {});
725
+ } catch (e) {
726
+ setStand(null);
727
+ showToast(`${t('secretary.dash.standError')}: ${e.message}`, true);
728
+ }
729
+ }, [active, owner, learn.goals, learn.decisions, auto.feed, wsList, loadSpaceSnapshot, showToast]);
730
+
731
+ // "What's next?" — the Secretary proposes CONCRETE next actions it can DO (each with Do it / Skip),
732
+ // grounded in the real workspace content + goals/decisions/routines/follow-ups. Not prose to read —
733
+ // an actionable list. Doing one runs it (gather/file/surface); skipping drops it.
734
+ const NEXT_CAPS = ['discover', 'file_intake', 'curate_knowledge', 'briefing', 'reminders', 'create_resource', 'delegate'];
735
+ const runWhatsNext = useCallback(async () => {
736
+ if (!active) return;
737
+ setNextAns({ loading: true });
738
+ try {
739
+ // Deciding the next actions is a reasoning task → requires a ≥200k model. No big model → notice.
740
+ const cap = await getAiCapability();
741
+ if (!cap.bigEnough) { setNextAns({ needModel: true }); return; }
742
+ const openGoals = (learn.goals || []).filter((g) => g.status !== 'done').map((g) => '- ' + g.title).join('\n');
743
+ const dueDecs = (learn.decisions || []).filter((d) => d.status !== 'reviewed').map((d) => '- ' + d.decision).join('\n');
744
+ const routines = (next.activeRoutines || []).map((r) => { const s = next.nextPendingStep(r); return `- ${r.title}${s ? ` (next: ${s.summary})` : ''}`; }).join('\n');
745
+ const followups = (next.actionItems || []).map((a) => '- ' + (a.summary || a.text || '')).join('\n');
746
+ const space = await loadSpaceSnapshot();
747
+ const sys = `You are ${owner || 'the user'}'s personal Secretary in the "${active.name}" context. Propose the concrete NEXT ACTIONS to take now, grounded in the real workspace content + goals/decisions/routines/follow-ups below. Each action is something the owner can approve with one click. Return ONLY a JSON object EXACTLY like {"actions":[{"summary":"a short imperative action (in the user's language)","capability":"discover","why":"one short line why, in the user's language"}]}. Propose 2–5 actions. "capability" MUST be one of: ${NEXT_CAPS.join(', ')} — prefer "discover" to gather info on something, "file_intake"/"curate_knowledge" to record a plan/note, "reminders"/"briefing" to flag something. Do NOT invent capabilities. CRUCIAL: do NOT propose anything already present in the workspace content above — if a deliverable already exists there, propose the genuine NEXT step instead (use, refine, or move it forward), never redo it. Write every "summary" and "why" in ${getLocale() === 'fi' ? 'Finnish' : 'English'}. Output ONLY the JSON object.`;
748
+ const snapshot = `Context purpose: ${active.brain.purpose}\nWorkspace contents:\n${space || '(no workspaces)'}\nOpen goals:\n${openGoals || '(none)'}\nOpen decisions:\n${dueDecs || '(none)'}\nActive routines:\n${routines || '(none)'}\nOpen follow-ups:\n${followups || '(none)'}`;
749
+ // Reasoning/decision task → reasoning model, temperature 0 for sound, repeatable choices.
750
+ const r = await api('/v1/ai/complete', { method: 'POST', body: JSON.stringify({ prompt: snapshot, systemPrompt: sys, modelRole: 'reasoning', temperature: 0, app_id: 'secretary-next' }), timeoutMs: 1_800_000, retries: 0 });
751
+ let parsed = null;
752
+ try { parsed = extractJson((r && r.data && r.data.content) || ''); } catch { /* fall through */ }
753
+ const raw = (parsed && Array.isArray(parsed.actions)) ? parsed.actions : [];
754
+ const actions = raw.slice(0, 6).map((a) => ({
755
+ id: 'na-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5),
756
+ summary: String((a && a.summary) || '').trim(),
757
+ capability: NEXT_CAPS.includes(a && a.capability) ? a.capability : 'file_intake',
758
+ why: String((a && a.why) || '').trim(),
759
+ status: 'open',
760
+ })).filter((a) => a.summary);
761
+ setNextAns({ actions, ctxId: active.id });
762
+ } catch (e) {
763
+ setNextAns(null);
764
+ showToast(`${t('secretary.next.error')}: ${e.message}`, true);
765
+ }
766
+ }, [active, owner, learn.goals, learn.decisions, next, loadSpaceSnapshot, showToast]);
767
+
768
+ // Carry out one proposed "What's next" action (band-spirit: gather/record/surface). Marks it done.
769
+ // ── Phase 3: "Design a workflow" — the Secretary composes a chain of agent+offer steps toward an
770
+ // outcome, proposes how to arm it, and saves it. ──
771
+ const openWfDesign = useCallback(() => setWfDesign((s) => s || { outcome: '' }), []);
772
+ const designWfFromStep = useCallback((step) => { setWfDesign({ outcome: (step && (step.summary || step.description)) || '' }); try { document.querySelector('.sec-wf')?.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch { /* noop */ } }, []);
773
+ const discardWfDesign = useCallback(() => setWfDesign(null), []);
774
+ const redoWfDesign = useCallback(() => setWfDesign((s) => ({ outcome: (s && s.outcome) || '' })), []);
775
+ const setWfOutcome = useCallback((v) => setWfDesign((s) => ({ ...(s || {}), outcome: v })), []);
776
+ const setWfTrigKind = useCallback((kind) => setWfDesign((s) => ({ ...s, trigger: kind === 'schedule' ? { kind: 'schedule', cron: (s.trigger && s.trigger.cron) || '0 9 * * *' } : kind === 'event' ? { kind: 'event', on: 'memory.write', match: {} } : { kind: 'manual' } })), []);
777
+ const setWfCron = useCallback((cron) => setWfDesign((s) => ({ ...s, trigger: { kind: 'schedule', cron } })), []);
778
+ const runWfDesign = useCallback(async () => {
779
+ const outcome = ((wfDesign && wfDesign.outcome) || '').trim();
780
+ if (!outcome) return;
781
+ setWfDesign((s) => ({ ...s, designing: true, error: null, draft: null }));
782
+ try {
783
+ const offers = await fetchWorkflowOffers();
784
+ const r = await designWorkflow({ outcome, offers, locale: getLocale() });
785
+ if (!r.ok) { setWfDesign((s) => ({ ...s, designing: false, error: r.error, title: r.title })); return; }
786
+ setWfDesign((s) => ({ ...s, designing: false, draft: r.def, trigger: r.def.trigger, errors: null }));
787
+ } catch (e) { setWfDesign((s) => ({ ...s, designing: false, error: 'fail' })); showToast(e.message, true); }
788
+ }, [wfDesign, showToast]);
789
+ const saveWfDesign = useCallback(async () => {
790
+ const d = wfDesign && wfDesign.draft;
791
+ if (!d) return;
792
+ setWfDesign((s) => ({ ...s, saving: true, errors: null }));
793
+ const id = slugifyWorkflowId(d.title || (wfDesign && wfDesign.outcome) || 'workflow');
794
+ const r = await saveDesignedWorkflow(id, { ...d, trigger: (wfDesign && wfDesign.trigger) || d.trigger });
795
+ if (!r.ok) { setWfDesign((s) => ({ ...s, saving: false, errors: r.errors })); return; }
796
+ setWfDesign((s) => ({ ...s, saving: false, saved: id }));
797
+ window.dispatchEvent(new CustomEvent('aimeat-live-update'));
798
+ }, [wfDesign]);
799
+
800
+ const doProposedAction = useCallback(async (action) => {
801
+ if (!active) return;
802
+ const patch = (st, extra) => setNextAns((s) => (s && s.actions) ? { ...s, actions: s.actions.map((a) => (a.id === action.id ? { ...a, status: st, ...extra } : a)) } : s);
803
+ patch('doing');
804
+ // Append a visible line to the Home feed ("What I've done") so every Do-it leaves a trail — with a
805
+ // link to where the result landed when there is one.
806
+ const feedLog = async (text, href) => {
807
+ const fr = await apiGet('/v1/memory/secretary.feed').catch(() => null);
808
+ const items = (fr && fr.data && fr.data.value && Array.isArray(fr.data.value.items)) ? fr.data.value.items : [];
809
+ const entry = { id: 'f-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 4), ts: new Date().toISOString(), kind: 'act', contextId: active.id, contextName: active.name || '', text, ...(href ? { href } : {}) };
810
+ await apiPost('/v1/memory', { key: 'secretary.feed', value: { items: [entry, ...items].slice(0, 50) }, visibility: 'private' });
811
+ };
812
+ // Best-matching workspace for the action (by word overlap with its summary).
813
+ const pickWs = () => {
814
+ const wsl = intake.wsList || [];
815
+ const words = new Set(String(action.summary).toLowerCase().split(/[^a-z0-9äöå]+/i).filter((w) => w.length >= 4));
816
+ let ws = wsl[0]; let best = -1;
817
+ for (const w of wsl) { let sc = 0; const hay = String(w.name).toLowerCase(); for (const wd of words) if (hay.includes(wd)) sc++; if (sc > best) { best = sc; ws = w; } }
818
+ return ws;
819
+ };
820
+ // Deep link that opens the organism + the exact workspace in Profile › Organisms (new tab).
821
+ const spaceUrl = (wsId) => (active.organismId ? `/v1/profile?tab=organisms&org=${encodeURIComponent(active.organismId)}${wsId ? `&ws=${encodeURIComponent(wsId)}` : ''}` : null);
822
+ const fileNote = async (ws, title, body) => {
823
+ const id = 'note-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
824
+ const key = `organism.${active.organismId}.w.${ws.id}.notes.${id}`;
825
+ await apiPost('/v1/memory', { key, value: { id, title: String(title).slice(0, 80), body, createdAt: new Date().toISOString(), via: 'secretary-next' }, visibility: 'private' });
826
+ return key;
827
+ };
828
+ try {
829
+ const cap = action.capability;
830
+ let resultMsg = ''; let href = null; let preview = ''; let noteKey = null;
831
+ if (cap === 'discover') {
832
+ // Scout AND KEEP the results: file the top hits as a list the owner can open.
833
+ const d = await apiGet('/v1/discover?scope=public&per_page=10&q=' + encodeURIComponent(action.summary)).catch(() => null);
834
+ const entries = (d && d.data && Array.isArray(d.data.entries)) ? d.data.entries : [];
835
+ const ws = pickWs();
836
+ if (active.organismId && ws && entries.length) {
837
+ const list = entries.slice(0, 10).map((e) => `- ${e.title || e.id}${e.type ? ` (${e.type})` : ''}${e.url ? ` — ${e.url}` : ''}`).join('\n');
838
+ preview = `Found ${entries.length}:\n\n${list}`;
839
+ noteKey = await fileNote(ws, `Scouted: ${action.summary}`, preview);
840
+ resultMsg = t('secretary.next.didScoutedSaved', { n: entries.length, ws: ws.name }); href = spaceUrl(ws.id);
841
+ } else {
842
+ resultMsg = t('secretary.next.didDiscover', { n: entries.length });
843
+ }
844
+ await feedLog(`🔎 ${action.summary} — ${t('secretary.next.didDiscover', { n: entries.length })}`, href);
845
+ } else if (cap === 'briefing' || cap === 'reminders') {
846
+ await feedLog(`⚑ ${action.summary}`);
847
+ resultMsg = t('secretary.next.didSurface');
848
+ } else {
849
+ // file_intake / curate_knowledge / create_resource → run the multi-step QUALITY pipeline
850
+ // (triage → gather → produce → verify) on a ≥200k model, grounded + no fabrication. If no big
851
+ // model is configured, hand the owner a copy-paste prompt for the prompt-driven path instead.
852
+ const ws = pickWs();
853
+ const space = await loadSpaceSnapshot();
854
+ const runDiscover = async (q) => {
855
+ const d = await apiGet('/v1/discover?scope=public&per_page=10&q=' + encodeURIComponent(q)).catch(() => null);
856
+ const es = (d && d.data && Array.isArray(d.data.entries)) ? d.data.entries : [];
857
+ return es.slice(0, 10).map((e) => `- ${e.title || e.id}${e.type ? ` (${e.type})` : ''}${e.url ? ` — ${e.url}` : ''}`).join('\n');
858
+ };
859
+ const out = await produceDeliverable({ action, owner, contextName: active.name, locale: getLocale(), space, runDiscover });
860
+ if (out.mode === 'clarify') {
861
+ // Missing facts only the owner has → ask in the inbox (batch questions); the tick produces the
862
+ // deliverable once answered. Never guess.
863
+ await apiPost('/v1/secretary/clarify', { contextId: active.id, contextName: active.name || '', action: { summary: action.summary, why: action.why || '' }, questions: out.questions, facts: out.facts || space, organismId: active.organismId || '', wsId: (ws && ws.id) || '' });
864
+ patch('asked', {});
865
+ window.dispatchEvent(new CustomEvent('aimeat-live-update'));
866
+ return;
867
+ }
868
+ if (out.mode === 'prompt-driven') {
869
+ // No ≥200k model — never run a degraded completion; show the composed multi-step prompt to
870
+ // copy into a big AI chat, with a paste-back box to save the result.
871
+ patch('prompt', { promptText: out.prompt, expanded: true });
872
+ return;
873
+ }
874
+ preview = out.content;
875
+ const issues = (out.verify && out.verify.ok === false && Array.isArray(out.verify.issues)) ? out.verify.issues.slice(0, 6) : [];
876
+ if (active.organismId && ws) {
877
+ noteKey = await fileNote(ws, action.summary, out.content);
878
+ resultMsg = t('secretary.next.didDrafted', { ws: ws.name }); href = spaceUrl(ws.id);
879
+ await feedLog(`✍️ ${action.summary} — ${t('secretary.next.didDrafted', { ws: ws.name })}`, href);
880
+ patch('done', { result: resultMsg, href, preview, expanded: true, noteKey, issues });
881
+ window.dispatchEvent(new CustomEvent('aimeat-live-update'));
882
+ return;
883
+ }
884
+ resultMsg = t('secretary.next.didNoted');
885
+ }
886
+ patch('done', { result: resultMsg, href, preview, expanded: true, noteKey });
887
+ window.dispatchEvent(new CustomEvent('aimeat-live-update'));
888
+ } catch (e) {
889
+ patch('open');
890
+ showToast(`${t('secretary.next.error')}: ${e.message}`, true);
891
+ }
892
+ }, [active, owner, intake.wsList, loadSpaceSnapshot, showToast]);
893
+
894
+ const skipProposedAction = useCallback((action) => {
895
+ setNextAns((s) => (s && s.actions) ? { ...s, actions: s.actions.map((a) => (a.id === action.id ? { ...a, status: 'skipped' } : a)) } : s);
896
+ }, []);
897
+
898
+ // Show/hide the produced deliverable inline under a done action (no navigation needed).
899
+ const togglePreview = useCallback((action) => {
900
+ setNextAns((s) => (s && s.actions) ? { ...s, actions: s.actions.map((a) => (a.id === action.id ? { ...a, expanded: !a.expanded } : a)) } : s);
901
+ }, []);
902
+
903
+ const setPasteDraft = useCallback((id, v) => setPasteDrafts((m) => ({ ...m, [id]: v })), []);
904
+
905
+ // Prompt-driven path: file the result the owner pasted back from their big AI chat, as a note.
906
+ const savePromptResult = useCallback(async (action) => {
907
+ const text = (pasteDrafts[action.id] || '').trim();
908
+ if (!text || !active) return;
909
+ const wsl = intake.wsList || [];
910
+ const words = new Set(String(action.summary).toLowerCase().split(/[^a-z0-9äöå]+/i).filter((w) => w.length >= 4));
911
+ let ws = wsl[0]; let best = -1;
912
+ for (const w of wsl) { let sc = 0; const hay = String(w.name).toLowerCase(); for (const wd of words) if (hay.includes(wd)) sc++; if (sc > best) { best = sc; ws = w; } }
913
+ let noteKey = null;
914
+ if (active.organismId && ws) {
915
+ const id = 'note-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
916
+ noteKey = `organism.${active.organismId}.w.${ws.id}.notes.${id}`;
917
+ await apiPost('/v1/memory', { key: noteKey, value: { id, title: action.summary.slice(0, 80), body: text, createdAt: new Date().toISOString(), via: 'secretary-next' }, visibility: 'private' }).catch(() => {});
918
+ }
919
+ const href = active.organismId && ws ? `/v1/profile?tab=organisms&org=${encodeURIComponent(active.organismId)}&ws=${encodeURIComponent(ws.id)}` : null;
920
+ setNextAns((s) => (s && s.actions) ? { ...s, actions: s.actions.map((a) => (a.id === action.id ? { ...a, status: 'done', promptText: '', preview: text, expanded: true, noteKey, result: ws ? t('secretary.next.didDrafted', { ws: ws.name }) : t('secretary.next.didNoted'), href } : a)) } : s);
921
+ setPasteDraft(action.id, '');
922
+ window.dispatchEvent(new CustomEvent('aimeat-live-update'));
923
+ }, [pasteDrafts, active, intake.wsList, setPasteDraft]);
924
+
925
+ // Discard a produced deliverable that's no good (e.g. an off-base draft): delete the filed note and
926
+ // clear the result from the list.
927
+ const discardProposedAction = useCallback(async (action) => {
928
+ if (action.noteKey) await apiDelete(`/v1/memory/${encodeURIComponent(action.noteKey)}`).catch(() => {});
929
+ setNextAns((s) => (s && s.actions) ? { ...s, actions: s.actions.map((a) => (a.id === action.id ? { ...a, status: 'discarded', preview: '', result: '', href: null, noteKey: null, expanded: false } : a)) } : s);
930
+ }, []);
931
+
289
932
  const switchContext = useCallback(async (id) => {
290
933
  const ctx = contexts.find((c) => c.id === id);
291
934
  if (!ctx) return;
@@ -299,9 +942,56 @@ ${SECRETARY_AIMEAT_PRIMER}`;
299
942
  persistConfig({ ...config, contexts: contexts.map((c) => (c.id === active.id ? updated : c)) });
300
943
  };
301
944
  const setBand = (capId, band) => updateActivePolicy({ bands: { ...policy.bands, [capId]: band } });
945
+ // One autonomy toggle: 'suggest' (surface only) | 'assist' (handle the small stuff). Sets the band preset
946
+ // underneath, so the band machinery + parity tests are unchanged — the toggle is just the simple surface.
947
+ const setAutonomy = (level) => updateActivePolicy({ bands: autonomyPreset(level) });
302
948
  const toggleStop = () => updateActivePolicy({ stopSpending: !policy.stopSpending });
303
949
  const setBudget = (v) => updateActivePolicy({ dailyMorselBudget: v === '' ? null : Math.max(0, Math.floor(Number(v) || 0)) });
304
950
 
951
+ // Strategy (optional steering frame). Stored on the active context as `strategy = { enabled, vision,
952
+ // mission, principles[], risks[], current, target, milestones:[{id,title,enables,goalRefs[],criterion,status}] }`.
953
+ // Milestones are ordered gates (a checkpoint, not a task — tasks live in goals). Shows only filled parts.
954
+ const strategy = (active && active.strategy && active.strategy.enabled) ? normalizeStrategy(active.strategy) : null;
955
+ const goalsById = useMemo(() => { const m = {}; for (const g of (learn.goals || [])) if (g && g.id) m[g.id] = g; return m; }, [learn.goals]);
956
+ const commitStrategy = (next) => {
957
+ if (!active) return;
958
+ const updated = { ...active, strategy: normalizeStrategy(next) };
959
+ persistConfig({ ...config, contexts: contexts.map((c) => (c.id === active.id ? updated : c)) });
960
+ };
961
+ const curStrat = () => normalizeStrategy(active && active.strategy);
962
+ const startStratEdit = () => { const s = curStrat(); setStratEdit({ vision: s.vision, mission: s.mission, current: s.current, target: s.target, principles: s.principles.join('\n'), risks: s.risks.join('\n') }); };
963
+ const saveStratEdit = () => {
964
+ if (!stratEdit) return;
965
+ const s = curStrat();
966
+ const changedTarget = stratEdit.target.trim() !== (s.target || '').trim();
967
+ commitStrategy({ ...s, enabled: true, vision: stratEdit.vision, mission: stratEdit.mission, current: stratEdit.current, target: stratEdit.target, principles: stratEdit.principles.split('\n'), risks: stratEdit.risks.split('\n') });
968
+ setStratEdit(null);
969
+ // Changing the target state often means the rest of the plan should be re-proposed — offer it.
970
+ if (changedTarget && stratEdit.target.trim()) setReplan({ note: t('secretary.strat.targetChanged') });
971
+ };
972
+ const cycleMilestone = (i) => { const s = curStrat(); const ms = [...s.milestones]; if (!ms[i]) return; const ni = (MILESTONE_STATUSES.indexOf(ms[i].status) + 1) % MILESTONE_STATUSES.length; ms[i] = { ...ms[i], status: MILESTONE_STATUSES[ni] }; commitStrategy({ ...s, milestones: ms }); };
973
+ const addMilestone = () => { const title = msInput.trim(); if (!title) return; const s = curStrat(); commitStrategy({ ...s, milestones: [...s.milestones, { title, enables: '', goalRefs: [], criterion: '', status: 'not-started' }] }); setMsInput(''); };
974
+ const removeMilestone = (i) => { const s = curStrat(); commitStrategy({ ...s, milestones: s.milestones.filter((_, j) => j !== i) }); };
975
+ const moveMilestone = (i, delta) => { const s = curStrat(); const ms = [...s.milestones]; const j = i + delta; if (j < 0 || j >= ms.length) return; [ms[i], ms[j]] = [ms[j], ms[i]]; commitStrategy({ ...s, milestones: ms }); };
976
+ const toggleMilestoneGoal = (i, goalId) => { const s = curStrat(); const ms = [...s.milestones]; if (!ms[i]) return; const refs = new Set(ms[i].goalRefs || []); if (refs.has(goalId)) refs.delete(goalId); else refs.add(goalId); ms[i] = { ...ms[i], goalRefs: [...refs] }; commitStrategy({ ...s, milestones: ms }); };
977
+ const enableStrategy = () => commitStrategy({ ...curStrat(), enabled: true });
978
+ const disableStrategy = () => { commitStrategy({ ...curStrat(), enabled: false }); setStratEdit(null); };
979
+ // Re-plan: change something (e.g. the target), let the AI re-propose a coherent strategy you can apply/edit.
980
+ const startReplan = () => setReplan({ note: '' });
981
+ const runReplan = async () => {
982
+ const note = (replan && replan.note || '').trim(); if (!note) return;
983
+ setReplan((r) => ({ ...r, busy: true }));
984
+ try {
985
+ const prompt = buildStrategyReplanPrompt(owner, active && active.strategy, note, learn.goals);
986
+ const r = await api('/v1/ai/complete', { method: 'POST', body: JSON.stringify({ prompt, app_id: 'secretary-strategy-replan' }), timeoutMs: 1_800_000, retries: 0 });
987
+ const j = extractJson((r && r.data && r.data.content) || '');
988
+ // Re-plan links milestones to EXISTING goal ids — keep only refs that point at a real goal.
989
+ const draft = keepKnownGoalRefs(normalizeStrategy(j.strategy || j), (learn.goals || []).map((g) => g && g.id));
990
+ setReplan({ draft });
991
+ } catch (e) { showToast(`${t('secretary.strat.replanErr')}: ${e.message}`, true); setReplan(null); }
992
+ };
993
+ const applyReplan = () => { if (replan && replan.draft) commitStrategy(replan.draft); setReplan(null); };
994
+
305
995
  const restore = useCallback(async (snap) => {
306
996
  if (!active) return;
307
997
  setApplying(true);
@@ -339,30 +1029,132 @@ ${SECRETARY_AIMEAT_PRIMER}`;
339
1029
  ${secretary === undefined
340
1030
  ? html`<div class="sec-empty">…</div>`
341
1031
  : secretary === null
342
- ? html`<div class="sec-empty">${t('secretary.notReady')}</div>`
1032
+ ? html`<div class="sec-empty">${hasOpenRouterKey ? t('secretary.provisioning') : t('secretary.notReady')}</div>`
343
1033
  : html`
344
1034
  ${hired ? contextSwitcher({ contexts, activeId, switchContext, openAdd }) : null}
345
- ${showHirePanel ? hirePanel({ firstEver, hireMode, owner, needs, setNeeds, result, setResult, applying, generating, generateInApp, applyResult, onCancel: cancelHire }) : null}
1035
+ ${showHirePanel ? hirePanel({ firstEver, hireMode, owner, current: hireMode === 'edit' ? active : null, needs, setNeeds, result, setResult, applying, setupStep, generating, generateInApp, applyResult: startSetup, onCancel: cancelHire }) : null}
346
1036
  ${hired && !showHire && active ? html`
347
- ${chatCard({ activeName: active.name, chat, chatSending, chatInput, setChatInput, sendChat, routeSuggestion, switchContext,
348
- onAttach: intake.handleAttach, attaching: intake.attaching, attachResult: intake.attachResult, canAttach: intake.wsList.length > 0 })}
349
- ${findCard({ findQ, setFindQ, findScope, setFindScope, finding, doFind, findResults })}
350
- ${(findResults && findResults.length === 0) || create.draft || create.created ? createResourceCard({ ...create, query: findQ }) : null}
351
- ${wsList.length > 0 ? noteCard(intake) : null}
352
- ${knowledgeCard(knowledge)}
353
- ${accessCard(access)}
354
- ${crewCard(crew)}
355
- ${guidedPlanCard(plan)}
356
- ${intake.pendingIds.length > 0 ? decisionsCard(intake) : null}
357
- ${goalsCard(learn)}
358
- ${decisionLogCard(learn)}
359
- ${automationCard({ ...auto, budgetInfo })}
360
- ${feedCard(auto)}
361
- ${brainCard({ brain, active, openEdit })}
362
- ${operatingCard({ policy, toggleStop, setBudget, setBand })}
363
- ${(Array.isArray(active.brainHistory) && active.brainHistory.length > 0) ? historyCard({ brainHistory: active.brainHistory, applying, restore }) : null}
364
- ${metaCard({ secretary, reliability })}
1037
+ ${/* Quick-action row (core verbs; dynamic actions arrive in B3) */ ''}
1038
+ ${quickActionRow({ items: [
1039
+ { key: 'next', label: t('secretary.next.title'), primary: true, disabled: !!(nextAns && nextAns.loading), title: t('secretary.next.hint'), onClick: runWhatsNext },
1040
+ { key: 'stand', label: t('secretary.dash.quickStand'), disabled: !!(stand && stand.loading), onClick: runStand },
1041
+ { key: 'find', label: t('secretary.dash.quickFind'), title: t('secretary.findTitle'), onClick: () => focusInto('.sec-find', '.sec-find-in') },
1042
+ { key: 'note', label: t('secretary.dash.quickNote'), title: t('secretary.noteTitle'), hidden: wsList.length === 0, onClick: () => focusInto('.sec-note', 'textarea') },
1043
+ { key: 'review', label: t('secretary.dash.quickReview'), title: t('secretary.learn.reviewNow'), hidden: !decisionsDue, onClick: () => focusInto('.sec-decisions-log', null) },
1044
+ { key: 'design-wf', label: t('secretary.wf.quick'), title: t('secretary.wf.title'), onClick: openWfDesign },
1045
+ ...dynamicQuickItems,
1046
+ { key: 'manage-qa', label: '✎', title: t('secretary.qa.title'), onClick: qa.toggleManage },
1047
+ ] })}
1048
+ ${quickActionsManager(qa)}
1049
+ ${workflowDesignPanel({ state: wfDesign, onOutcome: setWfOutcome, onDesign: runWfDesign, onTrigKind: setWfTrigKind, onCron: setWfCron, onSave: saveWfDesign, onRedo: redoWfDesign, onDiscard: discardWfDesign })}
1050
+
1051
+ ${/* Today — full-width status band, always on top */ ''}
1052
+ ${dashStatus({ reliability, budgetInfo, schedule: auto.schedule, lastScan, stale, onReconcile: reconcile, scanning })}
1053
+
1054
+ ${/* Heavy work to hand off — the Work Brief tray (the light-vs-heavy seam made physical). */ ''}
1055
+ ${briefTrayCard({ briefs, builders, dispatching, onDispatch: dispatchBrief, onResolve: resolveBrief })}
1056
+
1057
+ ${/* Customizable two-column dashboard (pin to column / reorder / hide per card; persisted). */ ''}
1058
+ ${(() => {
1059
+ const nodes = {
1060
+ strategy: strategy ? strategyCard({
1061
+ strategy, goals: learn.goals, goalsById,
1062
+ edit: stratEdit, onStartEdit: startStratEdit, onEditField: (k, v) => setStratEdit((d) => ({ ...(d || {}), [k]: v })), onSaveEdit: saveStratEdit, onCancelEdit: () => setStratEdit(null),
1063
+ onCycle: cycleMilestone, msInput, onMsInput: setMsInput, onAdd: addMilestone, onRemove: removeMilestone, onMove: moveMilestone, onToggleGoal: toggleMilestoneGoal,
1064
+ replan, onStartReplan: startReplan, onReplanNote: (v) => setReplan((r) => ({ ...(r || {}), note: v })), onRunReplan: runReplan, onApplyReplan: applyReplan, onCancelReplan: () => setReplan(null),
1065
+ onDisable: disableStrategy,
1066
+ }) : null,
1067
+ whatsNext: whatsNextPanel({ answer: nextAns, onDo: doProposedAction, onSkip: skipProposedAction, onTogglePreview: togglePreview, onDiscard: discardProposedAction, pasteDrafts, onPasteInput: setPasteDraft, onSavePrompt: savePromptResult, onDismiss: () => { setNextAns(null); if (activeId) apiDelete(`/v1/memory/${encodeURIComponent('secretary.next.' + activeId)}`).catch(() => {}); } }),
1068
+ stand: standPanel({ stand, onRefresh: runStand, onDismiss: () => setStand(null) }),
1069
+ actionItems: actionItemsCard(next),
1070
+ routines: routinesCard({ ...next, onDesignWorkflow: designWfFromStep }),
1071
+ decisions: intake.pendingIds.length > 0 ? decisionsCard(intake) : null,
1072
+ automation: automationCard({ ...auto, budgetInfo }),
1073
+ triggers: triggersCard({ ...trig, goals: learn.goals, routines: next.routines, specialists: spec.list }),
1074
+ specialists: specialistsCard({ ...spec }),
1075
+ calendar: calendarCard({ ...cal, feed: auto.feed, schedule: auto.schedule, routines: next.routines, triggers: trig.armed, onEventClick: setCalEvent }),
1076
+ feed: feedCard(auto),
1077
+ goals: goalsCard(learn),
1078
+ decisionLog: decisionLogCard(learn),
1079
+ chat: chatCard({ activeName: active.name, chat, chatSending, chatInput, setChatInput, sendChat, routeSuggestion, switchContext,
1080
+ onAttach: intake.handleAttach, attaching: intake.attaching, attachResult: intake.attachResult, canAttach: intake.wsList.length > 0 }),
1081
+ whatsNextCard: whatsNextCard({ ...next, agents: crew.agents }),
1082
+ find: findCard({ findQ, setFindQ, findScope, setFindScope, finding, doFind, findResults }),
1083
+ createResource: ((findResults && findResults.length === 0) || create.draft || create.created) ? createResourceCard({ ...create, query: findQ }) : null,
1084
+ note: wsList.length > 0 ? noteCard(intake) : null,
1085
+ };
1086
+ const present = lay.layout.filter((e) => nodes[e.key] != null && !e.hidden);
1087
+ const mainCol = present.filter((e) => e.col === 'main');
1088
+ const rightCol = present.filter((e) => e.col === 'right');
1089
+ const renderCol = (list) => list.map((e, i) => LayoutCard({ entry: e, node: nodes[e.key], prevKey: i > 0 ? list[i - 1].key : null, nextKey: i < list.length - 1 ? list[i + 1].key : null, onSwap: lay.swap, onMoveCol: lay.moveCol, onHide: lay.hide }));
1090
+ return html`
1091
+ <div class="sec-grid">
1092
+ <div class="sec-col sec-col-main">${renderCol(mainCol)}</div>
1093
+ <aside class="sec-col sec-col-right">${renderCol(rightCol)}</aside>
1094
+ </div>
1095
+ <div class="sec-layout-bar">
1096
+ ${lay.hidden.map((e) => html`<button class="sec-chip" key=${e.key} onClick=${() => lay.unhide(e.key)}>+ ${t('secretary.layout.section.' + e.key)}</button>`)}
1097
+ <button class="sec-linkbtn" onClick=${lay.reset}>${t('secretary.layout.reset')}</button>
1098
+ </div>`;
1099
+ })()}
1100
+
1101
+ ${/* Manage & setup — collapsed disclosure (set up once) */ ''}
1102
+ ${manageHeader({ open: manageOpen, onToggle: () => setManageOpen((v) => !v),
1103
+ crewSummary: crew.agents && crew.agents.length ? `${crew.agents.length} ${t('secretary.dash.crewAgents')}` : '' })}
1104
+ ${manageOpen ? html`
1105
+ ${brainCard({ brain, active, openEdit, brainDraft, setBrainDraft, startBrainEdit, saveBrain, cancelBrainEdit, savingBrain })}
1106
+ ${operatingCard({ policy, toggleStop, setBudget, setBand, setAutonomy })}
1107
+ ${!strategy ? html`<section class="sec-card">
1108
+ <h2 class="sec-h2">${t('secretary.strat.title')}</h2>
1109
+ <p class="sec-hint">${t('secretary.strat.enableHint')}</p>
1110
+ <button class="btn-outline btn-sm" onClick=${enableStrategy}>${t('secretary.strat.enable')}</button>
1111
+ </section>` : null}
1112
+ ${crewCard(crew)}
1113
+ ${knowledgeCard(knowledge)}
1114
+ ${accessCard(access)}
1115
+ ${(Array.isArray(active.brainHistory) && active.brainHistory.length > 0) ? historyCard({ brainHistory: active.brainHistory, applying, restore }) : null}
1116
+ ${metaCard({ secretary, reliability })}
1117
+ ${secretaryDangerCard({
1118
+ show: resetState.show, text: resetState.text, busy: resetState.busy,
1119
+ onOpen: () => setResetState({ show: true, text: '', busy: false }),
1120
+ onCancel: () => setResetState({ show: false, text: '', busy: false }),
1121
+ onText: (v) => setResetState((s) => ({ ...s, text: v })),
1122
+ onConfirm: doReset,
1123
+ })}
1124
+ ` : null}
365
1125
  ` : null}`}
1126
+ ${calEvent ? html`<${CalendarEventDialog} event=${calEvent} onClose=${() => setCalEvent(null)} triggers=${{ update: trig.updateTrigger, togglePause: trig.togglePause, remove: trig.removeTrigger }} workflows=${trig.workflows} specialists=${spec.list} routines=${{ setCadence: next.setRoutineCadence, setStatus: next.setRoutineStatus }} tick=${{ toggle: auto.toggleTick, run: auto.runTick }} feed=${{ remove: auto.deleteFeedItem }} />` : null}
1127
+ ${chooseMode ? html`
1128
+ <div class="sec-setup-overlay" onClick=${(e) => { if (e.target === e.currentTarget) setChooseMode(false); }}>
1129
+ <div class="sec-setup-box sec-mode-box">
1130
+ <h3 class="sec-h2">${t('secretary.mode.title')}</h3>
1131
+ <p class="sec-desc">${t('secretary.mode.intro')}</p>
1132
+ ${[['draft', t('secretary.mode.draftTitle'), t('secretary.mode.draftDesc')], ['act', t('secretary.mode.actTitle'), t('secretary.mode.actDesc')], ['off', t('secretary.mode.offTitle'), t('secretary.mode.offDesc')]].map(([val, title, desc]) => html`
1133
+ <label class=${'sec-mode-opt' + (setupMode === val ? ' sec-mode-opt--on' : '')}>
1134
+ <input type="radio" name="setupmode" checked=${setupMode === val} onChange=${() => setSetupMode(val)} />
1135
+ <span><span class="sec-mode-opt-t">${title}</span><span class="sec-desc">${desc}</span></span>
1136
+ </label>`)}
1137
+ <p class="sec-hint">${t('secretary.mode.changeLater')}</p>
1138
+ <div class="sec-actions">
1139
+ <button class="btn-ghost btn-sm" onClick=${() => setChooseMode(false)}>${t('secretary.cancel')}</button>
1140
+ <button class="btn-primary" onClick=${confirmSetup}>${t('secretary.apply')}</button>
1141
+ </div>
1142
+ </div>
1143
+ </div>` : null}
1144
+ ${applying && setupStep ? (() => {
1145
+ // Top-level setup-progress overlay — stays visible across the hire→dashboard switch (persistConfig
1146
+ // makes the Secretary "exist" before the slow fill, so a panel-local progress view would unmount).
1147
+ const s = setupStep, pct = s.total ? Math.round(((s.done || 0) / s.total) * 100) : null;
1148
+ const msg = s.phase === 'fill'
1149
+ ? `${t('secretary.stepFilling')} ${(s.done || 0) + 1}/${s.total}${s.name ? ' · ' + s.name : ''}`
1150
+ : (s.phase === 'done' ? t('secretary.stepDone') : t('secretary.stepStructure'));
1151
+ return html`<div class="sec-setup-overlay"><div class="sec-setup-box">
1152
+ <h3 class="sec-h2">${t('secretary.settingUpTitle')}</h3>
1153
+ <div class="sec-setup-row"><span class="sec-spin"></span><span>${msg}</span></div>
1154
+ ${pct != null ? html`<div class="sec-progress-bar"><div class="sec-progress-fill" style=${`width:${pct}%`}></div></div>` : null}
1155
+ <p class="sec-desc">${t('secretary.settingUpHint')}</p>
1156
+ </div></div>`;
1157
+ })() : null}
366
1158
  <${ToastContainer} />
367
1159
  </div>`;
368
1160
  }