@terreno/mcp 57.9.0 → 57.11.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 (111) hide show
  1. package/dist/docs/upgrades/57.10.1.md +54 -0
  2. package/dist/docs/upgrades/57.11.0.md +80 -0
  3. package/dist/docs/versioned/next/README.md +1 -0
  4. package/dist/docs/versioned/next/explanation/README.md +4 -0
  5. package/dist/docs/versioned/next/explanation/admin-plugin-frontend.md +2 -2
  6. package/dist/docs/versioned/next/explanation/agent-ui-asks.md +299 -0
  7. package/dist/docs/versioned/next/explanation/agent-ui-blocks.md +67 -0
  8. package/dist/docs/versioned/next/explanation/ai-observability.md +101 -0
  9. package/dist/docs/versioned/next/explanation/durable-agent-harness.md +544 -0
  10. package/dist/docs/versioned/next/explanation/example-coverage.md +3 -1
  11. package/dist/docs/versioned/next/explanation/local-first-data.md +1 -0
  12. package/dist/docs/versioned/next/explanation/loop-engineering.md +1 -1
  13. package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +29 -1
  14. package/dist/docs/versioned/next/explanation/typescript-configuration.md +1 -1
  15. package/dist/docs/versioned/next/explanation/versioning-policy.md +1 -1
  16. package/dist/docs/versioned/next/how-to/README.md +7 -0
  17. package/dist/docs/versioned/next/how-to/agent-ui-asks.md +730 -0
  18. package/dist/docs/versioned/next/how-to/agent-ui-blocks.md +104 -0
  19. package/dist/docs/versioned/next/how-to/ai-feature-development.md +156 -0
  20. package/dist/docs/versioned/next/how-to/build-a-durable-workflow.md +232 -0
  21. package/dist/docs/versioned/next/how-to/call-external-apis.md +1 -1
  22. package/dist/docs/versioned/next/how-to/charts-and-dashboards.md +2 -0
  23. package/dist/docs/versioned/next/how-to/circleci.md +5 -3
  24. package/dist/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +31 -1
  25. package/dist/docs/versioned/next/how-to/observe-llm-calls.md +197 -0
  26. package/dist/docs/versioned/next/how-to/preview-demo.md +7 -3
  27. package/dist/docs/versioned/next/how-to/run-harness-on-multiple-instances.md +93 -0
  28. package/dist/docs/versioned/next/how-to/run-tests-locally.md +2 -1
  29. package/dist/docs/versioned/next/how-to/ship-a-new-task-version.md +76 -0
  30. package/dist/docs/versioned/next/how-to/upgrade-terreno.md +1 -1
  31. package/dist/docs/versioned/next/implementationPlans/agent-ui-asks.md +688 -0
  32. package/dist/docs/versioned/next/implementationPlans/agent-ui-blocks.md +10 -6
  33. package/dist/docs/versioned/next/implementationPlans/ai-observability.md +475 -157
  34. package/dist/docs/versioned/next/implementationPlans/durable-agent-harness.md +340 -0
  35. package/dist/docs/versioned/next/implementationPlans/terreno-langfuse-integration.md +1 -1
  36. package/dist/docs/versioned/next/reference/README.md +3 -0
  37. package/dist/docs/versioned/next/reference/admin-config.md +2 -0
  38. package/dist/docs/versioned/next/reference/admin-frontend.md +216 -0
  39. package/dist/docs/versioned/next/reference/agent-ui-asks.md +1059 -0
  40. package/dist/docs/versioned/next/reference/ai-harness.md +1974 -0
  41. package/dist/docs/versioned/next/reference/ai.md +251 -25
  42. package/dist/docs/versioned/next/reference/api.md +110 -2
  43. package/dist/docs/versioned/next/reference/blocks.md +178 -0
  44. package/dist/docs/versioned/next/reference/environment-variables.md +2 -1
  45. package/dist/docs/versioned/next/reference/lifecycle-plugin.md +11 -0
  46. package/dist/docs/versioned/next/reference/mcp-server.md +17 -0
  47. package/dist/docs/versioned/next/reference/syncdb.md +156 -6
  48. package/dist/docs/versioned/next/reference/test.md +11 -2
  49. package/dist/docs/versioned/next/reference/ui.md +331 -6
  50. package/dist/docs/versioned/next/tasks/agent-ui-asks.md +213 -0
  51. package/dist/docs/versioned/next/tasks/agent-ui-blocks.md +2 -1
  52. package/dist/docs/versioned/next/tasks/ai-observability.md +233 -104
  53. package/dist/docs/versioned/next/tasks/durable-agent-harness.md +185 -0
  54. package/dist/tools.d.ts +4 -0
  55. package/dist/tools.d.ts.map +1 -1
  56. package/dist/tools.js +37 -0
  57. package/dist/tools.js.map +1 -1
  58. package/package.json +3 -2
  59. package/src/docs/upgrades/57.10.1.md +54 -0
  60. package/src/docs/upgrades/57.11.0.md +80 -0
  61. package/src/docs/versioned/next/README.md +1 -0
  62. package/src/docs/versioned/next/explanation/README.md +4 -0
  63. package/src/docs/versioned/next/explanation/admin-plugin-frontend.md +2 -2
  64. package/src/docs/versioned/next/explanation/agent-ui-asks.md +299 -0
  65. package/src/docs/versioned/next/explanation/agent-ui-blocks.md +67 -0
  66. package/src/docs/versioned/next/explanation/ai-observability.md +101 -0
  67. package/src/docs/versioned/next/explanation/durable-agent-harness.md +544 -0
  68. package/src/docs/versioned/next/explanation/example-coverage.md +3 -1
  69. package/src/docs/versioned/next/explanation/local-first-data.md +1 -0
  70. package/src/docs/versioned/next/explanation/loop-engineering.md +1 -1
  71. package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +29 -1
  72. package/src/docs/versioned/next/explanation/typescript-configuration.md +1 -1
  73. package/src/docs/versioned/next/explanation/versioning-policy.md +1 -1
  74. package/src/docs/versioned/next/how-to/README.md +7 -0
  75. package/src/docs/versioned/next/how-to/agent-ui-asks.md +730 -0
  76. package/src/docs/versioned/next/how-to/agent-ui-blocks.md +104 -0
  77. package/src/docs/versioned/next/how-to/ai-feature-development.md +156 -0
  78. package/src/docs/versioned/next/how-to/build-a-durable-workflow.md +232 -0
  79. package/src/docs/versioned/next/how-to/call-external-apis.md +1 -1
  80. package/src/docs/versioned/next/how-to/charts-and-dashboards.md +2 -0
  81. package/src/docs/versioned/next/how-to/circleci.md +5 -3
  82. package/src/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +31 -1
  83. package/src/docs/versioned/next/how-to/observe-llm-calls.md +197 -0
  84. package/src/docs/versioned/next/how-to/preview-demo.md +7 -3
  85. package/src/docs/versioned/next/how-to/run-harness-on-multiple-instances.md +93 -0
  86. package/src/docs/versioned/next/how-to/run-tests-locally.md +2 -1
  87. package/src/docs/versioned/next/how-to/ship-a-new-task-version.md +76 -0
  88. package/src/docs/versioned/next/how-to/upgrade-terreno.md +1 -1
  89. package/src/docs/versioned/next/implementationPlans/agent-ui-asks.md +688 -0
  90. package/src/docs/versioned/next/implementationPlans/agent-ui-blocks.md +10 -6
  91. package/src/docs/versioned/next/implementationPlans/ai-observability.md +475 -157
  92. package/src/docs/versioned/next/implementationPlans/durable-agent-harness.md +340 -0
  93. package/src/docs/versioned/next/implementationPlans/terreno-langfuse-integration.md +1 -1
  94. package/src/docs/versioned/next/reference/README.md +3 -0
  95. package/src/docs/versioned/next/reference/admin-config.md +2 -0
  96. package/src/docs/versioned/next/reference/admin-frontend.md +216 -0
  97. package/src/docs/versioned/next/reference/agent-ui-asks.md +1059 -0
  98. package/src/docs/versioned/next/reference/ai-harness.md +1974 -0
  99. package/src/docs/versioned/next/reference/ai.md +251 -25
  100. package/src/docs/versioned/next/reference/api.md +110 -2
  101. package/src/docs/versioned/next/reference/blocks.md +178 -0
  102. package/src/docs/versioned/next/reference/environment-variables.md +2 -1
  103. package/src/docs/versioned/next/reference/lifecycle-plugin.md +11 -0
  104. package/src/docs/versioned/next/reference/mcp-server.md +17 -0
  105. package/src/docs/versioned/next/reference/syncdb.md +156 -6
  106. package/src/docs/versioned/next/reference/test.md +11 -2
  107. package/src/docs/versioned/next/reference/ui.md +331 -6
  108. package/src/docs/versioned/next/tasks/agent-ui-asks.md +213 -0
  109. package/src/docs/versioned/next/tasks/agent-ui-blocks.md +2 -1
  110. package/src/docs/versioned/next/tasks/ai-observability.md +233 -104
  111. package/src/docs/versioned/next/tasks/durable-agent-harness.md +185 -0
