@terreno/mcp 57.3.0 → 57.4.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 (250) hide show
  1. package/README.md +7 -1
  2. package/dist/bootstrap.d.ts +0 -18
  3. package/dist/bootstrap.d.ts.map +1 -1
  4. package/dist/bootstrap.js +10 -2428
  5. package/dist/bootstrap.js.map +1 -1
  6. package/dist/docs/guidelines/admin-frontend/guidelines/core.md +5 -1
  7. package/dist/docs/guidelines/api/skills/mongoose-schema-safety/SKILL.md +23 -10
  8. package/dist/docs/resources/syncdb.md +4 -1
  9. package/dist/docs/upgrades/57.4.0.md +140 -0
  10. package/dist/docs/versioned/next/README.md +3 -0
  11. package/dist/docs/versioned/next/explanation/README.md +4 -1
  12. package/dist/docs/versioned/next/explanation/admin-interface.md +20 -13
  13. package/dist/docs/versioned/next/explanation/charts.md +29 -0
  14. package/dist/docs/versioned/next/explanation/deployment-architecture-gcp.md +32 -0
  15. package/dist/docs/versioned/next/explanation/deployment-baseline.md +1 -1
  16. package/dist/docs/versioned/next/explanation/example-coverage.md +43 -0
  17. package/dist/docs/versioned/next/explanation/local-first-data.md +5 -2
  18. package/dist/docs/versioned/next/explanation/loop-engineering.md +9 -6
  19. package/dist/docs/versioned/next/explanation/model-router-actions.md +19 -2
  20. package/dist/docs/versioned/next/explanation/organizations.md +88 -0
  21. package/dist/docs/versioned/next/explanation/planning-plugins.md +114 -0
  22. package/dist/docs/versioned/next/explanation/positioning.md +3 -3
  23. package/dist/docs/versioned/next/explanation/repository-settings.md +6 -5
  24. package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +44 -34
  25. package/dist/docs/versioned/next/explanation/source-rules.md +1 -1
  26. package/dist/docs/versioned/next/explanation/static-analysis.md +76 -16
  27. package/dist/docs/versioned/next/how-to/README.md +10 -0
  28. package/dist/docs/versioned/next/how-to/add-gpt-chat-mascot.md +92 -0
  29. package/dist/docs/versioned/next/how-to/add-organizations.md +204 -0
  30. package/dist/docs/versioned/next/how-to/admin-add-model.md +9 -1
  31. package/dist/docs/versioned/next/how-to/admin-import-prebuilt.md +7 -1
  32. package/dist/docs/versioned/next/how-to/audit-log.md +85 -0
  33. package/dist/docs/versioned/next/how-to/background-jobs.md +296 -0
  34. package/dist/docs/versioned/next/how-to/build-admin-screens.md +42 -13
  35. package/dist/docs/versioned/next/how-to/charts-and-dashboards.md +37 -0
  36. package/dist/docs/versioned/next/how-to/circleci.md +136 -27
  37. package/dist/docs/versioned/next/how-to/compare-chart-rendered-snapshots.md +36 -0
  38. package/dist/docs/versioned/next/how-to/configure-better-auth.md +4 -0
  39. package/dist/docs/versioned/next/how-to/create-a-terreno-app.md +154 -0
  40. package/dist/docs/versioned/next/how-to/deploy-backend-to-cloud-run.md +2 -0
  41. package/dist/docs/versioned/next/how-to/github-actions-ci.md +4 -3
  42. package/dist/docs/versioned/next/how-to/github-issue-lifecycle.md +12 -8
  43. package/dist/docs/versioned/next/how-to/in-app-notifications.md +147 -0
  44. package/dist/docs/versioned/next/how-to/install-agent-skills.md +24 -12
  45. package/dist/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +19 -3
  46. package/dist/docs/versioned/next/how-to/product-announcements.md +155 -0
  47. package/dist/docs/versioned/next/how-to/run-mongodb-migrations.md +127 -0
  48. package/dist/docs/versioned/next/how-to/run-tests-locally.md +102 -0
  49. package/dist/docs/versioned/next/how-to/seed-a-database.md +7 -2
  50. package/dist/docs/versioned/next/implementationPlans/README.md +8 -5
  51. package/dist/docs/versioned/next/implementationPlans/admin-scripts-durable-jobs.md +44 -0
  52. package/dist/docs/versioned/next/implementationPlans/agent-ui-blocks.md +386 -0
  53. package/dist/docs/versioned/next/implementationPlans/agentic-sdlc-plugin.md +11 -8
  54. package/dist/docs/versioned/next/implementationPlans/announcements-surfaces.md +231 -0
  55. package/dist/docs/versioned/next/implementationPlans/announcements.md +289 -0
  56. package/dist/docs/versioned/next/implementationPlans/b2b-platform-program.md +3 -1
  57. package/dist/docs/versioned/next/implementationPlans/charts-and-dashboards.md +246 -0
  58. package/dist/docs/versioned/next/implementationPlans/comms-admin-dashboard.md +4 -3
  59. package/dist/docs/versioned/next/implementationPlans/create-terreno-app.md +177 -0
  60. package/dist/docs/versioned/next/implementationPlans/datatable-server-side-filtering.md +219 -0
  61. package/dist/docs/versioned/next/implementationPlans/examples-demo-coverage.md +17 -17
  62. package/dist/docs/versioned/next/implementationPlans/framework-audit-log.md +247 -0
  63. package/dist/docs/versioned/next/implementationPlans/gpt-chat-mascot.md +40 -0
  64. package/dist/docs/versioned/next/implementationPlans/job-queues.md +330 -0
  65. package/dist/docs/versioned/next/implementationPlans/knip-cleanup.md +136 -0
  66. package/dist/docs/versioned/next/implementationPlans/migrate-admin-to-syncdb.md +210 -0
  67. package/dist/docs/versioned/next/implementationPlans/mongodb-migrations-tooling.md +254 -0
  68. package/dist/docs/versioned/next/implementationPlans/notification-center.md +185 -0
  69. package/dist/docs/versioned/next/implementationPlans/org-management-ui.md +8 -3
  70. package/dist/docs/versioned/next/implementationPlans/positioning-django-rails-universal.md +1 -1
  71. package/dist/docs/versioned/next/reference/README.md +4 -0
  72. package/dist/docs/versioned/next/reference/admin-backend.md +83 -5
  73. package/dist/docs/versioned/next/reference/admin-config.md +7 -0
  74. package/dist/docs/versioned/next/reference/admin-frontend.md +178 -5
  75. package/dist/docs/versioned/next/reference/admin-spa.md +18 -1
  76. package/dist/docs/versioned/next/reference/ai.md +2 -0
  77. package/dist/docs/versioned/next/reference/announcements.md +179 -0
  78. package/dist/docs/versioned/next/reference/api.md +343 -4
  79. package/dist/docs/versioned/next/reference/comms.md +33 -0
  80. package/dist/docs/versioned/next/reference/create-terreno-app.md +44 -0
  81. package/dist/docs/versioned/next/reference/environment-variables.md +29 -0
  82. package/dist/docs/versioned/next/reference/jobs.md +319 -0
  83. package/dist/docs/versioned/next/reference/legacy/rtk.md +5 -1
  84. package/dist/docs/versioned/next/reference/lifecycle-plugin.md +29 -11
  85. package/dist/docs/versioned/next/reference/mcp-server.md +2 -43
  86. package/dist/docs/versioned/next/reference/scan-plugin.md +169 -0
  87. package/dist/docs/versioned/next/reference/syncdb.md +38 -12
  88. package/dist/docs/versioned/next/reference/test.md +57 -6
  89. package/dist/docs/versioned/next/reference/ui-performance.md +2 -1
  90. package/dist/docs/versioned/next/reference/ui.md +257 -3
  91. package/dist/docs/versioned/next/tasks/admin-scripts-durable-jobs.md +10 -0
  92. package/dist/docs/versioned/next/tasks/agent-ui-blocks.md +114 -0
  93. package/dist/docs/versioned/next/tasks/announcements-surfaces.md +101 -0
  94. package/dist/docs/versioned/next/tasks/announcements.md +173 -0
  95. package/dist/docs/versioned/next/tasks/charts-and-dashboards.md +120 -0
  96. package/dist/docs/versioned/next/tasks/create-terreno-app.md +82 -0
  97. package/dist/docs/versioned/next/tasks/datatable-server-side-filtering.md +87 -0
  98. package/dist/docs/versioned/next/tasks/examples-demo-coverage.md +123 -17
  99. package/dist/docs/versioned/next/tasks/framework-audit-log.md +86 -0
  100. package/dist/docs/versioned/next/tasks/gpt-chat-mascot.md +13 -0
  101. package/dist/docs/versioned/next/tasks/job-queues.md +144 -0
  102. package/dist/docs/versioned/next/tasks/knip-cleanup.md +146 -0
  103. package/dist/docs/versioned/next/tasks/migrate-admin-to-syncdb.md +209 -0
  104. package/dist/docs/versioned/next/tasks/mongodb-migrations-tooling.md +107 -0
  105. package/dist/docs/versioned/next/tasks/notification-center.md +102 -0
  106. package/dist/docs/versioned/next/tasks/org-management-ui.md +14 -14
  107. package/dist/docs/versioned/next/tutorials/README.md +1 -1
  108. package/dist/docs/versioned/next/tutorials/getting-started.md +62 -22
  109. package/dist/help/excerpt.d.ts +2 -0
  110. package/dist/help/excerpt.d.ts.map +1 -0
  111. package/dist/help/excerpt.js +8 -0
  112. package/dist/help/excerpt.js.map +1 -0
  113. package/dist/help/updateNotes.d.ts +42 -0
  114. package/dist/help/updateNotes.d.ts.map +1 -0
  115. package/dist/help/updateNotes.js +235 -0
  116. package/dist/help/updateNotes.js.map +1 -0
  117. package/dist/index.js +1 -1
  118. package/dist/index.js.map +1 -1
  119. package/dist/local/tools/databaseQuery.d.ts +1 -0
  120. package/dist/local/tools/databaseQuery.d.ts.map +1 -1
  121. package/dist/local/tools/databaseQuery.js +1 -0
  122. package/dist/local/tools/databaseQuery.js.map +1 -1
  123. package/dist/packageGuidelines.d.ts +2 -2
  124. package/dist/packageGuidelines.d.ts.map +1 -1
  125. package/dist/packageGuidelines.js +2 -8
  126. package/dist/packageGuidelines.js.map +1 -1
  127. package/dist/resources.d.ts +3 -0
  128. package/dist/resources.d.ts.map +1 -1
  129. package/dist/resources.js +3 -0
  130. package/dist/resources.js.map +1 -1
  131. package/dist/search/chunker.d.ts +1 -0
  132. package/dist/search/chunker.d.ts.map +1 -1
  133. package/dist/search/chunker.js +1 -0
  134. package/dist/search/chunker.js.map +1 -1
  135. package/dist/search/docIndex.d.ts +1 -5
  136. package/dist/search/docIndex.d.ts.map +1 -1
  137. package/dist/search/docIndex.js +1 -0
  138. package/dist/search/docIndex.js.map +1 -1
  139. package/dist/search/docVersion.d.ts +2 -1
  140. package/dist/search/docVersion.d.ts.map +1 -1
  141. package/dist/search/docVersion.js +3 -1
  142. package/dist/search/docVersion.js.map +1 -1
  143. package/dist/tools.d.ts +2 -2
  144. package/dist/tools.d.ts.map +1 -1
  145. package/dist/tools.js +113 -2
  146. package/dist/tools.js.map +1 -1
  147. package/package.json +28 -27
  148. package/src/docs/guidelines/admin-frontend/guidelines/core.md +5 -1
  149. package/src/docs/guidelines/api/skills/mongoose-schema-safety/SKILL.md +23 -10
  150. package/src/docs/resources/syncdb.md +4 -1
  151. package/src/docs/upgrades/57.4.0.md +140 -0
  152. package/src/docs/versioned/next/README.md +3 -0
  153. package/src/docs/versioned/next/explanation/README.md +4 -1
  154. package/src/docs/versioned/next/explanation/admin-interface.md +20 -13
  155. package/src/docs/versioned/next/explanation/charts.md +29 -0
  156. package/src/docs/versioned/next/explanation/deployment-architecture-gcp.md +32 -0
  157. package/src/docs/versioned/next/explanation/deployment-baseline.md +1 -1
  158. package/src/docs/versioned/next/explanation/example-coverage.md +43 -0
  159. package/src/docs/versioned/next/explanation/local-first-data.md +5 -2
  160. package/src/docs/versioned/next/explanation/loop-engineering.md +9 -6
  161. package/src/docs/versioned/next/explanation/model-router-actions.md +19 -2
  162. package/src/docs/versioned/next/explanation/organizations.md +88 -0
  163. package/src/docs/versioned/next/explanation/planning-plugins.md +114 -0
  164. package/src/docs/versioned/next/explanation/positioning.md +3 -3
  165. package/src/docs/versioned/next/explanation/repository-settings.md +6 -5
  166. package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +44 -34
  167. package/src/docs/versioned/next/explanation/source-rules.md +1 -1
  168. package/src/docs/versioned/next/explanation/static-analysis.md +76 -16
  169. package/src/docs/versioned/next/how-to/README.md +10 -0
  170. package/src/docs/versioned/next/how-to/add-gpt-chat-mascot.md +92 -0
  171. package/src/docs/versioned/next/how-to/add-organizations.md +204 -0
  172. package/src/docs/versioned/next/how-to/admin-add-model.md +9 -1
  173. package/src/docs/versioned/next/how-to/admin-import-prebuilt.md +7 -1
  174. package/src/docs/versioned/next/how-to/audit-log.md +85 -0
  175. package/src/docs/versioned/next/how-to/background-jobs.md +296 -0
  176. package/src/docs/versioned/next/how-to/build-admin-screens.md +42 -13
  177. package/src/docs/versioned/next/how-to/charts-and-dashboards.md +37 -0
  178. package/src/docs/versioned/next/how-to/circleci.md +136 -27
  179. package/src/docs/versioned/next/how-to/compare-chart-rendered-snapshots.md +36 -0
  180. package/src/docs/versioned/next/how-to/configure-better-auth.md +4 -0
  181. package/src/docs/versioned/next/how-to/create-a-terreno-app.md +154 -0
  182. package/src/docs/versioned/next/how-to/deploy-backend-to-cloud-run.md +2 -0
  183. package/src/docs/versioned/next/how-to/github-actions-ci.md +4 -3
  184. package/src/docs/versioned/next/how-to/github-issue-lifecycle.md +12 -8
  185. package/src/docs/versioned/next/how-to/in-app-notifications.md +147 -0
  186. package/src/docs/versioned/next/how-to/install-agent-skills.md +24 -12
  187. package/src/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +19 -3
  188. package/src/docs/versioned/next/how-to/product-announcements.md +155 -0
  189. package/src/docs/versioned/next/how-to/run-mongodb-migrations.md +127 -0
  190. package/src/docs/versioned/next/how-to/run-tests-locally.md +102 -0
  191. package/src/docs/versioned/next/how-to/seed-a-database.md +7 -2
  192. package/src/docs/versioned/next/implementationPlans/README.md +8 -5
  193. package/src/docs/versioned/next/implementationPlans/admin-scripts-durable-jobs.md +44 -0
  194. package/src/docs/versioned/next/implementationPlans/agent-ui-blocks.md +386 -0
  195. package/src/docs/versioned/next/implementationPlans/agentic-sdlc-plugin.md +11 -8
  196. package/src/docs/versioned/next/implementationPlans/announcements-surfaces.md +231 -0
  197. package/src/docs/versioned/next/implementationPlans/announcements.md +289 -0
  198. package/src/docs/versioned/next/implementationPlans/b2b-platform-program.md +3 -1
  199. package/src/docs/versioned/next/implementationPlans/charts-and-dashboards.md +246 -0
  200. package/src/docs/versioned/next/implementationPlans/comms-admin-dashboard.md +4 -3
  201. package/src/docs/versioned/next/implementationPlans/create-terreno-app.md +177 -0
  202. package/src/docs/versioned/next/implementationPlans/datatable-server-side-filtering.md +219 -0
  203. package/src/docs/versioned/next/implementationPlans/examples-demo-coverage.md +17 -17
  204. package/src/docs/versioned/next/implementationPlans/framework-audit-log.md +247 -0
  205. package/src/docs/versioned/next/implementationPlans/gpt-chat-mascot.md +40 -0
  206. package/src/docs/versioned/next/implementationPlans/job-queues.md +330 -0
  207. package/src/docs/versioned/next/implementationPlans/knip-cleanup.md +136 -0
  208. package/src/docs/versioned/next/implementationPlans/migrate-admin-to-syncdb.md +210 -0
  209. package/src/docs/versioned/next/implementationPlans/mongodb-migrations-tooling.md +254 -0
  210. package/src/docs/versioned/next/implementationPlans/notification-center.md +185 -0
  211. package/src/docs/versioned/next/implementationPlans/org-management-ui.md +8 -3
  212. package/src/docs/versioned/next/implementationPlans/positioning-django-rails-universal.md +1 -1
  213. package/src/docs/versioned/next/reference/README.md +4 -0
  214. package/src/docs/versioned/next/reference/admin-backend.md +83 -5
  215. package/src/docs/versioned/next/reference/admin-config.md +7 -0
  216. package/src/docs/versioned/next/reference/admin-frontend.md +178 -5
  217. package/src/docs/versioned/next/reference/admin-spa.md +18 -1
  218. package/src/docs/versioned/next/reference/ai.md +2 -0
  219. package/src/docs/versioned/next/reference/announcements.md +179 -0
  220. package/src/docs/versioned/next/reference/api.md +343 -4
  221. package/src/docs/versioned/next/reference/comms.md +33 -0
  222. package/src/docs/versioned/next/reference/create-terreno-app.md +44 -0
  223. package/src/docs/versioned/next/reference/environment-variables.md +29 -0
  224. package/src/docs/versioned/next/reference/jobs.md +319 -0
  225. package/src/docs/versioned/next/reference/legacy/rtk.md +5 -1
  226. package/src/docs/versioned/next/reference/lifecycle-plugin.md +29 -11
  227. package/src/docs/versioned/next/reference/mcp-server.md +2 -43
  228. package/src/docs/versioned/next/reference/scan-plugin.md +169 -0
  229. package/src/docs/versioned/next/reference/syncdb.md +38 -12
  230. package/src/docs/versioned/next/reference/test.md +57 -6
  231. package/src/docs/versioned/next/reference/ui-performance.md +2 -1
  232. package/src/docs/versioned/next/reference/ui.md +257 -3
  233. package/src/docs/versioned/next/tasks/admin-scripts-durable-jobs.md +10 -0
  234. package/src/docs/versioned/next/tasks/agent-ui-blocks.md +114 -0
  235. package/src/docs/versioned/next/tasks/announcements-surfaces.md +101 -0
  236. package/src/docs/versioned/next/tasks/announcements.md +173 -0
  237. package/src/docs/versioned/next/tasks/charts-and-dashboards.md +120 -0
  238. package/src/docs/versioned/next/tasks/create-terreno-app.md +82 -0
  239. package/src/docs/versioned/next/tasks/datatable-server-side-filtering.md +87 -0
  240. package/src/docs/versioned/next/tasks/examples-demo-coverage.md +123 -17
  241. package/src/docs/versioned/next/tasks/framework-audit-log.md +86 -0
  242. package/src/docs/versioned/next/tasks/gpt-chat-mascot.md +13 -0
  243. package/src/docs/versioned/next/tasks/job-queues.md +144 -0
  244. package/src/docs/versioned/next/tasks/knip-cleanup.md +146 -0
  245. package/src/docs/versioned/next/tasks/migrate-admin-to-syncdb.md +209 -0
  246. package/src/docs/versioned/next/tasks/mongodb-migrations-tooling.md +107 -0
  247. package/src/docs/versioned/next/tasks/notification-center.md +102 -0
  248. package/src/docs/versioned/next/tasks/org-management-ui.md +14 -14
  249. package/src/docs/versioned/next/tutorials/README.md +1 -1
  250. package/src/docs/versioned/next/tutorials/getting-started.md +62 -22
