aimeat 1.33.2 → 1.34.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 (291) hide show
  1. package/dist/locales/en.json +388 -14
  2. package/dist/locales/fi.json +388 -14
  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 +412 -2
  9. package/dist/public/js/services/apps.js +4 -2
  10. package/dist/public/js/services/memory.js +9 -3
  11. package/dist/public/js/services/offers.js +1 -1
  12. package/dist/public/js/services/organisms.js +49 -17
  13. package/dist/public/js/services/secretary-helpers.js +262 -17
  14. package/dist/public/js/services/secretary-policy.js +1 -0
  15. package/dist/public/js/services/secretary-rules.js +70 -0
  16. package/dist/public/js/theme.js +20 -0
  17. package/dist/public/lib/VENDORED-STYLING.md +31 -0
  18. package/dist/public/lib/aimeat-daisyui-bridge.css +48 -0
  19. package/dist/public/lib/chartjs@4.js +14 -0
  20. package/dist/public/lib/daisyui@5.css +7 -0
  21. package/dist/public/lib/tailwindcss@4.js +8 -0
  22. package/dist/public/llms-template.txt +9 -6
  23. package/dist/public/showroom.html +104 -0
  24. package/dist/public/showroom.js +247 -0
  25. package/dist/public/spa.html +18 -16
  26. package/dist/public/views/aimeat-os.js +3 -2
  27. package/dist/public/views/landing.js +50 -6
  28. package/dist/public/views/profile/agents/agent-card.js +7 -0
  29. package/dist/public/views/profile/agents/state-detector.js +6 -0
  30. package/dist/public/views/profile/agents/tab-integration.js +6 -0
  31. package/dist/public/views/profile/apps-tab.js +46 -0
  32. package/dist/public/views/profile/extensions-tab.js +4 -0
  33. package/dist/public/views/profile/inbox-tab.js +1 -1
  34. package/dist/public/views/profile/landing-page.js +8 -3
  35. package/dist/public/views/profile/memory-tab.js +37 -6
  36. package/dist/public/views/profile/organisms/home.js +32 -0
  37. package/dist/public/views/profile/organisms/workspace-list.js +36 -5
  38. package/dist/public/views/profile/organisms/workspace.js +29 -4
  39. package/dist/public/views/profile/organisms-tab.js +61 -8
  40. package/dist/public/views/secretary/calendar.js +327 -0
  41. package/dist/public/views/secretary/cards-reach.js +16 -17
  42. package/dist/public/views/secretary/cards.js +173 -66
  43. package/dist/public/views/secretary/dashboard.js +551 -0
  44. package/dist/public/views/secretary/quality.js +156 -0
  45. package/dist/public/views/secretary/use-autonomy.js +17 -2
  46. package/dist/public/views/secretary/use-freshness.js +50 -0
  47. package/dist/public/views/secretary/use-intake.js +23 -4
  48. package/dist/public/views/secretary/use-layout.js +122 -0
  49. package/dist/public/views/secretary/use-quick-actions.js +98 -0
  50. package/dist/public/views/secretary/use-specialists.js +103 -0
  51. package/dist/public/views/secretary/use-triggers.js +159 -0
  52. package/dist/public/views/secretary/use-whats-next.js +340 -0
  53. package/dist/public/views/secretary/workflow-design.js +112 -0
  54. package/dist/public/views/secretary.js +663 -39
  55. package/dist/scripts/check-no-max-tokens.d.ts +2 -0
  56. package/dist/scripts/check-no-max-tokens.d.ts.map +1 -0
  57. package/dist/scripts/check-no-max-tokens.js +71 -0
  58. package/dist/scripts/check-no-max-tokens.js.map +1 -0
  59. package/dist/src/data/aimeat-iam-package.d.ts +25 -0
  60. package/dist/src/data/aimeat-iam-package.d.ts.map +1 -0
  61. package/dist/src/data/aimeat-iam-package.js +312 -0
  62. package/dist/src/data/aimeat-iam-package.js.map +1 -0
  63. package/dist/src/data/aimeat-marketplace-package.d.ts +25 -0
  64. package/dist/src/data/aimeat-marketplace-package.d.ts.map +1 -0
  65. package/dist/src/data/aimeat-marketplace-package.js +231 -0
  66. package/dist/src/data/aimeat-marketplace-package.js.map +1 -0
  67. package/dist/src/data/app-templates.d.ts +48 -0
  68. package/dist/src/data/app-templates.d.ts.map +1 -0
  69. package/dist/src/data/app-templates.js +856 -0
  70. package/dist/src/data/app-templates.js.map +1 -0
  71. package/dist/src/data/example-packages.d.ts.map +1 -1
  72. package/dist/src/data/example-packages.js +3 -1
  73. package/dist/src/data/example-packages.js.map +1 -1
  74. package/dist/src/generated/api-types.d.ts +380 -5
  75. package/dist/src/generated/api-types.d.ts.map +1 -1
  76. package/dist/src/generated/prisma-postgres/edge.js +10 -3
  77. package/dist/src/generated/prisma-postgres/index-browser.js +7 -0
  78. package/dist/src/generated/prisma-postgres/index.d.ts +205 -2
  79. package/dist/src/generated/prisma-postgres/index.js +10 -3
  80. package/dist/src/generated/prisma-postgres/package.json +1 -1
  81. package/dist/src/generated/prisma-postgres/schema.prisma +23 -13
  82. package/dist/src/generated/prisma-postgres/wasm.js +10 -3
  83. package/dist/src/mcp/annotations.d.ts.map +1 -1
  84. package/dist/src/mcp/annotations.js +1 -0
  85. package/dist/src/mcp/annotations.js.map +1 -1
  86. package/dist/src/mcp/catalog/definitions.d.ts.map +1 -1
  87. package/dist/src/mcp/catalog/definitions.js +16 -1
  88. package/dist/src/mcp/catalog/definitions.js.map +1 -1
  89. package/dist/src/mcp/catalog/surfaces.js +3 -3
  90. package/dist/src/mcp/catalog/surfaces.js.map +1 -1
  91. package/dist/src/mcp/index.d.ts +3 -0
  92. package/dist/src/mcp/index.d.ts.map +1 -1
  93. package/dist/src/mcp/index.js +11 -0
  94. package/dist/src/mcp/index.js.map +1 -1
  95. package/dist/src/mcp/organisms.d.ts.map +1 -1
  96. package/dist/src/mcp/organisms.js +48 -8
  97. package/dist/src/mcp/organisms.js.map +1 -1
  98. package/dist/src/mcp/workspaces.d.ts.map +1 -1
  99. package/dist/src/mcp/workspaces.js +6 -4
  100. package/dist/src/mcp/workspaces.js.map +1 -1
  101. package/dist/src/models/offer-schemas.d.ts +8 -8
  102. package/dist/src/routes/ai.d.ts +8 -4
  103. package/dist/src/routes/ai.d.ts.map +1 -1
  104. package/dist/src/routes/ai.js +38 -14
  105. package/dist/src/routes/ai.js.map +1 -1
  106. package/dist/src/routes/app-grants.d.ts +2 -0
  107. package/dist/src/routes/app-grants.d.ts.map +1 -1
  108. package/dist/src/routes/app-grants.js +7 -1
  109. package/dist/src/routes/app-grants.js.map +1 -1
  110. package/dist/src/routes/app-templates.d.ts +16 -0
  111. package/dist/src/routes/app-templates.d.ts.map +1 -0
  112. package/dist/src/routes/app-templates.js +40 -0
  113. package/dist/src/routes/app-templates.js.map +1 -0
  114. package/dist/src/routes/apps.d.ts.map +1 -1
  115. package/dist/src/routes/apps.js +55 -5
  116. package/dist/src/routes/apps.js.map +1 -1
  117. package/dist/src/routes/auth.d.ts +3 -0
  118. package/dist/src/routes/auth.d.ts.map +1 -1
  119. package/dist/src/routes/auth.js +9 -4
  120. package/dist/src/routes/auth.js.map +1 -1
  121. package/dist/src/routes/bootstrap.d.ts.map +1 -1
  122. package/dist/src/routes/bootstrap.js +1 -0
  123. package/dist/src/routes/bootstrap.js.map +1 -1
  124. package/dist/src/routes/generator.d.ts.map +1 -1
  125. package/dist/src/routes/generator.js +3 -2
  126. package/dist/src/routes/generator.js.map +1 -1
  127. package/dist/src/routes/lib-ai.d.ts +0 -1
  128. package/dist/src/routes/lib-ai.d.ts.map +1 -1
  129. package/dist/src/routes/lib-ai.js +13 -6
  130. package/dist/src/routes/lib-ai.js.map +1 -1
  131. package/dist/src/routes/lib-header.d.ts +2 -0
  132. package/dist/src/routes/lib-header.d.ts.map +1 -1
  133. package/dist/src/routes/lib-header.js +7 -16
  134. package/dist/src/routes/lib-header.js.map +1 -1
  135. package/dist/src/routes/lib-markdown.d.ts +18 -0
  136. package/dist/src/routes/lib-markdown.d.ts.map +1 -0
  137. package/dist/src/routes/lib-markdown.js +236 -0
  138. package/dist/src/routes/lib-markdown.js.map +1 -0
  139. package/dist/src/routes/libs.d.ts +22 -0
  140. package/dist/src/routes/libs.d.ts.map +1 -1
  141. package/dist/src/routes/libs.js +133 -10
  142. package/dist/src/routes/libs.js.map +1 -1
  143. package/dist/src/routes/memory.d.ts.map +1 -1
  144. package/dist/src/routes/memory.js +21 -2
  145. package/dist/src/routes/memory.js.map +1 -1
  146. package/dist/src/routes/openrouter.d.ts +4 -0
  147. package/dist/src/routes/openrouter.d.ts.map +1 -1
  148. package/dist/src/routes/openrouter.js +17 -0
  149. package/dist/src/routes/openrouter.js.map +1 -1
  150. package/dist/src/routes/organisms.d.ts.map +1 -1
  151. package/dist/src/routes/organisms.js +107 -7
  152. package/dist/src/routes/organisms.js.map +1 -1
  153. package/dist/src/routes/portal.d.ts +9 -0
  154. package/dist/src/routes/portal.d.ts.map +1 -1
  155. package/dist/src/routes/portal.js +1 -1
  156. package/dist/src/routes/portal.js.map +1 -1
  157. package/dist/src/routes/public-stats.d.ts +3 -0
  158. package/dist/src/routes/public-stats.d.ts.map +1 -1
  159. package/dist/src/routes/public-stats.js +9 -2
  160. package/dist/src/routes/public-stats.js.map +1 -1
  161. package/dist/src/routes/secretary.d.ts +29 -0
  162. package/dist/src/routes/secretary.d.ts.map +1 -0
  163. package/dist/src/routes/secretary.js +220 -0
  164. package/dist/src/routes/secretary.js.map +1 -0
  165. package/dist/src/routes/specialists.d.ts.map +1 -1
  166. package/dist/src/routes/specialists.js +21 -0
  167. package/dist/src/routes/specialists.js.map +1 -1
  168. package/dist/src/routes/subdomains.js +1 -1
  169. package/dist/src/routes/subdomains.js.map +1 -1
  170. package/dist/src/routes/workflows.d.ts +9 -3
  171. package/dist/src/routes/workflows.d.ts.map +1 -1
  172. package/dist/src/routes/workflows.js +9 -3
  173. package/dist/src/routes/workflows.js.map +1 -1
  174. package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
  175. package/dist/src/server-bootstrap/routes-loader.js +4 -0
  176. package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
  177. package/dist/src/server-bootstrap/service-init.d.ts.map +1 -1
  178. package/dist/src/server-bootstrap/service-init.js +7 -0
  179. package/dist/src/server-bootstrap/service-init.js.map +1 -1
  180. package/dist/src/server-bootstrap/static-files.d.ts +4 -0
  181. package/dist/src/server-bootstrap/static-files.d.ts.map +1 -1
  182. package/dist/src/server-bootstrap/static-files.js +18 -0
  183. package/dist/src/server-bootstrap/static-files.js.map +1 -1
  184. package/dist/src/services/archive.d.ts +74 -0
  185. package/dist/src/services/archive.d.ts.map +1 -0
  186. package/dist/src/services/archive.js +132 -0
  187. package/dist/src/services/archive.js.map +1 -0
  188. package/dist/src/services/cortex-seeder.d.ts +4 -1
  189. package/dist/src/services/cortex-seeder.d.ts.map +1 -1
  190. package/dist/src/services/cortex-seeder.js +25 -6
  191. package/dist/src/services/cortex-seeder.js.map +1 -1
  192. package/dist/src/services/directory.d.ts.map +1 -1
  193. package/dist/src/services/directory.js +3 -2
  194. package/dist/src/services/directory.js.map +1 -1
  195. package/dist/src/services/generator-prompt-seeds.d.ts.map +1 -1
  196. package/dist/src/services/generator-prompt-seeds.js +5 -4
  197. package/dist/src/services/generator-prompt-seeds.js.map +1 -1
  198. package/dist/src/services/message-send.d.ts.map +1 -1
  199. package/dist/src/services/message-send.js +14 -0
  200. package/dist/src/services/message-send.js.map +1 -1
  201. package/dist/src/services/organism-search.d.ts +6 -2
  202. package/dist/src/services/organism-search.d.ts.map +1 -1
  203. package/dist/src/services/organism-search.js +2 -2
  204. package/dist/src/services/organism-search.js.map +1 -1
  205. package/dist/src/services/owner-memory.d.ts +1 -0
  206. package/dist/src/services/owner-memory.d.ts.map +1 -1
  207. package/dist/src/services/owner-memory.js.map +1 -1
  208. package/dist/src/services/owner-session.d.ts +1 -0
  209. package/dist/src/services/owner-session.d.ts.map +1 -1
  210. package/dist/src/services/owner-session.js +10 -2
  211. package/dist/src/services/owner-session.js.map +1 -1
  212. package/dist/src/services/package-seeder.d.ts +22 -0
  213. package/dist/src/services/package-seeder.d.ts.map +1 -0
  214. package/dist/src/services/package-seeder.js +30 -0
  215. package/dist/src/services/package-seeder.js.map +1 -0
  216. package/dist/src/services/presence.d.ts +3 -0
  217. package/dist/src/services/presence.d.ts.map +1 -1
  218. package/dist/src/services/presence.js +10 -0
  219. package/dist/src/services/presence.js.map +1 -1
  220. package/dist/src/services/prompt-defaults.d.ts.map +1 -1
  221. package/dist/src/services/prompt-defaults.js +8 -3
  222. package/dist/src/services/prompt-defaults.js.map +1 -1
  223. package/dist/src/services/scheduler.d.ts +41 -2
  224. package/dist/src/services/scheduler.d.ts.map +1 -1
  225. package/dist/src/services/scheduler.js +429 -26
  226. package/dist/src/services/scheduler.js.map +1 -1
  227. package/dist/src/services/secretary-authoring.d.ts +71 -0
  228. package/dist/src/services/secretary-authoring.d.ts.map +1 -0
  229. package/dist/src/services/secretary-authoring.js +336 -0
  230. package/dist/src/services/secretary-authoring.js.map +1 -0
  231. package/dist/src/services/secretary-clarify.d.ts +57 -0
  232. package/dist/src/services/secretary-clarify.d.ts.map +1 -0
  233. package/dist/src/services/secretary-clarify.js +104 -0
  234. package/dist/src/services/secretary-clarify.js.map +1 -0
  235. package/dist/src/services/secretary-feed.d.ts +11 -0
  236. package/dist/src/services/secretary-feed.d.ts.map +1 -0
  237. package/dist/src/services/secretary-feed.js +28 -0
  238. package/dist/src/services/secretary-feed.js.map +1 -0
  239. package/dist/src/services/secretary-tick.d.ts +243 -3
  240. package/dist/src/services/secretary-tick.d.ts.map +1 -1
  241. package/dist/src/services/secretary-tick.js +273 -3
  242. package/dist/src/services/secretary-tick.js.map +1 -1
  243. package/dist/src/services/specialist-runner.d.ts +36 -0
  244. package/dist/src/services/specialist-runner.d.ts.map +1 -0
  245. package/dist/src/services/specialist-runner.js +91 -0
  246. package/dist/src/services/specialist-runner.js.map +1 -0
  247. package/dist/src/services/specialist.d.ts +9 -0
  248. package/dist/src/services/specialist.d.ts.map +1 -1
  249. package/dist/src/services/specialist.js +42 -0
  250. package/dist/src/services/specialist.js.map +1 -1
  251. package/dist/src/services/structure-overview.d.ts +13 -0
  252. package/dist/src/services/structure-overview.d.ts.map +1 -1
  253. package/dist/src/services/structure-overview.js +34 -11
  254. package/dist/src/services/structure-overview.js.map +1 -1
  255. package/dist/src/services/system-agent-responder.d.ts +36 -0
  256. package/dist/src/services/system-agent-responder.d.ts.map +1 -0
  257. package/dist/src/services/system-agent-responder.js +107 -0
  258. package/dist/src/services/system-agent-responder.js.map +1 -0
  259. package/dist/src/services/system-agent.d.ts +34 -1
  260. package/dist/src/services/system-agent.d.ts.map +1 -1
  261. package/dist/src/services/system-agent.js +73 -0
  262. package/dist/src/services/system-agent.js.map +1 -1
  263. package/dist/src/storage/interface.d.ts +33 -0
  264. package/dist/src/storage/interface.d.ts.map +1 -1
  265. package/dist/src/storage/interface.js.map +1 -1
  266. package/dist/src/storage/providers/mongodb/index.d.ts +37 -0
  267. package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
  268. package/dist/src/storage/providers/mongodb/index.js +132 -12
  269. package/dist/src/storage/providers/mongodb/index.js.map +1 -1
  270. package/dist/src/storage/providers/sqlite/index.d.ts +23 -1
  271. package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
  272. package/dist/src/storage/providers/sqlite/index.js +60 -8
  273. package/dist/src/storage/providers/sqlite/index.js.map +1 -1
  274. package/dist/src/storage/providers/sqlite/repos/memory.d.ts +29 -1
  275. package/dist/src/storage/providers/sqlite/repos/memory.d.ts.map +1 -1
  276. package/dist/src/storage/providers/sqlite/repos/memory.js +109 -10
  277. package/dist/src/storage/providers/sqlite/repos/memory.js.map +1 -1
  278. package/dist/src/storage/providers/sqlite/schema.d.ts +3 -0
  279. package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
  280. package/dist/src/storage/providers/sqlite/schema.js +64 -9
  281. package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
  282. package/dist/src/storage/repositories/app.repository.d.ts +15 -0
  283. package/dist/src/storage/repositories/app.repository.d.ts.map +1 -1
  284. package/dist/src/storage/repositories/memory.repository.d.ts +41 -1
  285. package/dist/src/storage/repositories/memory.repository.d.ts.map +1 -1
  286. package/dist/src/storage/repositories/organism.repository.d.ts +2 -1
  287. package/dist/src/storage/repositories/organism.repository.d.ts.map +1 -1
  288. package/dist/static/app-catalog.html +469 -122
  289. package/package.json +2 -1
  290. package/prisma/schema.postgres.prisma +10 -0
  291. package/prisma/schema.prisma +12 -0