@@ -0,0 +1,178 @@
1
+ # UI blocks
2
+
3
+ `@terreno/blocks` checks the YAML (or JSON) document an agent uses when a chat reply is
4
+ components instead of prose. Ask schemas are in [Agent UI Asks](agent-ui-asks.md). Validate a
5
+ file locally with [the blocks how-to](../how-to/agent-ui-blocks.md).
6
+
7
+ ## Document
8
+
9
+ A reply is one mapping. Keys, when present, are in this order: `v`, `datasets`, `blocks`.
10
+
11
+ ```yaml
12
+ v: 1
13
+ blocks:
14
+ - type: text
15
+ markdown: I could not find any signups for that range.
16
+ ```
17
+
18
+ `parseBlocks` strips one surrounding ` ```yaml ` fence, parses YAML 1.2 core schema or JSON,
19
+ and rejects anchors, aliases, and explicit tags (`YAML_FEATURE_DISALLOWED`). A value that is
20
+ not a mapping with `v` is `NOT_A_DOCUMENT`. Callers display that reply with
21
+ `wrapAsTextDocument`, which builds `{v: 1, blocks: [{type: text, markdown}]}`.
22
+
23
+ ## Datasets
24
+
25
+ A dataset name matches `^[a-z][a-z0-9_]{0,63}$`. A document has at most 8 datasets.
26
+
27
+ | Source | Fields | Rule |
28
+ | --- | --- | --- |
29
+ | inline | `columns`: `{name, type}` where `type` is `string`, `number`, or `date`; `rows`: arrays | At most 500 rows and 12 columns. Each row has one value per column. `source: inline` is optional. |
30
+ | ref | `source: ref`, `id`, optional `grain` (`hour`, `day`, `week`, `month`), optional `limit` | `limit` above 1,000 is `TOO_MANY_POINTS`. Set `grain` to a coarser bucket. Column checks run when `knownDatasets` is passed. |
31
+
32
+ ## Charts and tables
33
+
34
+ | Block | Required | Optional |
35
+ | --- | --- | --- |
36
+ | `chart` | `kind`: `line`, `bar`, `area`, `donut`, and either `data` + `x` + `y` or `points` (`{label, value}`) | `id`, `title`, `legend`, `emptyText`, `height`: `sm`, `md`, `lg` |
37
+ | `table` | `data` (dataset name) | `id`, `columns` (names), `title` |
38
+
39
+ `x` is a string or date column. `y` is a number column. A table lists at most 12 columns.
40
+
41
+ ## HTML
42
+
43
+ | Block | Required | Optional |
44
+ | --- | --- | --- |
45
+ | `html` | `html` (at most 100,000 bytes) | `id`, `title`, `height`: `sm`, `md`, `lg` |
46
+
47
+ `validateBlocks` returns `HTML_DISABLED` unless `options.allowHtml` is true. A host turns that on with `uiBlocks.html`. The server sanitizes the HTML before it is stored. The client renders it only when `allowHtml` is set.
48
+
49
+ ## Callout, image, and details
50
+
51
+ | Block | Required | Optional |
52
+ | --- | --- | --- |
53
+ | `callout` | `text` | `id`, `status`: `info`, `warning`, `alert` |
54
+ | `image` | `alt`, `src` | `id` |
55
+ | `details` | `title`, `text` | `id` |
56
+
57
+ `src` is a `data:image` URL, a `file:` ref (`file:` plus an id), or an `https` URL. `https` is allowed only when the hostname is in `validateBlocks` `imageHosts` (the server option is `uiBlocks.imageHosts`). Anything else is `IMAGE_HOST_NOT_ALLOWED`. An empty list rejects every `https` image. A ref dataset `id` uses 1–80 letters, digits, underscores, or hyphens. A block or action-element `id` stays lowercase and starts with a letter. `limit` of `0` is `INVALID_TYPE` (an integer of at least 1), not `TOO_SHORT`.
58
+
59
+ Warnings do not block rendering:
60
+
61
+ | Code | When |
62
+ | --- | --- |
63
+ | `BAR_TOO_MANY_CATEGORIES` | A bar chart would draw more than 60 categories. |
64
+ | `DONUT_TOO_MANY_SLICES` | A donut chart would draw more than 8 slices. |
65
+ | `LINE_SINGLE_POINT` | A line chart has one point. |
66
+
67
+ ## Blocks
68
+
69
+ | Block | Required | Optional | Renders as |
70
+ | --- | --- | --- | --- |
71
+ | `heading` | `text` (1–200) | `size`: `sm`, `md`, `lg`, `xl`, `2xl`; `id` | `Heading` |
72
+ | `text` | `markdown` (1–4,000) | `id` | `MarkdownView` |
73
+ | `metric` | `label` (1–80), `value` (1–80) | `delta` (1–40), `trend`: `up`, `down`, `flat`, `helper` (1–120), `id` | `Card` |
74
+ | `badge` | `text` (1–80) | `status`: `info`, `error`, `warning`, `success`, `neutral`, `active`; `id` | `Badge` |
75
+ | `divider` | — | `id` | `SectionDivider` |
76
+ | `context` | `text` (1–280) | `id` | `Text` |
77
+ | `columns` | `children`: 2–4 blocks | `id` | `Box` row |
78
+ | `card` | `children`: at least 1 block | `title` (1–120), `id` | `Card` |
79
+
80
+ `columns` and `card` sit at the top level. Their children are leaf blocks. A layout block
81
+ inside another layout block is `DEPTH_EXCEEDED`. Every block counts toward the 50-block
82
+ cap, including the `card` or `columns` block itself, so a card cannot hold 50 children.
83
+
84
+ `id` matches `^[a-z][a-z0-9_]{0,63}$`.
85
+
86
+ ## Actions
87
+
88
+ An `actions` block requires `id` and `elements` (1–25). An element is a `button` or a
89
+ `segmented` control.
90
+
91
+ | Element | Required | Optional |
92
+ | --- | --- | --- |
93
+ | `button` | `id`, `text` (1–80), `action` | `variant`: `primary`, `secondary`, `outline`, `ghost`, `destructive`; `iconName` |
94
+ | `segmented` | `id`, `target` (a chart or table `id`), `options` (2–8 of `{label, data}`) | — |
95
+
96
+ `action.kind` is `reply` (`text`), `open` (`url` or `route`, exactly one), `select`
97
+ (`target` plus `data`), or `callback` (`name` plus optional `payload`). A `select` target
98
+ that is not a chart or table `id` is `SELECT_TARGET_INVALID`. When `validateBlocks` is
99
+ called with `hostActions`, a callback `name` outside that list is `UNKNOWN_HOST_ACTION`.
100
+ Omitting `hostActions` skips that check.
101
+
102
+ ## Partial parsing
103
+
104
+ `parseBlocksPartial(text)` never throws. A document that `validateBlocks` accepts returns
105
+ `{datasets, blocks, pending: false}`. A cut reply returns only the top-level blocks whose
106
+ next `- ` already arrived, drops the unfinished tail, and sets `pending: true`. `datasets`
107
+ is present only after the `blocks:` key has started and the prefix parses.
108
+
109
+ ## CLI
110
+
111
+ `terreno-blocks validate <file | ->` prints each problem as `path CODE message — fix` and
112
+ exits 1. A valid document exits 0. Warnings are printed and still exit 0.
113
+
114
+ Unknown fields fail with `UNKNOWN_KEY`. `v` must be `1` (`UNSUPPORTED_VERSION`).
115
+
116
+ ## Limits
117
+
118
+ | Limit | Value |
119
+ | --- | --- |
120
+ | Blocks in one document, including nested blocks | 50 |
121
+ | Nesting | 2 (`blocks` → `columns` or `card` → leaf) |
122
+ | `columns` children | 2–4 |
123
+ | Elements in one `actions` block | 25 |
124
+ | Text in one `text` block | 4,000 characters |
125
+ | Document version | 1 |
126
+
127
+ ## Errors
128
+
129
+ `validateBlocks` returns `{ok: true, doc, warnings}` or `{ok: false, errors, warnings}`.
130
+ Each error is `{path, code, message, fix}`, sorted by path. `warnings` lists chart
131
+ heuristics and does not fail `ok`.
132
+
133
+ | Code | Meaning |
134
+ | --- | --- |
135
+ | `COLUMN_NOT_FOUND` | A chart or table names a column the dataset does not have. |
136
+ | `COLUMN_TYPE_MISMATCH` | A column value, or a chart axis, does not match the column type. |
137
+ | `DATASET_NOT_FOUND` | A chart or table names a dataset the document does not define. |
138
+ | `DATASET_TOO_LARGE` | A dataset has more than 500 rows or 12 columns. |
139
+ | `DEPTH_EXCEEDED` | A `columns` or `card` block is nested inside another layout block. |
140
+ | `DUPLICATE_ID` | A block id, an action-element id, or a column name is used more than once. |
141
+ | `HTML_DISABLED` | An html block is present and this host has not turned HTML on. |
142
+ | `HTML_TOO_LARGE` | An html block is larger than 100,000 bytes. |
143
+ | `IMAGE_HOST_NOT_ALLOWED` | An image URL is not a `data:image` URL, a `file:` ref, or an `https` URL on an allowed host. |
144
+ | `INVALID_ENUM` | A value is not one of the allowed values. |
145
+ | `INVALID_FORMAT` | A string does not match its required format. |
146
+ | `INVALID_TYPE` | A value has the wrong type, including a numeric field below its minimum. |
147
+ | `KEY_ORDER` | Top-level keys are not in the order `v`, `datasets`, `blocks`. |
148
+ | `MISSING_REQUIRED` | A required field is missing. |
149
+ | `NOT_A_DOCUMENT` | The reply is not one YAML or JSON mapping with a `v` field. |
150
+ | `SELECT_TARGET_INVALID` | A select action names a block that is not a chart or table. |
151
+ | `ROW_ARITY_MISMATCH` | A dataset row does not have one value per column. |
152
+ | `TABLE_TOO_WIDE` | A table lists more than 12 columns. |
153
+ | `TOO_FEW` | A list has fewer items than allowed. |
154
+ | `TOO_LONG` | A string is longer than allowed. |
155
+ | `TOO_MANY` | A list has more items than allowed. |
156
+ | `TOO_MANY_BLOCKS` | The document has more than 50 blocks. |
157
+ | `TOO_MANY_POINTS` | A ref dataset `limit` is above 1,000. |
158
+ | `TOO_SHORT` | A string is empty or only whitespace. |
159
+ | `UNKNOWN_HOST_ACTION` | A callback names a host action that is not registered. |
160
+ | `UNKNOWN_KEY` | An object has a field that its schema does not define. |
161
+ | `UNSUPPORTED_VERSION` | `v` is not 1. |
162
+ | `YAML_FEATURE_DISALLOWED` | The document uses a YAML anchor, alias, or explicit tag. |
163
+
164
+ ## Exports
165
+
166
+ | Export | Role |
167
+ | --- | --- |
168
+ | `parseBlocks(text)` | Fence strip, YAML or JSON parse |
169
+ | `parseBlocksPartial(text)` | Completed top-level blocks while a reply is still streaming |
170
+ | `validateBlocks(doc, options?)` | Structure, then dataset, chart, table, action, and html lint. `options.knownDatasets` checks `ref` columns. `options.hostActions` checks callback names. `options.allowHtml` allows `html` blocks. |
171
+ | `wrapAsTextDocument(text)` | Display fallback for a non-document |
172
+ | `blocksSchema` | Zod schema |
173
+ | `blocksJsonSchema` | JSON Schema for the same structure |
174
+ | `blocksPromptSection({hostActions, allowHtml})` | System-prompt section. Limits come from `BLOCK_LIMITS`. `allowHtml` adds the `html` block. |
175
+ | `BLOCK_LIMITS` | The numbers in the table above |
176
+ | `BLOCK_ERROR_CODES` | The codes in the table above |
177
+ | `BLOCK_WARNING_CODES` | `BAR_TOO_MANY_CATEGORIES`, `DONUT_TOO_MANY_SLICES`, `LINE_SINGLE_POINT` |
178
+ | `HTML_HEIGHTS`, `CALLOUT_STATUSES` | Allowed `html` heights and `callout` statuses. `HtmlBlock`, `CalloutBlock`, `ImageBlock`, and `DetailsBlock` are type exports. |
@@ -23,6 +23,7 @@ Comprehensive guide to environment variables used across Terreno packages and ex
23
23
  | `TERRENO_TEST_MONGODB_URI` | `@terreno/test` | ❌ | auto | Yes | tooling |
24
24
  | `TERRENO_TEST_USE_MEMORY_MONGO` | `@terreno/test` | ❌ | — | No | tooling |
25
25
  | `TERRENO_TEST_USE_REPLSET` | `@terreno/test` | ❌ | — | No | tooling |
26
+ | `TERRENO_TEST_MONGO_LAUNCH_TIMEOUT_MS` | `@terreno/test` | ❌ | `60000` | No | tooling |
26
27
  | `TERRENO_TEST_USE_FIXTURE_CACHE` | `@terreno/test` | ❌ | — | No | tooling |
27
28
  | `TERRENO_TEST_CACHE_DIR` | `@terreno/test` | ❌ | — | No | tooling |
28
29
  | `BUN_TEST_DISABLE_DB` | test harness | ❌ | — | No | tooling |
@@ -222,7 +223,7 @@ There is **no** `RATE_LIMIT_ENABLED` (or similar) read by `@terreno/api`. Apps t
222
223
  |----------|---------|----------|---------|--------|-------|
223
224
  | `SLACK_WEBHOOKS` | `@terreno/api` | ❌ | — | Yes | server |
224
225
  | `SLACK_WEBHOOK_URL` | scripts | ❌ | — | Yes | server |
225
- | `SLACK_BOT_TOKEN` | `@terreno/api` `lookupSlackUserIdByEmail` | ❌ | — | Yes | server |
226
+ | `SLACK_BOT_TOKEN` | `@terreno/api` Slack Web API (`lookupSlackUserIdByEmail`, private channels, invites, `chat.postMessage`) | ❌ | — | Yes | server |
226
227
  | `GOOGLE_CHAT_WEBHOOKS` | `@terreno/api` | ❌ | — | Yes | server |
227
228
  | `GOOGLE_CHAT_WEBHOOK_URL` | scripts | ❌ | — | Yes | server |
228
229
  | `ZOOM_CHAT_WEBHOOKS` | `@terreno/api` | ❌ | — | Yes | server |
@@ -16,6 +16,17 @@ loops that invoke those stages; they are not stages and must not appear as `stag
16
16
  | `terreno-4-brew` | Roast PASS for every in-scope task + branch/evidence | pushed head + PR + product-CI trigger check + review-bot wait + attached evidence | Taste |