@@ -71,6 +71,10 @@ of inserting a duplicate. If more than one document already shares the key,
71
71
  the survivor. Use `context.deleteMany()` only inside reset handlers.
72
72
  Custom writes must check `context.dryRun` themselves.
73
73
 
74
+ The example backend seeds active, read, and archived notification rows for the default
75
+ test user. This keeps the notification drawer and full history page populated for local
76
+ development and browser tests.
77
+
74
78
  For Better Auth credentials, call `seedBetterAuthUser({auth, user, userModel})`.
75
79
  It creates a missing credential account, signs in when the account already
76
80
  exists, and reconciles the application user document.
@@ -84,5 +88,6 @@ are true:
84
88
  2. `allowProductionReset` returns `true` (normally from a dedicated environment variable).
85
89
 
86
90
  Reset handlers should delete only records owned by the seed plan. Preserve
87
- authentication, migration history, and operator-created data unless the plan
88
- explicitly manages them.
91
+ authentication, migration history (`terreno_migrations`), and operator-created
92
+ data unless the plan explicitly manages them. Do not `dropDatabase` or delete
93
+ the history collection in seed `--reset`.
@@ -40,16 +40,19 @@ The [`terreno-planning` plugin](../../plugins/README.md) drives the pipeline.
40
40
 