@@ -5,6 +5,21 @@
5
5
  * Supports special @activate trigger: runs on extension activation AND every server startup.
6
6
  * Every execution creates an ExecutionLogEntry with timing, result, and memory I/O.
7
7
  * @version-history
8
+ * v2.12.0 — 2026-06-30 — Learning loop closes: (A) selectLessons threads the highest-signal reviewed-
9
+ * decision scores into the tick's briefing/action prompt (repeat what worked, avoid what didn't);
10
+ * (B) proposeLearnedRule — when a cluster of reviewed decisions scored poorly, the tick asks for ONE
11
+ * durable operating rule and posts it as a GATED inbox card (brain-rule) the owner approves; never
12
+ * auto-applied. Source decisions flagged proposalUsed so a cluster doesn't re-propose.
13
+ * v2.11.0 — 2026-06-30 — Secretary autonomous authoring: a band-gated phase (`author_content`) fills
14
+ * the active context's EMPTY workspaces — document spaces first, then records — toward the focus
15
+ * milestone (services/secretary-authoring.ts). act → write + publish · draft/ask → leave drafts ·
16
+ * off (default) → skip. Metered per workspace, capped by the same per-day morsel budget.
17
+ * v2.10.0 — 2026-06-28 — Secretary Strategy: the tick reads the active context's optional `strategy`
18
+ * (current → target + principles/risks + ordered milestones) and steers the briefing toward the
19
+ * current focus milestone (first not-yet-reached), respecting the principles + flagging risks.
20
+ * v2.9.0 — 2026-06-28 — Secretary bound triggers: a fired trigger can carry out a BOUND action —
21
+ * start an Agent Workflow run, or queue + run a specialist task (its prompt) node-side this tick —
22
+ * instead of only posting a reminder (fireBoundTriggerAction). Gated by the existing spend guards.
8
23
  * v2.8.0 — 2026-06-24 — Secretary P5 (S-C): scheduled extension jobs decrypt `type: secret` config