17
17
  | `terreno-5-taste` | PR + current state | one current-head reaction; before push: fetch latest `master` (merge only when needed), re-verify last-run failed tests locally, run root `prepush` when present (otherwise affected-package checks) in a no-context subagent, then watch CI; invoked directly, repeats bounded reactions until terminal | null or fresh Taste |
18
18
 
19
+ Pre-Grow skill (not a stage):
20
+
21
+ | Skill | Input | Primary output | Next |
22
+ | --- | --- | --- | --- |
23
+ | `terreno-prd` | app owner + conversation, prototype, repository | interview, then a published full product PRD (problem, why now, solution, user stories, success metrics and guardrails, constraints, rollout, out of scope); every metric measurable or `TBD` with its decider | Grow, with the PRD as its spec; metrics become acceptance criteria, `TBD`s become grilling questions, settled product decisions are not re-asked |
24
+
25
+ It is explicit-invoke only (`/terreno-prd`, `$terreno-prd`, `/terreno:prd`). It
26
+ interviews with Grow's grilling procedure over a product tree, is sized for one app owner
27
+ and one or two developers, and carries no implementation or testing decisions; those stay
28
+ in Grow.
29
+
19
30
  Outer loops (not stages):
20
31
 
21
32
  | Skill | What it walks | Default |