41
41
  **Grow** interviews in grilling rounds (one frontier of decisions per message, recommended
42
42
  answers, then wait). It stays on a question until the answer is executable. After the user
43
- confirms shared understanding it writes the two files and ends with a **15-line verify
44
- index** (destination, in/out, tracer, task graph, test seam). When grilling produced
45
- human decisions, it lists **every** one in a Decisions table with no row cap. If there
46
- were none, that table is omitted.
43
+ confirms shared understanding it writes the two files and ends with a **standalone
44
+ approval brief**: an orientation paragraph on where the repository is and where the change
45
+ takes it, optional background on current state, the idea, then the plan (tasks, tracer,
46
+ verification, risks). When grilling produced human decisions, a Decisions table with no
47
+ row cap follows the plan, pairing each settled choice with the question that prompted it.
48
+ If there were none, that table is omitted. The brief must stand on its own — a reviewer
49
+ should not need the IP, the ticket, or the interview history to approve it.
47
50
 
48
51
  For a small feature, the plugin's **feature profile** uses a compact approved task
49
52
  contract: invoke Pick once; it implements one frontier task, roasts it, then picks the
50
53
  next until the list is done, then Brew and bounded Taste iterations.
51
54
 
52
- 1. **Grow** (`terreno-1-grow`) — grill until answers are executable, then write the IP + tracer-bullet task list; end with the 15-line verify index and a full Decisions table when any exist.
55
+ 1. **Grow** (`terreno-1-grow`) — grill until answers are executable, then write the IP + tracer-bullet task list; end with the standalone approval brief (summary, background, idea, plan) and a full Decisions table when any exist.
53
56
  2. **Pick** (`terreno-2-pick`) — implement one approved slice via TDD, then continue the pick-roast inner loop.
54
57
  3. **Roast** (`terreno-3-roast`) — independently verify the current task, then return (`next: pick` or `next: brew`).
55
58
  4. **Brew** (`terreno-4-brew`) — commit, push, open the PR after every in-scope task has Roast `PASS`.