9
24
  * before the sandbox VM, and an instance-scoped job loads the instance's (decrypted) config so a
10
25
  * cron sync uses the same bring-your-own-key secret a live action would. See extension-secrets.ts.
@@ -22,6 +37,9 @@
22
37
  * v2.5.0 — 2026-06-24 — Add the `secretary` kind: the Secretary's autonomous tick (Phase 4) — runs
23
38
  * the active context's brain on the owner's key and appends a briefing to `secretary.feed`;
24
39
  * stop-spending skips the paid call.
40
+ * v2.5.1 — 2026-06-29 — Secretary tick: pre-flight AI-availability guard — when the owner has no usable
41
+ * AI key, skip the tick gracefully (like stop-spending/budget) instead of erroring every run once the
42
+ * "daily check-in always briefs" change removed the idle early-out.
25
43
  * v1.0.0 — 2026-03-01 — Initial implementation with croner
26
44
  * v2.0.0 — 2026-03-15 — Add @activate trigger, execution log, memory access tracking
27
45
  * v2.1.0 — 2026-06-05 — executeJob/triggerNow return a JobOutcome so a manual
@@ -42,11 +60,16 @@ import { executeExtensionAction, trackMemoryAccess } from './extension-runtime.j
42
60
  import { getEncryptionKey } from './encryption.js';
43
61
  import { getExtSecretKeys, getInstanceSecretKeys, decryptSecretFields } from './extension-secrets.js';
44
62
  import { completeForOwner } from './ai-completion.js';
63
+ import { authorWorkspaceContent } from './secretary-authoring.js';
64
+ import { appendSecretaryFeed } from './secretary-feed.js';
65
+ import { produceClarifyDeliverable } from './secretary-clarify.js';
66
+ import { listSpecialists } from './specialist.js';
67
+ import { runAllSpecialists, runSpecialistQueue } from './specialist-runner.js';
45
68
  import { getActiveWorkflowEngine } from './workflow/engine.js';
46
69
  import { getActiveConnectTunnelManager } from './connect-tunnel.js';
47
70
  import { parseGaiiLoose, buildGEAI } from '../utils/gaii.js';
48
71
  import { evaluateConstraints, applyAfterRun } from './schedule-constraints.js';
49
- import { classifySecretaryActions, hasWorkToDo, ledgerSpentToday, budgetExceeded, bumpLedger, routeTickNote } from './secretary-tick.js';
72
+ import { classifySecretaryActions, actionKind, ledgerSpentToday, budgetExceeded, bumpLedger, routeTickNote, deriveRoutineActions, actionItemKey, routineDueForRearm, rearmRoutine, isAllowedQuickAction, evaluateTriggers, settleFiredTrigger, selectLessons, poorDecisionCluster } from './secretary-tick.js';
50
73
  import { emitChange } from './event-bus.js';