@@ -391,6 +391,23 @@ Validate a Mongoose schema against Terreno conventions.
391
391
  - Recommendations for fixes
392
392
  - Severity levels (error, warning, info)
393
393
 
394
+ ### terreno_validate_ui_blocks
395
+
396
+ Check one whole-reply YAML or JSON document. The text is the same report `terreno-blocks validate` prints: each problem is `path CODE message — fix`. A valid document with no warnings returns an empty string.
397
+
398
+ **Parameters:**
399
+
400
+ ``````typescript
401
+ {
402
+ document: string; // Required — the YAML or JSON document
403
+ }
404
+ ``````
405
+
406
+ **Returns:**
407
+
408
+ - The CLI report, including a trailing newline when there is at least one line
409
+ - An empty string when the document is valid and has no warnings
410
+
394
411
  ### terreno_bootstrap_ai_rules
395
412
 
396
413
  Scaffold AI coding assistant rules (AGENTS.md, Cursor/Windsurf rules, Copilot instructions, rulesync config).
@@ -10,6 +10,7 @@ Local-first data layer for Terreno frontends. A TinyBase `MergeableStore` on dev
10
10
  - [createSyncDb configuration](#createsyncdb-configuration)
11
11
  - [SyncDb client methods](#syncdb-client-methods)
12
12
  - [React hooks](#react-hooks)
13
+ - [Query windows](#query-windows)
13
14
  - [Codegen](#codegen)
14
15
  - [Conflict API](#conflict-api)
15
16
  - [Sync status API](#sync-status-api)
@@ -25,7 +26,8 @@ Local-first data layer for Terreno frontends. A TinyBase `MergeableStore` on dev
25
26
  - `createSyncDb`, `SyncDb`, `SyncDbConfig`, `MutateArgs`
26
27
  - `betterAuthAdapter`, `bridgeBetterAuthReactClient`, `AuthProvider`
27
28
  - `listConflicts`, `wipeLocalData`, `generateMutationId`
28
- - React (`@terreno/syncdb/react`): `SyncDbProvider`, `useEntity`, `useQuery`, `useEntityIds`, `useMutate`, `useSyncStatus`, `useConflicts`, `useSyncDebugLog`, `createCollectionHooks`
29
+ - Queries: `QueryFieldNotAllowedError`, `ListRequestError`, `WhereFilter`, `SortSpec`, `WindowQuery`, `QueryWindowState`, `QueryWindows`, `compileWhere`, `DEFAULT_WINDOW_PAGE_SIZE`
30
+ - React (`@terreno/syncdb/react`): `SyncDbProvider`, `useEntity`, `useQuery`, `useEntityIds`, `useWindowQuery`, `useMutate`, `useSyncStatus`, `useConflicts`, `useSyncDebugLog`, `createCollectionHooks`
29
31
  - CLI: `terreno-syncdb-codegen` (generates `SYNC_COLLECTIONS` + friendly hooks from OpenAPI)
30
32
  - Testing (`@terreno/syncdb/testing`): `createFakeTransport`
31
33
 
@@ -165,6 +167,7 @@ be passed to `betterAuthAdapter` directly.
165
167
  | `name` | `string` | — (required) | Persisted database name |
166
168
  | `collections` | `string[]` | — (required) | Collection names to sync (local tables + subscriptions) |
167
169
  | `windowCollections` | `string[]` | `[]` | Collections that join `{collection}\|admin`, skip snapshots/reconcile, and hydrate only known REST membership ids |
170
+ | `queryCollections` | `Array<string \| {collection, path?, fullSync?}>` | `[]` | Collections too large to sync whole (or `fullSync: true` to keep snapshot sync and add windows). No snapshot/reconcile paging; rows arrive through [query windows](#query-windows) against the `modelRouter` list endpoint (`path`, default `/{collection}`) plus live deltas on the user's normal streams. Each entry must also be in `collections` and not in `windowCollections`. |
168
171
  | `organizationIdProvider` | `() => string \| undefined` | — | Read at send time: sets `X-Organization-Id` on HTTP sync calls and `organizationId` on socket mutate payloads |
169
172
  | `authProvider` | `AuthProvider` | — (required) | `{getToken, getUserId, onAuthChange, refresh?}` |
170
173
  | `baseUrl` | `string` | — | Server origin; required unless both `transport` and `httpChannel` are injected |
@@ -200,6 +203,7 @@ be passed to `betterAuthAdapter` directly.
200
203
  | `mutate({collection, operation, id?, data?})` | Optimistic local write + durable outbox enqueue + fire-and-forget replay. Returns `{mutationId, id}`. |
201
204
  | `reconcile()` | HTTP snapshot catch-up for every known stream; runs tombstone compaction on success. Also runs automatically on (re)connect, on a rate-limited seq-jump hint, and on the periodic timer; each `sync:subscribed` confirmation additionally pages just the streams it names. |
202
205
  | `hydrateWindow({collection, ids, restRows?})` | Admin window upsert: REST rows (optional) land immediately; every requested id is also fetched from `GET /sync/entities` before this resolves so seq/deleted metadata is canonical for immediate update/delete. A `{collection}|admin` delta that lands while the fetch is in flight wins, so hydration never rewinds a row to an older seq. Unknown ids are ignored. |
206
+ | `queryWindows` | Server query windows for `queryCollections`: `fetchWindow({query, nextPage?})`, `getWindow(query)`, `retain(query)` → release, `refetchRetained()`, `pruneUnretained()`, `subscribe(cb)`, `keyFor(query)`. See [Query windows](#query-windows). |
203
207
  | `forceResync()` | Purge every known stream locally and re-bootstrap from cursor 0 (outbox/conflicts untouched). Returns `{ok, reason?, streams, purged, repaired}`. After discovery and each stream bootstrap it waits for in-flight `start`/`stop`/auth-change work, then abandons with `reason: "superseded"` when that work switched users or generations. |
204
208
  | `replayOutbox()` | Drain queued mutations for the current user now. |
205
209
  | `resolveConflict({mutationId, strategy})` | Apply `"useServer"` or `"keepMine"` to a recorded conflict. |
@@ -271,13 +275,26 @@ Subscribe to a single entity. Re-renders when that row changes. `isPending` is t
271
275
 
272
276
  ```typescript
273
277
  const todos = useQuery<Todo>("todos", {
274
- filter: (t) => !t.completed,
275
- sort: (a, b) => b.created.localeCompare(a.created),
276
- includeDeleted: false,
278
+ where: {completed: false, created: {$gte: since}},
279
+ sort: "-created",
280
+ limit: 20,
277
281
  });
282
+
283
+ // Callbacks still work and combine with `where`:
284
+ const mine = useQuery<Todo>("todos", {filter: (t) => t.ownerId === userId, sort: byCreatedDesc});
278
285
  ```
279
286
 
280
- Returns decoded entity data arrays. Tombstones excluded unless `includeDeleted: true`. Filter and sort run in JS — memoize callbacks when collections are large.
287
+ Returns decoded entity data arrays from the local store. Tombstones excluded unless `includeDeleted: true`.
288
+
289
+ | Option | Type | Description |
290
+ |--------|------|-------------|
291
+ | `where` | `WhereFilter` | Mongo-style filter: equality, dot paths, `$eq` `$ne` `$gt` `$gte` `$lt` `$lte` `$in` `$nin` `$exists`, `$and` / `$or`. An array field matches when any element equals the value. Compared by value, so inline objects are fine. Throws for operators only the server can run (`$search`, `$regex`). |
292
+ | `filter` | `(data) => boolean` | Arbitrary predicate, applied after `where`. |
293
+ | `sort` | `SortSpec \| comparator` | `"-created name"`, `{created: "descending"}`, or `(a, b) => number`. |
294
+ | `limit` | `number` | Keep the first N rows after filter and sort. |
295
+ | `includeDeleted` | `boolean` | Include tombstones. |
296
+
297
+ Filter and sort run in JS — memoize `filter`/comparator callbacks when collections are large.
281
298
 
282
299
  ### `useEntityIds(collection, options?)`
283
300
 
@@ -288,6 +305,38 @@ return ids.map((id) => <TodoRow key={id} id={id} />);
288
305
 
289
306
  Same options as `useQuery`, but returns only ordered ids with **referential stability** — the array identity changes only when membership or order changes, not on field updates. Pair with per-row `useEntity` for large lists.
290
307
 
308
+ ### `useWindowQuery(collection, options?)`
309
+
310
+ ```typescript
311
+ const {data, isLoading, isFetching, hasMore, fetchNextPage, refetch, total, error} =
312
+ useWindowQuery<Message>("messages", {
313
+ where: {threadId, deleted: false},
314
+ sort: "-created",
315
+ pageSize: 30,
316
+ });
317
+ ```
318
+
319
+ Server-filtered list over a `queryCollections` collection. See [Query windows](#query-windows).
320
+
321
+ | Option | Type | Default | Description |
322
+ |--------|------|---------|-------------|
323
+ | `where` | `WhereFilter` | — | Sent as qs list params (`status[$in][0]=open`). Fields must be in the route's `queryFields`. |
324
+ | `sort` | `SortSpec` | route default | Server `sort`; also orders the local view. |
325
+ | `pageSize` | `number` | `50` | Server `limit` per page (capped by the route's `maxLimit`). |
326
+ | `skip` | `boolean` | `false` | Do not retain or fetch (e.g. a required param is missing). |
327
+ | `liveMatch` | `boolean` | `true` | Show cached rows that newly match `where` inside the loaded range. |
328
+
329
+ | Result | Description |
330
+ |--------|-------------|
331
+ | `data` / `ids` | This window's rows in view order. |
332
+ | `isLoading` | No membership yet (first fetch in flight). Cached membership renders with `isLoading: false`. |
333
+ | `isFetching` | Any request for this window is in flight. |
334
+ | `isError` / `error` | Last request failed (cached rows still render). |
335
+ | `errorCode` | Server error code of the last failure; `"query-param-not-allowed"` means a `where` field is missing from `queryFields`. |
336
+ | `hasMore` / `total` | From the list response's `more` / `total`. |
337
+ | `fetchNextPage()` | Append the next server page. |
338
+ | `refetch()` | Reload every loaded page in one request, replacing membership. |
339
+
291
340
  ### `useMutate(collection)`
292
341
 
293
342
  ```typescript
@@ -331,7 +380,7 @@ Returns the `SyncDb` instance from context (escape hatch for imperative calls li
331
380
 
332
381
  ### `createCollectionHooks`
333
382
 
334
- Factory used by `terreno-syncdb-codegen` and by hand-written custom collections. Returns five operation hooks (`useListQuery`, `useReadQuery`, `useCreateMutation`, `useUpdateMutation`, `useDeleteMutation`). Generated SDKs rename them to friendly names (`useTodos`, `useTodo`, `useCreateTodo`, …). Mutation hooks return `[trigger]`; triggers apply locally and return `{mutationId, id}` synchronously. Optional `retries` maps to `maxAttempts` (`false` → 1, a number → that many, omitted → engine default).
383
+ Factory used by `terreno-syncdb-codegen` and by hand-written custom collections. Returns six operation hooks (`useListQuery`, `useWindowQuery`, `useReadQuery`, `useCreateMutation`, `useUpdateMutation`, `useDeleteMutation`). Generated SDKs rename them to friendly names (`useTodos`, `useTodosWindow`, `useTodo`, `useCreateTodo`, …). `useWindowQuery` works only for collections listed in `queryCollections`. Mutation hooks return `[trigger]`; triggers apply locally and return `{mutationId, id}` synchronously. Optional `retries` maps to `maxAttempts` (`false` → 1, a number → that many, omitted → engine default).
335
384
 
336
385
  ```typescript
337
386
  export const {useListQuery: useNotes, useCreateMutation: useCreateNote} =
@@ -340,6 +389,107 @@ export const {useListQuery: useNotes, useCreateMutation: useCreateNote} =
340
389
  });
341
390
  ```
342
391
 
392
+ ## Query windows
393
+
394
+ Use query windows when a collection is too large to sync whole (messages, events, audit
395
+ rows). They match what an RTK list query did — `where`, `sort`, `limit`, `page`, `more`,
396
+ `total` — but keep the data local-first and deduplicated.
397
+
398
+ ```typescript
399
+ export const syncDb = createSyncDb({
400
+ collections: ["todos", "messages"],
401
+ queryCollections: [{collection: "messages", path: "/messages"}],
402
+ // ...
403
+ });
404
+ ```
405
+
406
+ **Membership, not copies.** A window is one list query against the collection's
407
+ `modelRouter` list endpoint. Its result is an ordered id list stored in the reserved
408
+ `_queryWindows` table. Each row lands once in the collection's entity table, keyed by id.
409
+ Two windows that overlap share those rows:
410
+
411
+ - A delta, mutation, or refetch for a shared row writes the store once; every window that
412
+ lists the row re-renders from it.
413
+ - Each window lists only its own members. Window B never shows rows that only window A
414
+ fetched.
415
+
416
+ **Live membership.** When `where` is locally evaluable (see `useQuery`'s `where`), a window
417
+ also:
418
+
419
+ - hides a member whose fields stop matching (an unpinned message leaves the "pinned" window);
420
+ - shows cached rows that newly match (a live delta, or a local create) when they sort inside
421
+ the loaded range. With more pages unloaded, a row that sorts after the last loaded row
422
+ waits for `fetchNextPage()`. Without a `sort` and with more pages, only local creates are
423
+ admitted.
424
+
425
+ A full fetch also records cached rows that matched `where` locally but that the server did
426
+ not return (deleted, changed, or no longer visible while this device was offline). They stay
427
+ out of the view until a newer delta or a local edit changes them, so stale cached data never
428
+ resurrects a row.
429
+
430
+ Set `liveMatch: false` to show server membership only. Operators only the server runs
431
+ (`$search`) also fall back to server membership.
432
+
433
+ **Fetching.** The first `retain` of a window in a session fetches it; persisted membership
434
+ renders meanwhile, so windows work offline. Concurrent fetches of the same window share one
435
+ request; a refetch and a `fetchNextPage()` on one window run one after the other. Retained
436
+ windows refetch on reconnect and after `forceResync()` — query collections
437
+ skip snapshot catch-up, so this is how deltas missed offline are recovered. Windows not on
438
+ screen at that moment refetch the next time they are shown. Window fetches
439
+ started before a `stop()` or user switch are discarded.
440
+
441
+ **Seq.** List rows that carry `_syncSeq` (the `syncPlugin` field) are written with that seq.
442
+ Rows without it (a `responseHandler` that strips it) are re-read from `GET /sync/entities` so
443
+ a later mutation never sends a stale `baseVersion`. Rows with a pending outbox mutation, or
444
+ with a newer seq from a delta, are never overwritten. A list `responseHandler` must return the
445
+ same shape as the sync serializer; otherwise list rows and delta rows differ.
446
+
447
+ **Missing `queryFields`.** Every `where` field must be in the route's `queryFields`. The
448
+ server answers 400 `query-param-not-allowed` naming the field, the model, and the allowed
449
+ list. syncdb turns it into `QueryFieldNotAllowedError` (`field`, `allowedQueryFields`,
450
+ `model`), logs it with `console.error`, sets the window's `errorCode` to
451
+ `"query-param-not-allowed"`, and rejects `fetchWindow` / `refetch` / `fetchNextPage` with it.
452
+ Other list failures throw `ListRequestError` (`status`, `code`, `detail`).
453
+
454
+ ```typescript
455
+ import {QueryFieldNotAllowedError} from "@terreno/syncdb";
456
+
457
+ try {
458
+ await refetch();
459
+ } catch (error) {
460
+ if (error instanceof QueryFieldNotAllowedError) {
461
+ // error.field === "title", error.allowedQueryFields === ["completed", "created", "ownerId"]
462
+ }
463
+ }
464
+ ```
465
+
466
+ **`maxLimit` and pipelining.** `modelRouter` clamps `limit` to the route's `maxLimit` and
467
+ echoes the applied value. Window requests always send `page` (so the server never logs a
468
+ truncated unpaginated list). When a fetch needs more rows than one request returns — a
469
+ `refetch()` after several `fetchNextPage()` calls, or a `pageSize` above `maxLimit` — syncdb
470
+ learns the cap from the echoed `limit` and splits the range into sequential page requests
471
+ of `maxLimit` rows starting at the aligned page that contains the range, trimming the extra
472
+ rows, so offsets stay exact. Each pipelined fetch logs a
473
+ `console.warn` naming the row count, the cap, and the request count. Fix it by loading fewer
474
+ pages, lowering `pageSize`, or raising `maxLimit` on the route.
475
+
476
+ **Fully synced collections.** `queryCollections: [{collection: "todos", fullSync: true}]`
477
+ keeps normal snapshot sync and also enables windows, for server-side filtering, paging, and
478
+ totals over a collection that is already local. See `example-frontend/app/todo-windows.tsx`.
479
+
480
+ **Limits.**
481
+
482
+ - `refetch()` with several pages loaded reloads `pageSize × pages` rows (see below).
483
+ - Page appends dedupe ids. A delete between page loads can shift offsets and skip one row
484
+ until the next refetch.
485
+ - `null` in `where` is rejected (qs would send the string `"null"`); use `{$exists: false}`.
486
+ A top-level `{$in: []}` returns an empty window without a request; an empty array anywhere
487
+ else throws instead of silently widening the query.
488
+ - Rows stay in the entity table after their windows unmount. `start()` prunes persisted
489
+ membership of unretained windows not fetched for 7 days (`pruneStale`);
490
+ `pruneUnretained()` drops all unretained membership. Rows are dropped by leave-purge,
491
+ tombstone compaction, or a wipe.
492
+
343
493
  ## Codegen
344
494
 
345
495
  `terreno-syncdb-codegen` is a bin of `@terreno/syncdb`. It reads OpenAPI, discovers list operations with `x-terreno-sync`, and writes typed hooks plus `SYNC_COLLECTIONS`.
@@ -79,10 +79,14 @@ Defaults: `TOKEN_SECRET`, `TOKEN_ISSUER`, `REFRESH_TOKEN_SECRET`, `SESSION_SECRE
79
79
  |----------|--------|
80
80
  | `TERRENO_TEST_USE_MEMORY_MONGO` | When `"true"`, `registerSimpleMongoPreload` starts `mongodb-memory-server` if `TERRENO_TEST_MONGODB_URI` is unset. |
81
81
  | `TERRENO_TEST_MONGODB_URI` | External MongoDB URI. Takes priority over in-memory server in `startMongoServer` and `registerSimpleMongoPreload`. |
82
+ | `TERRENO_TEST_MONGO_LAUNCH_TIMEOUT_MS` | Per-mongod start timeout for in-memory servers started by `startMongoServer`. Default `60000` (mongodb-memory-server's own default is 10s). Startup is tried twice. |
82
83
  | `BUN_TEST_DISABLE_DB` | When `"true"`, `registerBackendPreload` skips all Mongo `beforeAll`/`afterAll` hooks (no DB startup). |
83
84
 
84
85
  `startMongoServer` also publishes the resolved URI to `TERRENO_TEST_MONGO_URI` and honors `TERRENO_TEST_USE_REPLSET=true` for replica-set memory servers.
85
86
 
87
+ If `registerBackendPreload` cannot start Mongo, every later test fails at once with
88
+ `MongoDB test server failed to start` instead of waiting on mongoose command buffering.
89
+
86
90
  ## Exported helpers
87
91
 
88
92
  | Export | Description |
@@ -92,6 +96,7 @@ Defaults: `TOKEN_SECRET`, `TOKEN_ISSUER`, `REFRESH_TOKEN_SECRET`, `SESSION_SECRE
92
96
  | `setTerrenoTestEnv` | Apply and validate auth test env vars. |
93
97
  | `startMongoServer` / `stopMongoServer` | Start or stop shared in-memory Mongo and connect mongoose. |
94
98
  | `getMongoServerUri` | Resolved URI after `startMongoServer`. |
99
+ | `resolveMongoLaunchTimeoutMs` | In-memory mongod launch timeout from `TERRENO_TEST_MONGO_LAUNCH_TIMEOUT_MS` (integer ≥ 1000), default 60000. |
95
100
  | `getBaseServer` | Build an Express app for supertest from route registrars. |
96
101
  | `authAsUser` | Login helper returning auth headers for supertest. |
97
102
  | `createMongoTestCache` | Fixture-cache controller for fast `@terreno/api` tests. |
@@ -112,8 +117,12 @@ Defaults: `TOKEN_SECRET`, `TOKEN_ISSUER`, `REFRESH_TOKEN_SECRET`, `SESSION_SECRE
112
117
  Package CI uses `scripts/check-coverage.ts` (`bun run test:coverage`, default 95%
113
118
  functions and lines) as the live gate. Bun 1.4.2+ may exit 1 on bunfig
114
119
  `coverageThreshold` before isolated LCOV merges; the script continues when tests
115
- reported `0 fail` and then enforces 95% on the merged report. Dedicated CircleCI
116
- jobs (`api-ci`, `ai-ci`,
120
+ reported `0 fail` and then enforces 95% on the merged report. The blocks
121
+ coverage pass sets `--max-concurrency=1`, because Bun's default 20-way run
122
+ exits 0 on a 2-vCPU worker before it prints the `All files` row. When that
123
+ row is still missing after one retry, the script uses `coverage/lcov.info`
124
+ only if it lists every non-test source file under `src/`. Dedicated CircleCI
125
+ jobs (`api-ci`, `ai-ci`, `blocks-ci`,
117
126
  `rtk-ci`, `ui-ci`, `syncdb-ci`, `comms-ci`, `mcp-server-ci`, `admin-spa-ci`) run
118
127
  that script. Published packages without a dedicated workflow
119
128
  (`admin-backend`, `admin-frontend`, `api-health`, `feature-flags`, `@terreno/test`)