@@ -0,0 +1,44 @@
1
+ # Implementation Plan: Admin scripts via durable jobs
2
+
3
+ **Status:** Approved (operator) — 2026-09-14
4
+ **Branch:** `cursor/scripts-via-durable-jobs-803d`
5
+ **Task list:** [admin-scripts-durable-jobs.md](../tasks/admin-scripts-durable-jobs.md)
6
+ **Depends on:** [job-queues.md](job-queues.md)
7
+
8
+ ## Goal
9
+
10
+ Admin HTTP script runs (`POST /admin/scripts/:name/run`) enqueue `@terreno/jobs` job
11
+ `admin/script` when `JobsApp` is registered, while the Scripts UI keeps polling
12
+ `BackgroundTask` by `taskId`.
13
+
14
+ ## Non-Goals
15
+
16
+ - Deleting `BackgroundTask` or merging Scripts and Jobs screens
17
+ - Routing `runScriptCli` through the queue
18
+ - Executing `POST /admin/background-tasks` kinds
19
+ - Moving comms/webhooks/billing onto jobs
20
+ - Requiring scripts to be idempotent (use `maxAttempts: 1`)
21
+
22
+ ## Decisions
23
+
24
+ | Question | Decision |
25
+ |----------|----------|
26
+ | Job name | `admin/script` |
27
+ | Payload | `{scriptName, taskId, wetRun, args, createdByName?}` |
28
+ | Retry | `maxAttempts: 1` |
29
+ | UI | Unchanged poll/cancel on `BackgroundTask` |
30
+ | Fallback | No `JobsApp` → existing in-process runner |
31
+ | Worker | Same script catalog via `defineAdminScriptJob` |
32
+ | Cloud Tasks | Inherited from `JobsApp` runner; execute process must load scripts |
33
+ | GCP topology | Infra Manager queue pushes OIDC callbacks to a private Cloud Run tasks service |
34
+ | Execution pool | Cloud Tasks rate limits + Cloud Run service instances; not Cloud Run worker pools (no HTTP ingress) |
35
+ | PR isolation | Shared queue, per-PR tasks-service tag callback, and per-PR Mongo database |
36
+
37
+ ## Acceptance
38
+
39
+ - With `JobsApp` + worker, run creates a `Job` and completes the `BackgroundTask`
40
+ - Without `JobsApp`, existing admin-backend script tests still pass
41
+ - Cancel marks `BackgroundTask` cancelled and cancels the job when present
42
+ - Example worker defines `admin/script`
43
+ - Deployed example backend selects `GcpCloudTasksRunner` and rejects unverified callbacks
44
+ - Concurrent PR previews target only their matching tasks-service tag and database
@@ -0,0 +1,386 @@
1
+ # Agent UI Blocks — a strict YAML DSL for agent-rendered Terreno components
2
+
3
+ **Status:** Approved 2026-09-15 (grilling rounds 1–2 closed: D1, D2, D4, D6, D8, D15, D17, D18 confirmed; remaining `assumed` rows are engineering defaults Pick may revise with a note)
4
+ **Branch:** `cursor/agent-ui-blocks-grow-e5c0`
5
+ **Owner:** unassigned
6
+ **Created:** 2026-09-15
7
+ **Task list:** [`docs/tasks/agent-ui-blocks.md`](../tasks/agent-ui-blocks.md)
8
+ **Linear:** none
9
+ **Roadmap issue:** none yet (handoff after Approved)
10
+ **Related:** [`charts-and-dashboards.md`](./charts-and-dashboards.md) (PR #1302 — chart components this DSL targets), [`ai-observability.md`](./ai-observability.md), [`app-mcp-server.md`](./app-mcp-server.md)
11
+ **Primary packages:** new `@terreno/blocks` (contract), `@terreno/ui` (renderer), `@terreno/ai` (prompting + validation), `@terreno/mcp` (validator tool), `demo`, `example-frontend`, `example-backend`
12
+
13
+ ## Goal
14
+
15
+ Let an agent answer with **components, not just prose**. When UI blocks are enabled for a
16
+ chat, **every assistant reply is one YAML document**: a short, ordered list of **blocks**
17
+ (heading, text, metric, chart, table, actions, columns, card, …) plus named **datasets**
18
+ that charts and tables bind to. Prose is a `text` block. Terreno owns the grammar, the
19
+ validator, and the renderer, so the agent can only ever produce `@terreno/ui` components
20
+ painted from the app theme — never HTML, JSX, or hex colors.
21
+
22
+ The grammar is deliberately **strict and small** so an agent can check its own work in
23
+ milliseconds: unknown keys, missing dataset columns, a non-numeric `y`, more than 50
24
+ blocks, or a donut with 20 slices all fail with a path, a code, and a one-line fix. The
25
+ same pure validator runs on the backend after the stream ends, in the MCP server, in the
26
+ demo playground, and in the renderer.
27
+
28
+ Charting is the headline use case: "show me signups by month" becomes a `chart` block
29
+ bound to a dataset — inline when the agent already has a few hundred numbers, or a `ref`
30
+ to a server-side handle a tool returned when there are thousands — rendered by the
31
+ `BarChart` / `LineChart` / `AreaChart` / `DonutChart` components from
32
+ [PR #1302](https://github.com/FlourishHealth/terreno/pull/1302).
33
+
34
+ ## Non-Goals
35
+
36
+ - Arbitrary component trees. The agent picks from a closed catalog with a depth cap of 2.
37
+ - Free-form styling. No hex colors, pixel padding, fonts, or `style` objects — only the
38
+ theme tokens `@terreno/ui` already exposes (`color: primary`, `size: lg`, …).
39
+ - Forms and data entry (`TextField`, `DateTimeField`, file upload). v1 interaction is
40
+ buttons, a segmented dataset switch, table row press, chart tooltips, and typed server
41
+ callbacks.
42
+ - Agent-authored queries. The agent never names a collection or writes a filter; `ref`
43
+ datasets come only from server tools (D17).
44
+ - Mixed markdown-and-blocks replies. With `uiBlocks` on, the reply *is* the document; a
45
+ reply that is not a document degrades to one `text` block and is reported to the model
46
+ (D2, D10). Chats without `uiBlocks` are untouched.
47
+ - Multi-series, stacked, or combo charts, zoom, brush. These depend on the chart
48
+ components; the schema reserves room (`series`) but v1 rejects it.
49
+ - Replacing tool-call cards or image/file `contentParts` in `GPTChat`.
50
+ - Slack, A2UI, or json-render wire compatibility. Adapters can come later; v1 owns its
51
+ own format (see Decisions D1).
52
+
53
+ ## Approaches considered
54
+
55
+ The request asked for multiple options. Four were evaluated against: strictness,
56
+ checkability, LLM error rate, chart fit, human readability, and Terreno ownership.
57
+
58
+ | # | Approach | Shape | Strengths | Costs | Verdict |
59
+ | --- | --- | --- | --- | --- | --- |
60
+ | A | **Terreno-owned block list + datasets** (Slack Block Kit shape, dbt Charts data binding) | Ordered `blocks:` (depth ≤ 2) + named `datasets:`; one YAML document per reply | Smallest grammar that covers charts + text + actions; reads like the rendered output; one Zod schema → JSON Schema → prompt; flat enough that LLM edits are local; Terreno owns evolution | We write parser, lint, renderer (~1.5k LOC + tests); no streaming patches in v1 | **Chosen (Q1 = A)** |
61
+ | B | **Adopt Vercel `json-render`** (`@json-render/core` + `@json-render/yaml` + `@json-render/react-native`) with a Terreno catalog | Flat adjacency list `root:` + `elements: {id: {type, props, children}}`; Zod catalog | Streaming patches, devtools, MCP Apps, RN renderer exist today; we only write the catalog | Pre-1.0 (200+ releases since Jan 2026) on the lockstep dependency path; adjacency list is hard for humans to read and for LLMs to keep consistent (dangling ids); RN-web support unverified; styling model is theirs; dataset/chart binding must be bolted on as custom props | Rejected for v1; keep an adapter in mind (D1) |
62
+ | C | **Chart grammar only** (dbt Charts style: `queries` + `charts` + `rows`) | Datasets + chart encodings + row/col layout; text limited to titles | Extremely strict; best chart lint story; tiny prompt | Cannot express metric cards, badges, follow-up buttons, or tables without inventing a second grammar later | Folded into A as the `datasets` + `chart` block |
63
+ | D | **Pure Block Kit clone** (flat `blocks:` array, no nesting, `block_id`/`action_id`, `blocks.validate`) | Section / context / actions / divider / header / table blocks | Proven with LLMs (Slack's Block Kit skill); trivially checkable; interaction payload model is well understood | No first-class data or chart model; `mrkdwn` vs markdown split is Slack-specific; 1-level flat makes side-by-side charts impossible | Folded into A: same block/`id` discipline, plus `columns`/`card` for depth 2 |
64
+
65
+ Option A keeps the parts of B, C, and D that matter — Zod catalog and prompt generation
66
+ (B), datasets with column-typed encodings and visualization lint (C), ordered block list
67
+ with ids, caps, and a `validate` call (D) — without importing a moving dependency.
68
+
69
+ ## Decisions
70
+
71
+ | ID | Question | Decision | Status |
72
+ | --- | --- | --- | --- |
73
+ | D1 | Own the grammar or adopt json-render / A2UI? (Q1) | Own it (Approach A). Ship the contract as a tiny dependency-free-ish package so an adapter to json-render or A2UI can be added later without touching the renderer. | **confirmed** (Q1 = A) |
74
+ | D2 | Wire format on the model side? (Q2: fenced YAML inside markdown / whole reply is YAML) | **The whole assistant reply is one YAML document** (`v`, `datasets`, `blocks`, in that key order). Prose is a `text` block (markdown inside). No fences: the model's text output is the document; a leading/trailing ` ```yaml ` fence is tolerated and stripped. JSON is accepted too (YAML superset; what the structured-output path emits). A reply that does not parse as a document is wrapped as `{v: 1, blocks: [{type: text, markdown: <reply>}]}` for display and reported to the model as `NOT_A_DOCUMENT` (D10, D11). The model is told to emit no text before tool calls; only the final step is the document. | **confirmed** (Q2 = b) |
75
+ | D3 | Nested tree or flat adjacency list? | Nested, depth-capped at 2 (`blocks` → `columns`/`card` → leaf). Nested YAML mirrors the rendered layout and is what humans review; the depth cap keeps LLM error rates near flat-list levels. Ids are optional except on interactive blocks and elements. | assumed |
76
+ | D4 | Where does chart/table data live, and how do thousands of points work? (Q6) | In a top-level `datasets:` map. Each dataset is one of two sources. **`inline`**: `columns: [{name, type: string\|number\|date}]` plus `rows: [[...]]`, capped at 500 rows — for numbers the agent already has. **`ref`**: `{source: ref, id, grain?, limit?}` pointing at a server-side `AIDataset` handle that a tool returned (the model sees `{datasetId, columns, rowCount, preview}` instead of rows); the renderer fetches an aggregated or paginated slice from `GET /gpt/datasets/:id`. Charts and tables reference `data: <name>` and column names (`x`, `y`, `columns`); one dataset can feed a chart and a table; lint checks refs, types, and rendered-point caps. Per-chart inline `points` is also accepted for one-off charts. | **confirmed** (Q6 = b) |
77
+ | D5 | v1 block catalog? | `heading`, `text` (markdown), `metric`, `badge`, `divider`, `context`, `chart` (`kind: line\|bar\|area\|donut`), `table`, `actions`, `columns` (2–4 children), `card`. Maps 1:1 onto `Heading`, `MarkdownView`, `Text`+`Heading`, `Badge`, `SectionDivider`, `Text size=sm color=secondaryLight`, chart components, `DataTable`, `Button`/`SegmentedControl`, `Box direction=row`, `Card`. | assumed |
78
+ | D6 | Interaction model, and how do Block Kit-style server callbacks work? (Q7) | Elements carry an `action` with a closed `kind`: `reply` (post `text` to the chat as the user's next message), `open` (`url` or app `route`; host may allowlist), `select` (client-local: switch a target chart/table's `data` to another dataset), `callback` (`name` + `payload`). The renderer emits one `onAction({blockId, elementId, messageId, action})`; it never executes code from the document. **`callback` ships in v1** with the Slack `block_actions` analog: hosts register `hostActions: {name: {payload: zodSchema, handler}}` on `addGptRoutes`; the client posts to `POST /gpt/actions`; the payload is validated against the host schema before the handler runs; the handler may return `text` (appended assistant message), `blocks` with `replace: block \| message` (in-place re-render, the `chat.update` analog), or nothing (button shows `loading` then success). Registered names are injected into the system prompt; an unregistered name fails validation (`UNKNOWN_HOST_ACTION`) and renders disabled. | **confirmed** (Q7 = b) |
79
+ | D7 | Hard limits (strict mode)? | Unknown keys fail. ≤ 50 blocks total, depth ≤ 2, ≤ 25 elements per `actions`, ≤ 4 columns, ≤ 8 datasets, inline ≤ 500 rows × 12 columns, `ref` `limit` ≤ 1,000 rendered rows (≤ 60 categories for bar/donut), ≤ 8 donut slices, text ≤ 4,000 chars per block, ≤ 20,000 chars per document. Same numbers everywhere (schema, prompt, docs). | assumed |
80
+ | D8 | Where does the contract live? (Q3) | New workspace package `blocks/` → `@terreno/blocks` (schema, parse, partial parse, validate, lint, JSON Schema, prompt section). Deps: `zod` (catalog) and `yaml` (already in `api`). No React, no Express. `@terreno/ui`, `@terreno/ai`, `@terreno/mcp` depend on it. | **confirmed** (Q3 = A) |
81
+ | D9 | Styling vocabulary exposed to the agent? | Only semantic enums already on `@terreno/ui` props: `Text`/`Heading` `size`, `Badge` `status`, `Button` `variant`, `metric.trend: up\|down\|flat`, `Box` `color` surface names. No numbers for spacing, no hex. | assumed |
82
+ | D10 | Rendering while streaming? | Progressive by top-level block. `parseBlocksPartial(text)` returns the completed top-level `blocks` items of a truncated document (a top-level item is complete once the next `- ` at the same indent — or end of stream — arrives); `GPTChat` renders those and shows a `Spinner` row for the in-progress tail. This is why key order `v` → `datasets` → `blocks` is required: datasets are known before the first block renders. On `{done}` the full document is validated and replaces the partial render. An invalid final document renders a compact error `Banner` listing the first three errors and collapses the raw YAML behind an `Accordion`; a non-document falls back to one `text` block; the chat never crashes (wrapped in `ErrorBoundary`). | assumed |
83
+ | D11 | How does the agent check its work? | Three surfaces, one function: (1) `validateBlocks()` runs on the backend when the stream ends and emits SSE `{blocks: {ok, errors, warnings}}` before `{done}`, and the errors are appended to the stored assistant turn so the model's next turn sees them; with `uiBlocks.repair: true` the route runs one bounded repair pass before sending `{done}`; (2) MCP tool `terreno_validate_ui_blocks`; (3) `AIService.generateBlocks()` uses `Output.object(schema)` then `validateBlocks()` with one bounded repair retry. Optional CLI `terreno-blocks validate <file>` ships with the package. | assumed |
84
+ | D12 | Error shape? | `{path: "blocks[3].y", code: "COLUMN_NOT_FOUND", message, fix}` sorted by path; codes are a closed enum documented in the reference page; `fix` is one imperative sentence (dbt Charts style). Lint warnings use the same shape with `severity: warning` and never block rendering. | assumed |
85
+ | D13 | Version field? | Required `v: 1` at the top. Unknown major → `UNSUPPORTED_VERSION`. Additive changes stay in v1; removals bump. | assumed |
86
+ | D14 | Where is it demonstrated first? | `demo` gets a `BlocksView` story and a **Blocks Playground** (YAML editor → live render + error list, the Block Kit Builder analog). `example-frontend` AI tab turns on `uiBlocks` and handles `reply`/`open`/`callback`/`ref`. `example-backend` passes `uiBlocks: {hostActions}` to `addGptRoutes` and registers one tool that returns a dataset handle. | assumed |
87
+ | D15 | Dependency on PR #1302 charts? (Q5) | `chart` block lands after #1302 merges and targets its single-series `{label, value}[]` API exactly. Multi-series stays rejected by the schema until the chart components grow it. No placeholder renderer. | **confirmed** (Q5 = A) |
88
+ | D16 | Documents per message? | Exactly one. `GptHistory` stores the raw document string as the assistant message `content`; rendering derives from it, so history replays identically. | assumed |
89
+ | D17 | Who can create `AIDataset` handles? (Q8) | Only server code: tools call `registerAiDataset({rows, columns, userId, historyId})` from `@terreno/ai`; the agent cannot mint handles or point a `ref` at a collection. `GET /gpt/datasets/:id` is `IsOwner` (the user who ran the tool) and scoped to the history. A `collection` source that lets the agent query models directly is deferred. | **confirmed** (Q8 = A) |
90
+ | D18 | `AIDataset` retention and shape? (Q9) | Mongoose model `AIDataset` (`userId`, `historyId`, `columns`, `rows` as Mixed, `rowCount`, `expiresAt?`, `created`). **Retention is configurable and defaults to unlimited**: `uiBlocks.datasetTtlDays` (default `0` = never expire). Implemented with an `expiresAt` field plus `{expiresAt: 1}, {expireAfterSeconds: 0}` index — documents without `expiresAt` never expire, so the schema stays static. Per-handle cap `uiBlocks.datasetMaxRows` (default 50,000) × 12 columns; `grain` buckets a `date` column server-side (`hour\|day\|week\|month`), `limit` + `page` paginate tables, and line/area over `limit` are downsampled with LTTB. | **confirmed** (Q9: configurable, default unlimited) |
91
+ | D19 | Callback transport and logging? | `POST /gpt/actions` body `{historyId, messageId, blockId, elementId, name, payload}`, `IsAuthenticated` + owner-of-history check, shares the `/gpt` rate-limit bucket, logs an `AIRequest` with `requestType: "ui_action"`. Handler timeout 10 s; on timeout the button shows an error `Toast` and stays enabled. | assumed |
92
+ | D20 | Tool-call turns under whole-reply YAML? | The system prompt instructs: no prose before or between tool calls; the final step's text is the document. `/gpt/prompt` concatenates only the **final step's** `text-delta`s as the document (earlier-step text is dropped from the document but still logged). If the model violates this the `NOT_A_DOCUMENT` fallback applies. | assumed |
93
+
94
+ ## Architecture
95
+
96
+ ```
97
+ MODEL
98
+ system prompt ◄── blocksPromptSection(catalog, limits, hostActions) ── @terreno/blocks
99
+ │
100
+ ▼ reply text == one YAML document (v → datasets → blocks) (or Output.object(schema) JSON)
101
+ @terreno/ai /gpt/prompt ── streams text-delta SSE ──────────────────────┐
102
+ │ tools: registerAiDataset(rows) → {datasetId, columns, rowCount, preview}
103
+ │ on stream end: parseBlocks → validateBlocks({hostActions, knownDatasets})
104
+ │ emits SSE {blocks: {ok, errors[], warnings[]}} → optional repair → {done}
105
+ ▼ ▼
106
+ @terreno/ui GPTChat(uiBlocks) ── parseBlocksPartial while streaming ── BlocksView(doc, onAction, resolveDataset)
107
+ │
108
+ Heading · MarkdownView · Badge · DataTable · Card · Box
109
+ BarChart · LineChart · AreaChart · DonutChart (#1302)
110
+ Button · SegmentedControl ──► onAction({blockId, elementId, messageId, action})
111
+ │
112
+ host: reply → onSubmit(text) · open → router
113
+ select → local dataset switch
114
+ callback → POST /gpt/actions → hostActions[name].handler
115
+ └─► {text?, blocks?, replace?} → re-render block/message
116
+ ref datasets → GET /gpt/datasets/:id?grain=&limit=&page=
117
+ ```
118
+
119
+ Layers and ownership:
120
+
121
+ | Layer | Package | Owns |
122
+ | --- | --- | --- |
123
+ | Contract | `@terreno/blocks` | Zod schema (`strict()`), `parseBlocks`, `parseBlocksPartial`, `validateBlocks` (structure + semantic lint), `wrapAsTextDocument`, `blocksJsonSchema`, `blocksPromptSection`, error codes, limits, fixtures |
124
+ | Renderer | `@terreno/ui` | `BlocksView` (lazy, `heavyOptionalExports`), `GPTChat.uiBlocks` mode (progressive render, `onBlockAction`, `onBlockCallback`, `resolveDataset`), error/placeholder states |
125
+ | Producer | `@terreno/ai` | `TERRENO_UI_BLOCKS_SYSTEM_PROMPT`, `addGptRoutes({uiBlocks: {hostActions, repair, datasetTtlDays, datasetMaxRows}})`, post-stream validation SSE, `AIService.generateBlocks`, repair pass, `AIDataset` model + `registerAiDataset` + `GET /gpt/datasets/:id`, `POST /gpt/actions`, request logging |
126
+ | Tooling | `@terreno/mcp`, `blocks/bin` | `terreno_validate_ui_blocks` tool, `terreno-blocks validate` CLI |
127
+ | Proof | `demo`, `example-*` | Stories, playground, AI tab wiring, e2e mock |
128
+
129
+ ### The grammar (v1)
130
+
131
+ A complete assistant reply:
132
+
133
+ ```yaml
134
+ v: 1
135
+ datasets:
136
+ signups: # inline: the agent already has the numbers (≤ 500 rows)
137
+ columns:
138
+ - {name: month, type: string}
139
+ - {name: count, type: number}
140
+ rows:
141
+ - [Jan, 120]
142
+ - [Feb, 145]
143
+ - [Mar, 138]
144
+ signups_daily: # ref: a tool returned a handle; rows never pass through the model
145
+ source: ref
146
+ id: ds_8f2c1
147
+ grain: week
148
+ limit: 200
149
+ blocks:
150
+ - type: heading
151
+ text: Signups this quarter
152
+ size: lg
153
+ - type: text
154
+ markdown: |
155
+ Signups grew **12%** quarter over quarter. February was the strongest month.
156
+ - type: columns
157
+ children:
158
+ - type: metric
159
+ label: Total
160
+ value: "403"
161
+ delta: "+12%"
162
+ trend: up
163
+ - type: metric
164
+ label: Best month
165
+ value: Feb
166
+ - type: chart
167
+ id: signups_chart
168
+ kind: bar
169
+ data: signups
170
+ x: month
171
+ y: count
172
+ title: Signups by month
173
+ - type: chart
174
+ kind: line
175
+ data: signups_daily
176
+ x: day
177
+ y: count
178
+ title: Weekly signups (from 12,480 daily rows)
179
+ - type: table
180
+ data: signups
181
+ columns: [month, count]
182
+ - type: actions
183
+ id: followups
184
+ elements:
185
+ - type: button
186
+ id: weekly
187
+ text: Show weekly
188
+ action: {kind: reply, text: Show weekly signups for this quarter}
189
+ - type: button
190
+ id: export
191
+ text: Export CSV
192
+ variant: outline
193
+ action: {kind: callback, name: exportDataset, payload: {dataset: signups}}
194
+ ```
195
+
196
+ A plain answer with no data is still a document:
197
+
198
+ ```yaml
199
+ v: 1
200
+ blocks:
201
+ - type: text
202
+ markdown: I could not find any signups for that range. Try a wider date window.
203
+ ```
204
+
205
+ Block reference (full field tables live in `docs/reference/blocks.md`):
206
+
207
+ | Block | Required | Optional | Renders |
208
+ | --- | --- | --- | --- |
209
+ | `heading` | `text` | `size` | `Heading` |
210
+ | `text` | `markdown` | — | `MarkdownView` |
211
+ | `metric` | `label`, `value` | `delta`, `trend`, `helper` | `Card` with `Text`+`Heading` |
212
+ | `badge` | `text` | `status` | `Badge` |
213
+ | `divider` | — | — | `SectionDivider` |
214
+ | `context` | `text` | — | `Text size=sm color=secondaryLight` |
215
+ | `chart` | `kind`, (`data`+`x`+`y`) or `points` | `id`, `title`, `legend`, `emptyText`, `height: sm\|md\|lg` | `BarChart` / `LineChart` / `AreaChart` / `DonutChart` in `Card` |
216
+ | `table` | `data` | `id`, `columns`, `title`, `rowAction` | `DataTable` |
217
+ | `actions` | `id`, `elements` | — | `Box direction=row wrap` of `Button` / `SegmentedControl` |
218
+ | `columns` | `children` (2–4 blocks) | — | `Box direction=row`, stacks on `sm` |
219
+ | `card` | `children` | `title` | `Card` |
220
+
221
+ Element reference: `button {id, text, action, variant?, iconName?}`,
222
+ `segmented {id, target, options: [{label, data}]}` (client-local dataset switch).
223
+
224
+ ### Validation pipeline
225
+
226
+ 1. `parseBlocks(text)` → strip an optional surrounding ` ```yaml ` / ` ``` ` fence → YAML
227
+ (or JSON) → plain object; YAML anchors/tags rejected (`schema: "core"`,
228
+ `uniqueKeys: true`, no custom tags). Not a mapping with `v` → `NOT_A_DOCUMENT`
229
+ (callers may use `wrapAsTextDocument(text)` for display).
230
+ 2. Zod `strict()` structural pass → all structural errors at once, not first-failure.
231
+ Key order `v` → `datasets` → `blocks` is enforced (`KEY_ORDER`) so the partial parser
232
+ is sound.
233
+ 3. Semantic lint pass: dataset refs, column existence and type (`y` numeric, `x`
234
+ string/date), row arity, id uniqueness, `select` targets exist and are charts/tables,
235
+ `callback` names in the host allowlist (`UNKNOWN_HOST_ACTION`, when `hostActions` is
236
+ passed), limits from D7 including `ref` `limit` caps (`TOO_MANY_POINTS — set grain to a
237
+ coarser bucket`), chart heuristics (`BAR_TOO_MANY_CATEGORIES` > 60,
238
+ `DONUT_TOO_MANY_SLICES` > 8, `LINE_SINGLE_POINT`, `TABLE_TOO_WIDE` > 12 columns). `ref`
239
+ column checks run where the handle's columns are known (`knownDatasets` option, used
240
+ server-side); otherwise they defer to fetch time.
241
+ 4. Result: `{ok: true, doc, warnings}` or `{ok: false, errors, warnings}`. Pure, sync,
242
+ no I/O, target < 5 ms for a 50-block document (asserted in a test).
243
+
244
+ `parseBlocksPartial(text)` is a separate, lenient entry point used only while streaming:
245
+ it returns `{datasets, blocks: completedTopLevelBlocks, pending: boolean}` and never
246
+ throws; each completed block is validated individually so a bad block shows its error
247
+ inline without blocking the rest.
248
+
249
+ ### Interaction flow
250
+
251
+ `BlocksView` is controlled and side-effect free. Every element press calls
252
+ `onAction({blockId, elementId, messageId, action})`. Hosts decide:
253
+
254
+ | `action.kind` | `GPTChat` default | `example-frontend` |
255
+ | --- | --- | --- |
256
+ | `reply` | calls `onSubmit(action.text)` | same |
257
+ | `open` | calls `onBlockAction`; no default navigation | `router.push(route)` or `Linking.openURL(url)` after allowlist |
258
+ | `select` | handled inside `BlocksView` (local state), then reported | reported only |
259
+ | `callback` | calls `onBlockCallback`; button enters `loading` until the promise settles | `POST /gpt/actions`; on `{blocks, replace}` the message/block re-renders; on `{text}` an assistant message is appended |
260
+
261
+ Server callback contract (Block Kit `block_actions` analog, typed):
262
+
263
+ ```ts
264
+ addGptRoutes(router, {
265
+ aiService,
266
+ openApiOptions: options,
267
+ uiBlocks: {
268
+ hostActions: {
269
+ exportDataset: {
270
+ payload: z.object({dataset: z.string()}).strict(),
271
+ handler: async ({payload, user, history}) => ({
272
+ replace: "block",
273
+ blocks: {v: 1, blocks: [{type: "badge", text: "Exporting…", status: "info"}]},
274
+ }),
275
+ },
276
+ },
277
+ datasetTtlDays: 0, // default: keep forever
278
+ },
279
+ });
280
+ ```
281
+
282
+ Datasets from tools (`ref` source):
283
+
284
+ ```ts
285
+ const handle = await registerAiDataset({userId, historyId, columns, rows}); // server only
286
+ // tool result seen by the model:
287
+ // {datasetId: "ds_8f2c1", columns: [...], rowCount: 12480, preview: rows.slice(0, 20), stats: {...}}
288
+ ```
289
+
290
+ ## Models
291
+
292
+ | Model | Fields | Notes |
293
+ | --- | --- | --- |
294
+ | `AIDataset` (new, `@terreno/ai`) | `userId` (ref User, required), `historyId` (ref GptHistory), `columns: [{name, type}]`, `rows: Mixed[]`, `rowCount: number`, `expiresAt?: Date`, `created` | `{expiresAt: 1}` index with `expireAfterSeconds: 0`; `expiresAt` set only when `datasetTtlDays > 0`; cap `datasetMaxRows` (default 50,000) × 12; `isDeletedPlugin`, `createdUpdatedPlugin`; every field has a `description` |
295
+
296
+ `AIRequest.metadata` gains `uiBlocks: {ok, errorCodes[], warningCodes[], blockCount, repaired}` for
297
+ observability; `requestType` gains `"ui_blocks"` and `"ui_action"`. `GptHistory` is
298
+ unchanged: the assistant message `content` is the raw document string.
299
+
300
+ ## APIs
301
+
302
+ | Surface | Change |
303
+ | --- | --- |
304
+ | `addGptRoutes(router, {aiService, openApiOptions, uiBlocks?: boolean \| UiBlocksOptions})` | `UiBlocksOptions = {hostActions?: Record<string, HostAction>, repair?: boolean, datasetTtlDays?: number, datasetMaxRows?: number}`. When enabled, appends `TERRENO_UI_BLOCKS_SYSTEM_PROMPT` (with host `callback` names) to the effective system prompt; after the final step's text stream ends, validates the document and emits SSE `{blocks: {ok, errors, warnings}}` before `{done}`; runs one repair pass when `repair` is on and validation failed; mounts the two routes below. |
305
+ | `POST /gpt/actions` | Body `{historyId, messageId, blockId, elementId, name, payload}`; validates `payload` with `hostActions[name].payload`; runs the handler (10 s timeout); returns `{text?, blocks?, replace?}`; `IsAuthenticated` + history owner; logged as `ui_action`. |
306
+ | `GET /gpt/datasets/:id` | Query `grain?`, `limit?`, `page?`; `IsOwner`; returns `{columns, rows, rowCount, page, more}` after server-side bucketing / LTTB / pagination. |
307
+ | `registerAiDataset({userId, historyId, columns, rows})` | Server-only helper for tools; returns the tool-result shape the model sees (`datasetId`, `columns`, `rowCount`, `preview`, `stats`); applies `datasetTtlDays` / `datasetMaxRows`. |
308
+ | `AIService.generateBlocks({prompt, systemPrompt?, userId?, repair?: boolean})` | `Output.object(blocksJsonSchema)` → `validateBlocks`; on failure and `repair !== false`, one retry with errors appended; logs `requestType: "ui_blocks"`. |
309
+ | `@terreno/blocks` exports | `parseBlocks`, `parseBlocksPartial`, `validateBlocks`, `wrapAsTextDocument`, `blocksSchema` (Zod), `blocksJsonSchema`, `blocksPromptSection`, `BLOCK_LIMITS`, `BLOCK_ERROR_CODES`, types. |
310
+ | MCP | `terreno_validate_ui_blocks({document: string})` → text report identical to CLI output. |
311
+ | CLI | `terreno-blocks validate <file\|->` exit 0/1, prints errors as `path CODE message — fix`. |
312
+
313
+ ## UI
314
+
315
+ | Component | Package | Notes |
316
+ | --- | --- | --- |
317
+ | `BlocksView` | `@terreno/ui` (lazy) | Props: `document: string \| BlocksDocument \| PartialBlocksDocument`, `onAction?`, `hostActions?` (names), `resolveDataset?: (ref) => Promise<Dataset>`, `pendingElementIds?`, `overrides?` (per-block replacement), `testID?`. Validates when given a string; renders error `Banner` + collapsed raw when invalid; `ref` datasets show chart `loading` until resolved. |
318
+ | `GPTChat.uiBlocks` | `@terreno/ui` | When `true`, assistant messages render through `BlocksView` instead of `MarkdownView`: streaming messages use `parseBlocksPartial` + a `Spinner` tail row; completed messages use `parseBlocks` with `wrapAsTextDocument` fallback. New props `onBlockAction`, `onBlockCallback`, `resolveDataset`. `reply` → `onSubmit`. |
319
+ | Blocks Playground | `demo` | Story with `TextArea` (YAML) → live `BlocksView` + error list; ships the golden fixtures as presets. |
320
+
321
+ ## Phases
322
+
323
+ | Phase | Slice | Proves |
324
+ | --- | --- | --- |
325
+ | 1 | `@terreno/blocks` contract: schema, parse, partial parse, validate, lint, JSON Schema, prompt, CLI | Golden fixtures: every valid fixture parses; every invalid fixture yields the expected code at the expected path; < 5 ms; partial parser yields completed blocks from truncated fixtures |
326
+ | 2 | `@terreno/ui` renderer: leaf blocks → chart/table → layout + actions → `GPTChat` `uiBlocks` mode + playground | Bun tests per block; demo screenshots; playground video |
327
+ | 3 | `@terreno/ai` producer: `uiBlocks` route option, validation SSE, repair, `generateBlocks`, `AIDataset` + `GET /gpt/datasets/:id`, `POST /gpt/actions` | supertest with mock model emitting a document; SSE `blocks` event asserted; repair pass asserted; dataset bucketing and action payload validation asserted |
328
+ | 4 | Tooling + examples + docs: MCP tool, example-frontend AI tab, example-backend option + dataset tool, how-to | e2e `ai-chat.spec.ts` renders a chart from mocked SSE; MCP tool test |
329
+
330
+ ## Feature Flags & Migrations
331
+
332
+ None. `uiBlocks` is off unless the consumer passes it. No data migration; `AIDataset` is
333
+ a new collection.
334
+
335
+ ## Not Included / Future Work
336
+
337
+ - Sub-block streaming (render a chart before its dataset rows finish) via YAML patches.
338
+ - A `collection` dataset source that lets the agent query Mongoose models directly
339
+ through permission-scoped aggregation (Q8, deferred).
340
+ - Multi-series / stacked charts when #1302 grows them.
341
+ - Form elements (`select`, `date`, `text input`) with a `submit` action.
342
+ - Adapters: emit A2UI or json-render specs from a validated `BlocksDocument`.
343
+ - Admin AI playground rendering blocks in the observability screens (#1196).
344
+
345
+ ## Files to Create / Modify
346
+
347
+ | Package | Files |
348
+ | --- | --- |
349
+ | `blocks/` (new) | `package.json`, `tsconfig.json`, `biome.jsonc`, `src/index.ts`, `src/schema.ts`, `src/limits.ts`, `src/errors.ts`, `src/parse.ts`, `src/parsePartial.ts`, `src/validate.ts`, `src/lint.ts`, `src/prompt.ts`, `src/jsonSchema.ts`, `src/cli.ts`, `src/fixtures/valid/*.yaml`, `src/fixtures/invalid/*.yaml`, `src/fixtures/partial/*.yaml`, `*.test.ts` |
350
+ | `ui/` | `src/blocks/BlocksView.tsx`, `src/blocks/blockRenderers.tsx`, `src/blocks/BlocksError.tsx`, `src/blocks/useResolvedDatasets.ts`, `src/blocks/useBlockSelections.ts`, `src/GPTChat.tsx` (`uiBlocks` mode, progressive render, callbacks), `src/lazyBoundaries/heavyOptionalExports.tsx`, `src/index.tsx`, `src/Common.ts` (props), tests |
351
+ | `ai/` | `src/service/prompts.ts` (`TERRENO_UI_BLOCKS_SYSTEM_PROMPT`), `src/service/aiService.ts` (`generateBlocks`), `src/routes/gpt.ts` (`uiBlocks`, SSE `blocks`, repair), `src/routes/gptActions.ts` (`POST /gpt/actions`), `src/routes/gptDatasets.ts` (`GET /gpt/datasets/:id`), `src/models/aiDataset.ts`, `src/service/aiDatasets.ts` (`registerAiDataset`, bucketing, LTTB), `src/types/index.ts`, tests |
352
+ | `mcp-server/` | `src/tools.ts` (`terreno_validate_ui_blocks`), test |
353
+ | `demo/` | `stories/BlocksView.stories.tsx`, `story-config/BlocksView.config.tsx`, `stories/BlocksPlayground.stories.tsx`, `demoConfig.tsx` |
354
+ | `example-frontend/` | `app/(tabs)/ai.tsx`, `store/openApiSdk.ts` (generated), `e2e/helpers/mockGpt.ts`, `e2e/ai-chat.spec.ts` |
355
+ | `example-backend/` | `src/server.ts` (`uiBlocks`), `src/ai/hostActions.ts`, `src/ai/tools.ts` |
356
+ | root | `package.json` (workspace `blocks`, catalog `yaml`), `knip.jsonc`, `.github/workflows` (blocks CI), `changelog/unreleased/agent-ui-blocks.md` |
357
+ | docs | `docs/explanation/agent-ui-blocks.md` (new), `docs/reference/blocks.md` (new), `docs/how-to/agent-ui-blocks.md` (new), `docs/reference/ui.md`, `docs/reference/ai.md`, `docs/reference/mcp-server.md`, `docs/explanation/README.md`, `docs/reference/README.md`, `docs/how-to/README.md` |
358
+
359
+ ## Task List
360
+
361
+ [`docs/tasks/agent-ui-blocks.md`](../tasks/agent-ui-blocks.md)
362
+
363
+ ## Acceptance Criteria
364
+
365
+ | # | Criterion | Verification |
366
+ | --- | --- | --- |
367
+ | AC1 | Every fixture under `blocks/src/fixtures/valid/` validates with zero errors; every fixture under `invalid/` fails with exactly the expected `{path, code}` pairs | `bun test blocks/` golden test |
368
+ | AC2 | Unknown keys, wrong enum values, wrong key order, missing dataset/column refs, non-document input, and each D7 limit produce a distinct documented error code | `blocks/src/validate.test.ts` one case per code; `docs/reference/blocks.md` lists the same codes (doc-code parity test) |
369
+ | AC3 | `validateBlocks` on a 50-block, 8-dataset document completes in < 5 ms median | `blocks/src/validate.perf.test.ts` |
370
+ | AC4 | `blocksJsonSchema` accepts and rejects the same fixtures as the Zod schema | round-trip test using `ajv` (dev dependency) |
371
+ | AC5 | `BlocksView` renders every v1 block type with `@terreno/ui` components only; no raw `View`/`Text` in `ui/src/blocks/` | `BlocksView.test.tsx` per block + `rg` guard test |
372
+ | AC6 | Invalid document → error `Banner` with first three errors, raw YAML in a collapsed `Accordion`, no throw; non-document text → rendered as one `text` block | `BlocksView.test.tsx` |
373
+ | AC7 | Pressing a `reply` button in `GPTChat` calls `onSubmit(text)`; `open` reaches `onBlockAction` and `callback` reaches `onBlockCallback` with `{blockId, elementId, messageId, action}` | `GPTChat.test.tsx` |
374
+ | AC8 | `segmented` element switches the target chart's dataset locally and reports `select` | `BlocksView.test.tsx` |
375
+ | AC9 | `/gpt/prompt` with `uiBlocks` appends the blocks prompt section, treats the final step's text as the document, and emits SSE `{blocks: {ok, errors, warnings}}` before `{done}`; with `repair: true` an invalid document triggers exactly one repair call and the repaired text is what gets stored | `ai/src/routes/gpt.test.ts` with a mock model that streams a document |
376
+ | AC10 | `AIService.generateBlocks` returns a validated document; an invalid first attempt triggers exactly one repair call whose prompt includes the error codes | `aiService.test.ts` mock `doGenerate` sequence |
377
+ | AC11 | MCP `terreno_validate_ui_blocks` returns the same report text as the CLI for the same input | `mcp-server` tool test + `blocks/src/cli.test.ts` |
378
+ | AC12 | Demo: `BlocksView` story and Blocks Playground registered; `bun run check:demo-coverage` passes | CI + screenshots under `/opt/cursor/artifacts/` |
379
+ | AC13 | example-frontend AI tab renders a bar chart from a mocked SSE document reply and a follow-up button sends a reply | `example-frontend/e2e/ai-chat.spec.ts` + recording |
380
+ | AC14 | Docs: explanation, reference, how-to pages exist and are linked from their READMEs; `docs/reference/ai.md`, `ui.md`, `mcp-server.md` updated | `bun run website:build` + reviewer read |
381
+ | AC15 | `bun run prepush` green (lint, knip, no-barrel-imports, source rules, demo coverage) | CI |
382
+ | AC16 | `GET /gpt/datasets/:id` returns ≤ `limit` rows for a 12,480-row handle: `grain=week` buckets a `date` column, line/area over `limit` are LTTB-downsampled, tables paginate with `more`; another user gets 404; with `datasetTtlDays: 0` no `expiresAt` is set, with `7` it is `created + 7d` | `ai/src/routes/gptDatasets.test.ts`, `ai/src/service/aiDatasets.test.ts` |
383
+ | AC17 | `POST /gpt/actions` rejects an unregistered `name` (404), a payload failing the host Zod schema (400 with `fields`), and a non-owner (403); a valid call returns the handler's `{blocks, replace}` and logs `ui_action` | `ai/src/routes/gptActions.test.ts` |
384
+ | AC18 | A `ref` dataset whose `limit` exceeds the cap fails validation with `TOO_MANY_POINTS` and a `grain` fix; a `callback` with an unregistered name fails server-side with `UNKNOWN_HOST_ACTION` | `blocks/src/lint.test.ts`, `ai/src/routes/gpt.test.ts` |
385
+ | AC19 | In `GPTChat`, a `callback` button shows `loading` until the host resolves; a `{blocks, replace: "block"}` response replaces only that block; `{text}` appends an assistant message | `GPTChat.test.tsx`, `BlocksView.test.tsx` |
386
+ | AC20 | While streaming, `GPTChat` renders each completed top-level block as soon as its successor starts and shows a `Spinner` tail; a truncated fixture cut mid-block yields exactly the preceding blocks from `parseBlocksPartial` | `blocks/src/parsePartial.test.ts`, `GPTChat.test.tsx` |
@@ -20,8 +20,8 @@ Grow (shape) → Pick (build) → Roast (prove) → Brew (submit) → Taste (rea
20
20
  The outer loop owns invocation, persistence, Taste `PENDING` reinvocation, retry, stop,
21
21
  and escalation. Brew waits in-process for async review bots. Taste waits in-process for
22
22
  review bots and for product CI (GitHub CLI or CircleCI CLI watch loop). Before any
23
- push it pulls latest `master`, lints in a no-context subagent, then pushes and watches
24
- CI.
23
+ push it pulls latest `master`, runs root `prepush` when present (otherwise affected
24
+ package lint/typecheck/tests) in a no-context subagent, then pushes and watches CI.
25
25
  Lifecycle skills own how one stage is performed. Repository skills own how this codebase
26
26
  works. Roast owns independent acceptance proof. State/evidence bridge fresh invocations.
27
27
 
@@ -56,13 +56,13 @@ This is a refactor of the existing strong workflow, not a parallel implementatio
56
56
  | AP2 | Sensitive-data rules cover credentials, customer data, PII/PHI, and evidence media |
57
57
  | AP3 | Canonical stages are Grow, Pick, Roast, Brew, Taste |
58
58
  | AP4 | Stages discover supporting skills by description; exact skill names are never universal dependencies |
59
- | AP5 | Taste is one observe/act/emit iteration after in-process waits for async review bots and product CI. Before any push: always pull latest `master`, then run lint, typecheck, and affected tests in a fresh subagent with no parent conversation, then push and watch CI (`gh` / `circleci`). The outer loop reinvokes on Taste `PENDING` |
59
+ | AP5 | Taste is one observe/act/emit iteration after in-process waits for async review bots and product CI. Before any push: always pull latest `master`, record last-run failed tests and re-verify them locally, then run the repository root's `prepush` package script when present (otherwise affected-package lint, typecheck, and tests) in a fresh subagent with no parent conversation, then push and watch CI (`gh` / `circleci`). The outer loop reinvokes on Taste `PENDING` |
60
60
  | AP6 | Shared results use compact `v: 2` YAML; required keys are `v`, `stage`, `status`, `next`, `action`; empty keys are omitted; YAML is collapsed for humans |
61
61
  | AP7 | Existing repository state convention wins; fallback reuses loop-owned `.terreno/pipeline/<slug>.json`, not committed by default |
62
62
  | AP8 | Brew emits PR/head state and exits; direct Taste invocation is standalone compatibility only |
63
63
  | AP9 | No deprecated command aliases: old implementation-Roast conflicts with new verification-Roast and no maintained alias mechanism exists |
64
64
  | AP10 | Plugin major version is `2.0.0` because lifecycle semantics and command names are breaking |
65
- | AP11 | Grow lists every grilled decision in an unbounded Decisions table after the 15-line index, or omits the table when there were none; grilling stays on a question until the answer is executable |
65
+ | AP11 | Grow ends with a standalone approval brief — orientation paragraph, optional background, idea, plan — then an unbounded Decisions table pairing every grilled decision with the question that prompted it, or omits that table when there were none; grilling stays on a question until the answer is executable |
66
66
  | AP12 | Brew and Taste wait until Bugbot, CodeQL, and similar review bots on the current head have reported, preferring hooks targeted to the matched bot or harness subscriptions over timer polling; unfiltered PR-check watches are product-CI waits owned by Taste |
67
67
  | AP13 | Product CI is every discovered host (GitHub Actions, CircleCI, Buildkite, GitLab CI, and similar). Taste observes native jobs when GitHub checks are incomplete; Brew confirms each host triggered or documented a not-applicable skip. An unexplained untriggered host prevents Brew `PASS`; a documented skip is terminal for Taste |
68
68
  | AP14 | Taste waits in-process with the provider's bounded native watch command (`gh pr checks --watch`, `gh run watch`, `circleci run watch`, `bk build watch`) in a loop until jobs are terminal or the wait times out. Outer loops honor Taste `PENDING` with the same hooks. Watch exit codes trigger a fresh classification rather than becoming stage verdicts directly |
@@ -133,7 +133,8 @@ current head rather than trusting stale state.
133
133
  Researches repository facts, distinguishes human decisions from discoverable facts and
134
134
  low-risk conventional details, grills until each answer is executable, then writes
135
135
  approved, implementation-ready IP/tasks. Every acceptance criterion maps to verification.
136
- Approval shows a 15-line index plus a full Decisions table when any grilled decisions
136
+ Approval shows a standalone brief — orientation paragraph, optional background, idea,
137
+ plan — plus a full Decisions table with prompting questions when any grilled decisions
137
138
  exist.
138
139
 
139
140
  ### Pick
@@ -165,9 +166,11 @@ provider CLI watch hooks or harness subscriptions, then waits in a loop for prod
165
166
  using GitHub CLI (`gh pr checks --watch`, `gh run watch`) or CircleCI CLI
166
167
  (`circleci run watch`) until jobs on every discovered host are terminal or the wait
167
168
  times out. It then classifies mergeability and reviews, performs one bounded set of
168
- actionable fixes. Before any push it always pulls latest `master`, then spawns a fresh
169
- subagent with no parent conversation to run lint, typecheck, and locally affected tests
170
- in each affected package, then pushes and watches review bots and product CI. It may act
169
+ actionable fixes. Before any push it always pulls latest `master`, records last-run
170
+ failed tests and re-verifies them locally, then spawns a fresh
171
+ subagent with no parent conversation to run root `prepush` when present, falling back to
172
+ lint, typecheck, and locally affected tests in each affected package. It then pushes and
173
+ watches review bots and product CI. It may act
171
174
  once more after that watch. It emits
172
175
  `PASS`/`PENDING`/`BLOCKED`/`FAIL` and exits. The outer loop reinvokes after timeout or a
173
176
  second post-fix push.