51
74
  import { emitResourceUpdated } from '../mcp/index.js';
52
75
  import { logger } from '../utils/logger.js';
@@ -499,6 +522,14 @@ export class Scheduler {
499
522
  this.notifyOwner(job, 'Secretary paused for today', `daily budget reached (${spentToday}/${budget} morsels)`);
500
523
  return { reads: ['secretary.config'], writes: [], skipped: true, skipReason: `daily morsel budget reached (${spentToday}/${budget})` };
501
524
  }
525
+ // (2.5) AI-availability guard: the autonomous tick's core output is a PAID AI briefing — the daily
526
+ // check-in always briefs (there is no "nothing to do" early-out). If the owner has no usable AI key,
527
+ // no paid call can run this tick, so skip gracefully — exactly like the stop-spending / budget guards
528
+ // above — instead of letting completeForOwner throw NO_API_KEY and marking EVERY tick "failed" (which
529
+ // also fires a "Schedule failed" notification each run). The tick resumes once a key is set in Settings.
530
+ if (!(await this.ownerHasUsableAiKey(owner))) {
531
+ return { reads: ['secretary.config', 'openrouter.apikey'], writes: [], skipped: true, skipReason: 'no AI key configured' };
532
+ }
502
533
  // (3) Cheap "anything to do?" pre-check (P1-B): no open goals + no due decisions → skip the paid call.
503
534
  const goalRecs = await this.storage.listMemory(owner, { prefix: 'secretary.goal.' });
504
535
  const openGoals = goalRecs
@@ -509,12 +540,74 @@ export class Scheduler {
509
540
  const dueDecisions = decRecs
510
541
  .map((r) => (r.value ?? {}))
511
542
  .filter((d) => d.status === 'open' && typeof d.revisitWhen === 'string' && Date.parse(d.revisitWhen) <= nowMs);
512
- if (!hasWorkToDo({ openGoals: openGoals.length, dueDecisions: dueDecisions.length })) {
513
- return { reads: ['secretary.config', 'secretary.goal.*', 'secretary.decision.*'], writes: [], skipped: true, skipReason: 'nothing to do (no open goals or decisions due)' };
543
+ // (3a) G2: re-arm any RECURRING routine whose cadence has elapsed since its last completed run —
544
+ // reset its steps to 'pending' + re-activate it (FREE, no AI). Applied to the working `active` so the
545
+ // advancement pass below picks up the freshly-pending steps; persisted via persistTickState.
546
+ const rearmNowIso = new Date().toISOString();
547
+ const rearmNowMs = Date.now();
548
+ const rearmedRoutines = [];
549
+ if (Array.isArray(active.routines)) {
550
+ active.routines = active.routines.map((r) => {
551
+ if (r.id && routineDueForRearm(r, rearmNowMs)) {
552
+ const armed = rearmRoutine(r, rearmNowIso, rearmNowMs);
553
+ rearmedRoutines.push({ id: r.id, routine: armed });
554
+ return armed;
555
+ }
556
+ return r;
557
+ });
514
558
  }
559
+ // (3a2) Triggers (Slice 2): evaluate the owner's triggers — fire any that are due (time/recurring),
560
+ // whose bound target completed, or whose condition is met. FREE (no AI). Signals from memory.
561
+ const trigRecs = await this.storage.listMemory(owner, { prefix: 'secretary.trigger.' });
562
+ const triggers = trigRecs.map((r) => (r.value ?? {})).filter((tr) => tr && tr.id);
563
+ let firedTriggers = [];
564
+ if (triggers.length) {
565
+ const completed = {};
566
+ for (const r of goalRecs) {
567
+ const g = (r.value ?? {});
568
+ if (g.id)
569
+ completed[String(g.id)] = g.status === 'done';
570
+ }
571
+ for (const c of contexts)
572
+ for (const rt of (c.routines || []))
573
+ if (rt.id)
574
+ completed[rt.id] = rt.status === 'done';
575
+ const memoryCounts = {};
576
+ const lastActivityMs = {};
577
+ const prefixes = Array.from(new Set(triggers.filter((tr) => tr.kind === 'condition' && tr.condition?.prefix).map((tr) => tr.condition.prefix)));
578
+ for (const pfx of prefixes) {
579
+ const recs = await this.storage.listMemory(owner, { prefix: pfx });
580
+ memoryCounts[pfx] = recs.length;
581
+ let newest = 0;
582
+ for (const r of recs) {
583
+ const ts = Date.parse(r.updatedAt || '');
584
+ if (!Number.isNaN(ts) && ts > newest)
585
+ newest = ts;
586
+ }
587
+ if (newest)
588
+ lastActivityMs[pfx] = newest;
589
+ }
590
+ firedTriggers = evaluateTriggers(triggers, { completed, memoryCounts, lastActivityMs }, nowMs).map((f) => f.trigger);
591
+ }
592
+ // (3a2) Clarify jobs the owner may have answered: the Secretary asked questions instead of guessing;
593
+ // once answered (in the inbox) the tick produces the deliverable grounded in those answers.
594
+ const clarifyRecs = await this.storage.listMemory(owner, { prefix: 'secretary.clarify.' });
595
+ const askedClarify = clarifyRecs.map((r) => (r.value ?? {})).filter((j) => j && j.status === 'asked');
596
+ // (3a3) Specialists the Secretary directs: any with QUEUED tasks have node-run work to do this tick.
597
+ const specialistAgents = await listSpecialists(this.storage, owner.split('@')[0]);
598
+ const hasSpecialistWork = specialistAgents.length > 0
599
+ && (await Promise.all(specialistAgents.map((s) => this.storage.listAgentTasks(s.gaii, { status: 'queued', perPage: 1 })))).some((r) => r.total > 0);
600
+ // (3b) B5: band-driven routine advancement (FREE — no AI). Decide, from each active routine's next
601
+ // step band, what the tick should auto-run (act-band file steps) vs surface as a follow-up action-item.
602
+ const routineActions = deriveRoutineActions(active);
603
+ // The daily check-in always produces a briefing + proposed next actions (its whole purpose) — it never
604
+ // skips for "nothing to do". Cost is bounded by the earlier stop-spending + daily-budget guards.
515
605
  const ownerName = owner.split('@')[0];
516
606
  const writes = [];
517
607
  const newPending = {};
608
+ // B5: routine step completions (act-band file steps the tick ran) + the action-items it derived.
609
+ const stepCompletions = [];
610
+ let newActionItems = [];
518
611
  let paidMorsels = 0;
519
612
  try {
520
613
  // Learning loop (Phase 5): score any decisions whose revisit time passed (each = 1 morsel).
@@ -525,10 +618,76 @@ export class Scheduler {
525
618
  await this.appendFeed(owner, { kind: 'review', contextId: active.id, contextName: active.name || '', text: `Reviewed ${reviewed.length} decision(s):\n${summary}` });
526
619
  writes.push(...reviewed.map((r) => `secretary.decision.${r.id}`));
527
620
  }
621
+ // Learning loop (B): when a cluster of reviewed decisions scored poorly, propose ONE durable
622
+ // operating rule — GATED: posted as an inbox card the owner approves; never auto-applied to the brain.
623
+ const proposal = await this.proposeLearnedRule(owner, ownerName, active, cfg.pendingDecisions ?? {});
624
+ if (proposal) {
625
+ newPending[proposal.id] = proposal.pending;
626
+ paidMorsels += 1;
627
+ writes.push('agent-message', ...proposal.writes);
628
+ }
629
+ const wsList = await this.loadContextWorkspaces(owner, active);
630
+ // Resume any answered clarify jobs: produce the deliverable grounded in the owner's answers (+ facts).
631
+ const clarifyWrites = await this.resumeClarifyJobs(owner, ownerName, askedClarify);
632
+ writes.push(...clarifyWrites);
633
+ paidMorsels += clarifyWrites.filter((w) => w.startsWith('organism.')).length;
634
+ // Direct the specialists: run each specialist's queued tasks node-side (owner key + quality contract).
635
+ if (hasSpecialistWork) {
636
+ const specialistWrites = await runAllSpecialists(this.storage, this.config, owner, ownerName);
637
+ if (specialistWrites.length) {
638
+ writes.push(...specialistWrites);
639
+ paidMorsels += specialistWrites.length;
640
+ await this.appendFeed(owner, { kind: 'act', contextId: active.id, contextName: active.name || '', text: `🧩 Specialists produced ${specialistWrites.length} deliverable(s)` });
641
+ writes.push('secretary.feed');
642
+ }
643
+ }
644
+ // B5 routine pass (FREE — no AI): auto-run each act-band performable step + mark it done, and stamp
645
+ // the derived follow-up action-items. draft/ask/delegate steps were already turned into action-items
646
+ // by deriveRoutineActions — the owner handles those from the dashboard. G4: the step's kind comes
647
+ // from actionKind (note caps file a note; briefing/reminders append a feed entry) — performSecretaryAct
648
+ // carries out the right one, so briefing/reminders are FED here exactly as in the legacy action-loop.
649
+ const nowIso = new Date().toISOString();
650
+ for (const fs of routineActions.runSteps) {
651
+ const routine = (active.routines || []).find((r) => r.id === fs.routineId);
652
+ const step = (routine?.steps || []).find((s) => s.id === fs.stepId);
653
+ if (!step)
654
+ continue;
655
+ const kind = actionKind(String(step.capability)); // 'note' | 'feed'
656
+ const a = { capability: String(step.capability), summary: String(step.summary || ''), payload: { note: String(step.summary || '') }, kind, band: 'act' };
657
+ writes.push(...await this.performSecretaryAct(owner, active, a, wsList));
658
+ const verb = kind === 'feed' ? 'Surfaced' : 'Filed';
659
+ stepCompletions.push({ routineId: fs.routineId, stepId: fs.stepId, status: 'done', summary: `${verb}: ${String(step.summary || '')}`.slice(0, 200) });
660
+ }
661
+ newActionItems = routineActions.items.map((it) => ({
662
+ id: 'ai-' + randomUUID().slice(0, 8), labelKind: it.labelKind, summary: it.summary, suggestedAction: it.suggestedAction, source: it.source, createdAt: nowIso, status: 'open',
663
+ }));
664
+ // Fire triggers (Slice 2 + bound actions): each fired trigger either just REMINDS (feed entry +
665
+ // action-item, the default) or carries out a BOUND action — start an Agent Workflow run, or queue +
666
+ // run a task for one of the owner's specialists. A bound action that ran does the work, so no
667
+ // follow-up action-item is added (the feed records what happened). Then the trigger settles
668
+ // (recurring re-arms with a new nextFireAt; one-off/completion/condition → 'fired'). Paid bound
669
+ // actions (workflow/specialist) are metered like any autonomous spend; the earlier stop-spending +
670
+ // daily-budget guards already gate this whole block.
671
+ for (const tr of firedTriggers) {
672
+ const label = String(tr.label || tr.action?.summary || 'Trigger').slice(0, 200);
673
+ const bound = await this.fireBoundTriggerAction(owner, ownerName, tr, label, specialistAgents, writes);
674
+ await this.appendFeed(owner, { kind: 'trigger', contextId: tr.contextId || active.id, contextName: active.name || '', text: bound ? `⏰ ${label} → ${bound.note}` : `⏰ ${label}` });
675
+ if (bound)
676
+ paidMorsels += bound.morsels;
677
+ else
678
+ newActionItems.push({ id: 'ai-' + randomUUID().slice(0, 8), text: label, suggestedAction: { kind: 'advance', routineId: '' }, source: 'trigger:' + tr.id, createdAt: nowIso, status: 'open' });
679
+ const settled = settleFiredTrigger(tr, nowMs);
680
+ const key = `secretary.trigger.${tr.id}`;
681
+ const ex = trigRecs.find((r) => r.key === key);
682
+ await this.storage.setMemory({ key, ownerGaii: owner, value: settled, visibility: 'private', tags: ['secretary', 'trigger', String(settled.status || '')], ttlHours: null, version: ex ? ex.version + 1 : 1, createdAt: ex?.createdAt ?? nowIso, updatedAt: nowIso });
683
+ writes.push(key);
684
+ }
528
685
  // Action generation (P1-A): ask the model for a STRUCTURED action list tied to the open goals +
529
686
  // a bounded slice of the self-organism (the context's workspaces). 1 paid morsel.
530
- const wsList = await this.loadContextWorkspaces(owner, active);
531
- const { systemPrompt, prompt } = this.buildTickPrompt(ownerName, active, openGoals, wsList);
687
+ // Learning loop (A): thread the highest-signal reviewed-decision lessons into the prompt so the next
688
+ // cycle repeats what scored well and avoids what scored poorly (the scores aren't a dead-end record).
689
+ const lessons = selectLessons(decRecs.map((r) => (r.value ?? {})), active.id);
690
+ const { systemPrompt, prompt } = this.buildTickPrompt(ownerName, active, openGoals, wsList, lessons);
532
691
  const result = await completeForOwner(this.storage, this.config, owner, { prompt, systemPrompt, appId: `schedule:${job.id}` });
533
692
  paidMorsels += 1;
534
693
  const parsed = extractJsonObject(result.content);
@@ -576,19 +735,62 @@ export class Scheduler {
576
735
  const feedText = deferred.length ? `${briefingText}\n\n${deferred.join('\n')}` : briefingText;
577
736
  await this.appendFeed(owner, { kind: 'briefing', contextId: active.id, contextName: active.name || '', text: feedText });
578
737
  writes.push('secretary.feed');
738
+ // Autonomous authoring phase: actually FILL the workspaces (document-first). Band-gated on
739
+ // `author_content`: 'act' = write + publish · 'draft' = leave drafts for review · 'off' = skip.
740
+ // Each filled workspace is a paid AI call, so it respects the same per-day budget — pass the
741
+ // remaining headroom and let authoring stop when it's exhausted. OFF by default (it spends + can
742
+ // publish): a context that never opts in behaves exactly as before; the owner enables draft/act.
743
+ const authorBand = active.policy?.bands?.author_content ?? 'off';
744
+ if (authorBand !== 'off') {
745
+ const budgetRemaining = budget === null ? null : Math.max(0, budget - spentToday - paidMorsels);
746
+ if (budgetRemaining === null || budgetRemaining > 0) {
747
+ // Honour the owner's autoRetry/maxRetries so a flaky stealth model (owl-alpha intermittently 400s)
748
+ // is retried instead of killing the authoring on the first transient error. Off → no retry.
749
+ const aiPrefs = (await this.storage.getMemory(owner, 'openrouter.settings'))?.value;
750
+ const maxRetries = aiPrefs?.autoRetry === false ? 0 : (typeof aiPrefs?.maxRetries === 'number' ? aiPrefs.maxRetries : 3);
751
+ const authored = await authorWorkspaceContent(this.storage, this.config, owner, ownerName, active, wsList, {
752
+ openGoals, band: authorBand, aggressive: true, useGeneralKnowledge: true, budgetRemaining, maxRetries, appId: `schedule:${job.id}:author`,
753
+ });
754
+ paidMorsels += authored.morsels;
755
+ writes.push(...authored.writes);
756
+ for (const line of authored.summaries) {
757
+ await this.appendFeed(owner, { kind: 'act', contextId: active.id, contextName: active.name || '', text: line, href: `/v1/profile?tab=organisms&org=${active.organismId}` });
758
+ }
759
+ if (authored.summaries.length)
760
+ writes.push('secretary.feed');
761
+ }
762
+ }
579
763
  }
580
764
  finally {
581
- // Persist the spend ledger (+ any new pending decisions) even if generation threw mid-way, so the
582
- // soft budget reflects what was actually spent. Read-modify-write a fresh config to avoid clobber.
583
- if (paidMorsels > 0 || Object.keys(newPending).length) {
584
- await this.persistTickState(owner, active.id, today, paidMorsels, newPending);
765
+ // Persist the spend ledger (+ any new pending decisions, routine step completions, and derived
766
+ // action-items) even if generation threw mid-way, so the soft budget reflects what was actually
767
+ // spent. Read-modify-write a fresh config to avoid clobber.
768
+ if (paidMorsels > 0 || Object.keys(newPending).length || stepCompletions.length || newActionItems.length || rearmedRoutines.length) {
769
+ await this.persistTickState(owner, active.id, today, paidMorsels, newPending, stepCompletions, newActionItems, rearmedRoutines);
585
770
  writes.push('secretary.config');
586
771
  }
587
772
  }
588
773
  if (Object.keys(newPending).length)
589
774
  emitChange('agent-messages');
775
+ if (stepCompletions.length || newActionItems.length || rearmedRoutines.length)
776
+ emitChange('agent-tasks', owner);
590
777
  return { reads: ['secretary.config', 'secretary.goal.*', 'secretary.decision.*'], writes };
591
778
  }
779
+ /**
780
+ * True when the owner has a usable AI key for autonomous completions. Mirrors the key resolution in
781
+ * completeForOwner(): the default 'openrouter' provider REQUIRES an encrypted key; other providers
782
+ * (e.g. a local model) may run keyless. Used as the Secretary tick's pre-flight guard so the tick
783
+ * skips rather than errors when no paid call is possible.
784
+ */
785
+ async ownerHasUsableAiKey(owner) {
786
+ const [keyRec, prefsRec] = await Promise.all([
787
+ this.storage.getMemory(owner, 'openrouter.apikey'),
788
+ this.storage.getMemory(owner, 'openrouter.settings'),
789
+ ]);
790
+ const provider = prefsRec?.value?.provider || 'openrouter';
791
+ const hasKey = !!keyRec?.value?.encrypted;
792
+ return provider !== 'openrouter' || hasKey;
793
+ }
592
794
  /** Read the active context's workspaces (id + name) from its self-organism registry. */
593
795
  async loadContextWorkspaces(owner, active) {
594
796
  if (!active.organismId)
@@ -598,14 +800,36 @@ export class Scheduler {
598
800
  return list.filter((w) => w && w.id).map((w) => ({ id: w.id, name: w.name || w.id }));
599
801
  }
600
802
  /** Build the tick's system + user prompt asking for a STRUCTURED action list (P1-A). */
601
- buildTickPrompt(ownerName, active, openGoals, wsList) {
803
+ buildTickPrompt(ownerName, active, openGoals, wsList, lessons = []) {
602
804
  const rules = (active.brain?.rules || []).map((r) => '- ' + (r.description || '')).join('\n');
603
805
  const systemPrompt = `You are ${ownerName}'s personal Secretary, working in the "${active.name || 'personal'}" context.\n${active.brain?.purpose || ''}\n\nOperating rules:\n${rules || '(none)'}\n\nYou are doing an autonomous check-in for the owner (they are not present). Reply in the owner's language. Return ONLY a JSON object — no prose around it.`;
604
806
  const goalLines = openGoals.length
605
807
  ? openGoals.map((g) => `- ${String(g.title || '')}${g.why ? ` (why: ${String(g.why)})` : ''}`).join('\n')
606
808
  : '(no open goals)';
607
809
  const wsNames = wsList.map((w) => w.name).join(', ') || '(none yet)';
608
- const prompt = `Open goals in this context:\n${goalLines}\n\nThe owner's filing space "${active.organismName || active.name}" has these workspaces: ${wsNames}.\n\nDecide what to do for the owner right now. Return a JSON object EXACTLY like:\n{\n "briefing": "2-4 sentence check-in: what to focus on and anything needing attention",\n "actions": [\n { "capability": "file_intake", "summary": "short label", "payload": { "workspace": "<an existing workspace name>", "note": "the text to file" } },\n { "capability": "reminders", "summary": "a concrete reminder for the owner", "payload": {} }\n ]\n}\nRules for actions: propose 0-3 CONCRETE actions that move the open goals forward. Only use these capabilities: "file_intake"/"curate_knowledge" (file a note into a workspace — set payload.workspace to an existing workspace name and payload.note to the text) and "reminders"/"briefing" (surface a note to the owner — payload may be empty). Do not invent other capabilities. If nothing concrete is warranted, return an empty actions array.`;
810
+ // Optional strategy steering: if this context has a strategy, give the model the current → target,
811
+ // the principles/risks to respect, and the CURRENT FOCUS milestone (first not-yet-reached) so the
812
+ // briefing steers toward it and flags when it looks reached. Free text; never invents progress.
813
+ let strategyBlock = '';
814
+ const strat = active.strategy;
815
+ if (strat && strat.enabled) {
816
+ const msl = Array.isArray(strat.milestones) ? strat.milestones : [];
817
+ const focus = msl.find((m) => m && m.status !== 'reached');
818
+ const principles = (strat.principles || []).filter(Boolean);
819
+ const risks = (strat.risks || []).filter(Boolean);
820
+ strategyBlock = `\n\nStrategy for this area:\n- Current state: ${strat.current || '(unstated)'}\n- Target state: ${strat.target || '(unstated)'}\n- Current focus milestone: ${focus ? `${focus.title}${focus.enables ? ` (enables: ${focus.enables})` : ''}` : '(all milestones reached — suggest what the next target could be)'}`
821
+ + (principles.length ? `\n- Principles to respect: ${principles.join('; ')}` : '')
822
+ + (risks.length ? `\n- Risks to watch: ${risks.join('; ')}` : '')
823
+ + `\nSteer the briefing toward this focus milestone, respect the principles, and flag a risk if it is materialising. If recent activity suggests the milestone is reached, say so and point at the next one.`;
824
+ }
825
+ // Learning loop (A): the highest-signal lessons from past decisions the owner's reviews scored —
826
+ // repeat what worked, avoid what didn't. Free text; the model weighs them, nothing is auto-applied.
827
+ let lessonsBlock = '';
828
+ if (lessons.length) {
829
+ lessonsBlock = `\n\nLessons from your recently reviewed decisions (let these steer you — lean on what scored well, avoid repeating what scored poorly):\n`
830
+ + lessons.map((l) => `- [${l.score}/100] ${l.decision}${l.verdict ? ` — ${l.verdict}` : ''}`).join('\n');
831
+ }
832
+ const prompt = `Open goals in this context:\n${goalLines}\n\nThe owner's filing space "${active.organismName || active.name}" has these workspaces: ${wsNames}.${strategyBlock}${lessonsBlock}\n\nDecide what to do for the owner right now. Return a JSON object EXACTLY like:\n{\n "briefing": "2-4 sentence check-in: what to focus on and anything needing attention",\n "actions": [\n { "capability": "file_intake", "summary": "short label", "payload": { "workspace": "<an existing workspace name>", "note": "the text to file" } },\n { "capability": "reminders", "summary": "a concrete reminder for the owner", "payload": {} }\n ]\n}\nRules for actions: propose 0-3 CONCRETE actions that move the open goals forward. Only use these capabilities: "file_intake"/"curate_knowledge" (file a note into a workspace — set payload.workspace to an existing workspace name and payload.note to the text) and "reminders"/"briefing" (surface a note to the owner — payload may be empty). Do not invent other capabilities. If nothing concrete is warranted, return an empty actions array.`;
609
833
  return { systemPrompt, prompt };
610
834
  }
611
835
  /** Best-match a workspace by (possibly fuzzy) name; falls back to the first workspace. */
@@ -623,6 +847,37 @@ export class Scheduler {
623
847
  }
624
848
  return wsList[0];
625
849
  }
850
+ /**
851
+ * Resume answered clarify jobs (ask-don't-hallucinate): for each job whose inbox questions the owner
852
+ * answered, produce the deliverable grounded ONLY in the stored facts + those answers, file it into the
853
+ * job's workspace, reply the outcome back into the thread, and mark the job done. Unanswered jobs are
854
+ * left untouched. Produce logic is shared with the live DM responder (services/secretary-clarify.ts).
855
+ */
856
+ async resumeClarifyJobs(owner, ownerName, jobs) {
857
+ const writes = [];
858
+ if (!jobs.length)
859
+ return writes;
860
+ const secretaryGaii = `secretary#${ownerName}@${this.config.nodeId}`;
861
+ // Local agent→owner delivery never touches federation peers, so an empty map is sufficient here.
862
+ const ctx = { config: this.config, storage: this.storage, peers: new Map() };
863
+ for (const job of jobs) {
864
+ const conversationId = job.conversationId;
865
+ const questionId = job.questionId;
866
+ if (!conversationId || !questionId)
867
+ continue;
868
+ const thread = await this.storage.listAgentDmThread(secretaryGaii, conversationId, { perPage: 50 }).catch(() => null);
869
+ const msgs = thread?.messages || [];
870
+ const ans = msgs.find((m) => m.interactive?.role === 'answers' && m.interactive.answersFor === questionId);
871
+ if (!ans || ans.interactive?.role !== 'answers')
872
+ continue; // not answered yet
873
+ const { writes: w } = await produceClarifyDeliverable(ctx, {
874
+ ownerGhii: owner, ownerName, agentGaii: secretaryGaii,
875
+ job: job, answers: ans.interactive.answers,
876
+ });
877
+ writes.push(...w);
878
+ }
879
+ return writes;
880
+ }
626
881
  /**
627
882
  * Perform an `act`-band action: file a note (note kind) or append a feed entry (feed kind). `ctx` is
628
883
  * the destination context — normally the active context, but for a G1 auto-routed note it is the
@@ -644,7 +899,7 @@ export class Scheduler {
644
899
  const text = routedFrom
645
900
  ? `Routed a note to ${ctx.name || ctx.id} → ${ws.name}: ${a.summary}`
646
901
  : `Filed a note → ${ws.name}: ${a.summary}`;
647
- await this.appendFeed(owner, { kind: 'act', contextId: ctx.id, contextName: ctx.name || '', text });
902
+ await this.appendFeed(owner, { kind: 'act', contextId: ctx.id, contextName: ctx.name || '', text, href: `/v1/profile?tab=organisms&org=${ctx.organismId}&ws=${ws.id}` });
648
903
  return [key, 'secretary.feed'];
649
904
  }
650
905
  }
@@ -658,6 +913,69 @@ export class Scheduler {
658
913
  const map = rec?.value?.map;
659
914
  return (map && typeof map === 'object') ? map : {};
660
915
  }
916
+ /**
917
+ * Learning loop (B): when a CLUSTER of reviewed decisions in this context scored poorly, ask the model
918
+ * for ONE durable operating rule that would avoid repeating them, and post it as a GATED inbox decision
919
+ * card (type 'brain-rule'). The owner approves it in the inbox (applyDecision appends it to the brain) —
920
+ * it is NEVER auto-applied. The source decisions are flagged `proposalUsed` so the same cluster doesn't
921
+ * re-propose every tick. Returns the pending card (+ the keys written) or null when there isn't enough
922
+ * signal / a brain-rule proposal is already pending / the model returns nothing. One paid morsel on propose.
923
+ */
924
+ async proposeLearnedRule(owner, ownerName, active, existingPending) {
925
+ // One brain-rule proposal at a time — don't stack cards.
926
+ if (Object.values(existingPending).some((p) => p?.type === 'brain-rule'))
927
+ return null;
928
+ const recs = await this.storage.listMemory(owner, { prefix: 'secretary.decision.' });
929
+ const byId = new Map();
930
+ const values = [];
931
+ for (const r of recs) {
932
+ const d = (r.value ?? {});
933
+ if (d.id)
934
+ byId.set(String(d.id), r);
935
+ values.push(d);
936
+ }
937
+ const cluster = poorDecisionCluster(values, active.id);
938
+ if (!cluster.length)
939
+ return null;
940
+ const secretaryGaii = `secretary#${ownerName}@${this.config.nodeId}`;
941
+ const agent = await this.storage.getAgent(secretaryGaii);
942
+ if (!agent)
943
+ return null;
944
+ const systemPrompt = `You distil ONE durable operating rule for ${ownerName}'s Secretary from past decisions that turned out poorly. Reply in the owner's language. Return ONLY a JSON object: {"rule": "one concrete, actionable sentence the Secretary should follow from now on"}.`;
945
+ const prompt = `These past decisions scored poorly on review:\n${cluster.map((d, i) => `${i + 1}. ${String(d.decision || '')} — chosen: ${String(d.chosen || '')} — verdict: ${String(d.verdict || '')} (${d.score}/100)`).join('\n')}\n\nPropose ONE operating rule that, followed from now on, would avoid repeating these. Make it concrete and general enough to reuse — not a restatement of a single case.`;
946
+ let rule;
947
+ try {
948
+ const out = await completeForOwner(this.storage, this.config, owner, { prompt, systemPrompt, appId: 'schedule:learn:rule' });
949
+ rule = String(extractJsonObject(out.content)?.rule ?? '').trim().slice(0, 400);
950
+ }
951
+ catch {
952
+ return null;
953
+ }
954
+ if (!rule)
955
+ return null;
956
+ // Flag the source decisions so the cluster doesn't re-propose every tick.
957
+ const now = new Date().toISOString();
958
+ const writes = [];
959
+ for (const d of cluster) {
960
+ const rec = byId.get(String(d.id));
961
+ if (!rec)
962
+ continue;
963
+ await this.storage.setMemory({ key: rec.key, ownerGaii: owner, value: { ...d, proposalUsed: true }, visibility: 'private', tags: rec.tags ?? ['secretary', 'decision', 'reviewed', active.id], ttlHours: null, version: rec.version + 1, createdAt: rec.createdAt, updatedAt: now });
964
+ writes.push(rec.key);
965
+ }
966
+ const promptId = 'learn-' + randomUUID().slice(0, 8);
967
+ const question = `I noticed ${cluster.length} past decisions that went poorly. Add this rule to my brain so I avoid repeating them?\n\n"${rule}"`;
968
+ const pending = { type: 'brain-rule', rule, contextId: active.id, contextName: active.name || '', basisCount: cluster.length, question, createdAt: now };
969
+ const record = {
970
+ id: randomUUID(), agentGaii: secretaryGaii, threadId: randomUUID(), direction: 'outbound', senderGaii: owner,
971
+ content: `Proposed operating rule: ${rule}`, status: 'delivered',
972
+ metadata: { prompt: { promptId, question, options: ['Yes, add it', 'No'], allowOther: false } }, createdAt: now,
973
+ };
974
+ await this.storage.createMessage(record);
975
+ await this.appendFeed(owner, { kind: 'review', contextId: active.id, contextName: active.name || '', text: `💡 Proposed a learned rule from ${cluster.length} reviewed decisions — approve it in your inbox.` });
976
+ writes.push('secretary.feed');
977
+ return { id: promptId, pending, writes };
978
+ }
661
979
  /**
662
980
  * Post a draft/ask-band action as an inbox decision card (reusing the Phase-3b prompt rails) and
663
981
  * return the stashed pending action keyed by promptId. Note actions reuse the existing `file-note`
@@ -699,31 +1017,69 @@ export class Scheduler {
699
1017
  await this.storage.createMessage(record);
700
1018
  return { id: promptId, pending };
701
1019
  }
702
- /** Read-modify-write secretary.config to persist the day's spend ledger + any new pending decisions. */
703
- async persistTickState(owner, contextId, today, morsels, newPending) {
1020
+ /**
1021
+ * Read-modify-write secretary.config to persist the day's spend ledger + any new pending decisions,
1022
+ * plus (B5) routine step completions the tick performed and the action-items it derived. Applied to a
1023
+ * FRESH read (targeted by routine/step id) to avoid clobbering concurrent view edits.
1024
+ */
1025
+ async persistTickState(owner, contextId, today, morsels, newPending, stepCompletions = [], newActionItems = [], rearmedRoutines = []) {
704
1026
  const rec = await this.storage.getMemory(owner, 'secretary.config');
705
1027
  const cfg = (rec?.value ?? {});
706
1028
  const ledger = morsels > 0 ? bumpLedger(cfg.autonomousLedger, contextId, today, morsels) : cfg.autonomousLedger;
707
1029
  const pending = Object.keys(newPending).length ? { ...(cfg.pendingDecisions ?? {}), ...newPending } : cfg.pendingDecisions;
708
- const next = { ...cfg, autonomousLedger: ledger, pendingDecisions: pending };
709
1030
  const now = new Date().toISOString();
1031
+ // B5/G2: apply routine re-arms + step completions + merge action-items; G8: scrub quick actions.
1032
+ let contexts = cfg.contexts;
1033
+ if (Array.isArray(cfg.contexts)) {
1034
+ contexts = cfg.contexts.map((c) => {
1035
+ if (c.id !== contextId)
1036
+ return c;
1037
+ // G8 defense-in-depth: drop any disallowed (e.g. smuggled 'run') quick action server-side.
1038
+ const quickActions = Array.isArray(c.quickActions) ? c.quickActions.filter(isAllowedQuickAction) : c.quickActions;
1039
+ let routines = c.routines;
1040
+ // G2: replace re-armed routines first (steps reset to pending) so a same-tick file-step
1041
+ // completion below lands on the freshly-armed step.
1042
+ if (rearmedRoutines.length && Array.isArray(routines)) {
1043
+ routines = routines.map((r) => {
1044
+ const armed = rearmedRoutines.find((x) => x.id === r.id);
1045
+ return armed ? armed.routine : r;
1046
+ });
1047
+ }
1048
+ if (stepCompletions.length && Array.isArray(routines)) {
1049
+ routines = routines.map((r) => {
1050
+ const comps = stepCompletions.filter((sc) => sc.routineId === r.id);
1051
+ if (!comps.length)
1052
+ return r;
1053
+ const steps = (r.steps || []).map((s) => {
1054
+ const comp = comps.find((x) => x.stepId === s.id);
1055
+ return comp ? { ...s, status: comp.status, result: { summary: comp.summary, ts: now } } : s;
1056
+ });
1057
+ // B4 fix: a 'delegated' step is NOT settled (its task is still out) — the routine stays active.
1058
+ const settled = steps.every((s) => s.status !== 'pending' && s.status !== 'running' && s.status !== 'delegated');
1059
+ const results = [...comps.map((x) => ({ ts: now, summary: x.summary })), ...(r.results || [])].slice(0, 20);
1060
+ return { ...r, steps, results, lastRunAt: now, status: settled ? 'done' : r.status };
1061
+ });
1062
+ }
1063
+ let actionItems = c.actionItems;
1064
+ if (newActionItems.length) {
1065
+ const existing = Array.isArray(c.actionItems) ? c.actionItems : [];
1066
+ const seenOpen = new Set(existing.filter((a) => a.status === 'open').map((a) => actionItemKey(a)));
1067
+ const fresh = newActionItems.filter((a) => !seenOpen.has(actionItemKey(a)));
1068
+ actionItems = [...fresh, ...existing].slice(0, 50);
1069
+ }
1070
+ return { ...c, routines, actionItems, quickActions };
1071
+ });
1072
+ }
1073
+ const next = { ...cfg, contexts, autonomousLedger: ledger, pendingDecisions: pending };
710
1074
  await this.storage.setMemory({
711
1075
  key: 'secretary.config', ownerGaii: owner, value: next, visibility: 'private', tags: ['secretary', 'config'],
712
1076
  ttlHours: null, version: rec ? rec.version + 1 : 1, createdAt: rec?.createdAt ?? now, updatedAt: now,
713
1077
  });
714
1078
  }
715
- /** Append one entry to the owner's Home feed (`secretary.feed`, newest first, capped at 50). */
1079
+ /** Append one entry to the owner's Home feed (`secretary.feed`, newest first, capped). Delegates to the
1080
+ * shared helper so the live DM responder / clarify-produce write the SAME feed. */
716
1081
  async appendFeed(owner, entry) {
717
- const feedKey = 'secretary.feed';
718
- const existing = await this.storage.getMemory(owner, feedKey);
719
- const list = Array.isArray(existing?.value?.items)
720
- ? existing.value.items : [];
721
- const now = new Date().toISOString();
722
- const items = [{ id: randomUUID(), ts: now, ...entry }, ...list].slice(0, 50);
723
- await this.storage.setMemory({
724
- key: feedKey, ownerGaii: owner, value: { items }, visibility: 'private', tags: ['secretary', 'feed'],
725
- ttlHours: null, version: existing ? existing.version + 1 : 1, createdAt: existing?.createdAt ?? now, updatedAt: now,
726
- });
1082
+ await appendSecretaryFeed(this.storage, owner, entry);
727
1083
  }
728
1084
  /**
729
1085
  * Learning-loop review sweep (Phase 5): find open decision-log contracts whose `revisitWhen` has
@@ -766,6 +1122,53 @@ export class Scheduler {
766
1122
  }
767
1123
  return done;
768
1124
  }
1125
+ /**
1126
+ * Carry out a fired trigger's BOUND action (if any). Returns null for a plain reminder (the caller then
1127
+ * posts a follow-up action-item); otherwise `{ note, morsels }` describing what ran (the caller folds the
1128
+ * note into the feed entry and meters the morsels). Two bindings:
1129
+ * - workflow: start the named Agent Workflow run via the deterministic engine.
1130
+ * - specialist: queue a task (the trigger's `prompt`, defaulting to its label) for the named specialist
1131
+ * and run it node-side now (owner key + quality contract), so the deliverable lands this same tick.
1132
+ * Best-effort: any failure (unknown workflow/specialist, engine down, spend-pause) degrades to a plain
1133
+ * reminder so a misconfigured binding never breaks the tick.
1134
+ */
1135
+ async fireBoundTriggerAction(owner, ownerName, tr, label, specialists, writes) {
1136
+ const act = tr.action || {};
1137
+ try {
1138
+ if (act.kind === 'workflow' && act.workflowId) {
1139
+ const engine = getActiveWorkflowEngine();
1140
+ if (!engine)
1141
+ return null;
1142
+ const r = await engine.startRun(owner, ownerName, String(act.workflowId), { mode: 'full-live' });
1143
+ if ('error' in r) {
1144
+ logger.info('[secretary tick] bound workflow refused to start', { id: tr.id, wf: act.workflowId, err: r.error.join('; ') });
1145
+ return null;
1146
+ }
1147
+ writes.push(`workflows.run.${act.workflowId}.${r.runId}`);
1148
+ return { note: `▶️ ${act.workflowId}`, morsels: 1 };
1149
+ }
1150
+ if (act.kind === 'specialist' && act.specialistName) {
1151
+ const spec = specialists.find((s) => s.name === act.specialistName);
1152
+ if (!spec)
1153
+ return null;
1154
+ const now = new Date().toISOString();
1155
+ await this.storage.createAgentTask({
1156
+ id: randomUUID(), agentGaii: spec.gaii, ownerGaii: owner,
1157
+ title: label, description: String(act.prompt || label).slice(0, 4000),
1158
+ scope: [], rules: [], verification: { userExpects: '', technicalChecks: [] }, todos: [],
1159
+ status: 'queued', parentTaskId: `trigger:${tr.id}`, createdAt: now, updatedAt: now,
1160
+ });
1161
+ const specWrites = await runSpecialistQueue(this.storage, this.config, owner, spec, { limit: 1 });
1162
+ writes.push(...specWrites);
1163
+ // 1 morsel for the specialist run even if it deferred (kept the queued task) — the run was attempted.
1164
+ return { note: `🧩 ${spec.name}`, morsels: Math.max(1, specWrites.length) };
1165
+ }
1166
+ }
1167
+ catch (e) {
1168
+ logger.info('[secretary tick] bound trigger action failed', { id: tr.id, kind: act.kind, err: e.message });
1169
+ }
1170
+ return null;
1171
+ }
769
1172
  /**
770
1173
  * `agent_task` kind: materialise an AgentTaskRecord into the target agent's
771
1174
  * queue and wake it via the existing webhook/MCP/SSE fan-out. The schedule is