@fugood/bricks-project 2.24.2 → 2.24.4

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 (45) hide show
  1. package/compile/index.ts +195 -13
  2. package/package.json +2 -2
  3. package/package.json.bak +2 -2
  4. package/skills/bricks-ctor/SKILL.md +3 -1
  5. package/skills/bricks-ctor/rules/animation.md +3 -2
  6. package/skills/bricks-ctor/rules/buttress.md +97 -8
  7. package/skills/bricks-ctor/rules/verification-toolchain.md +170 -0
  8. package/skills/bricks-design/SKILL.md +160 -45
  9. package/skills/bricks-design/references/architecture-truths.md +125 -0
  10. package/skills/bricks-design/references/avoiding-complexity.md +91 -0
  11. package/skills/bricks-design/references/design-critique.md +195 -0
  12. package/skills/bricks-design/references/design-languages.md +265 -0
  13. package/skills/bricks-design/references/performance.md +116 -0
  14. package/skills/bricks-design/references/presentation-and-slideshow.md +137 -0
  15. package/skills/bricks-design/references/translating-inputs.md +152 -0
  16. package/skills/bricks-design/references/variations-and-tweaks.md +124 -0
  17. package/skills/bricks-design/references/when-the-brief-is-branded.md +284 -0
  18. package/skills/bricks-design/references/when-the-brief-is-vague.md +85 -0
  19. package/skills/bricks-design/references/workflow.md +134 -0
  20. package/skills/bricks-ux/SKILL.md +120 -0
  21. package/skills/bricks-ux/references/accessibility.md +162 -0
  22. package/skills/bricks-ux/references/flow-states.md +175 -0
  23. package/skills/bricks-ux/references/interaction-archetypes.md +189 -0
  24. package/skills/bricks-ux/references/monitoring-screens.md +153 -0
  25. package/skills/bricks-ux/references/pressable-composition.md +126 -0
  26. package/skills/bricks-ux/references/user-journey.md +168 -0
  27. package/skills/bricks-ux/references/ux-critique.md +256 -0
  28. package/tools/_git-author.ts +10 -2
  29. package/tools/_last-pushed-commit.ts +28 -0
  30. package/tools/deploy.ts +15 -0
  31. package/tools/preview-main.mjs +18 -5
  32. package/tools/pull.ts +91 -16
  33. package/tools/update-config.ts +118 -0
  34. package/types/animation.ts +16 -5
  35. package/types/automation.ts +1 -0
  36. package/types/bricks/Image.ts +12 -0
  37. package/types/data-calc.ts +1 -0
  38. package/types/data.ts +1 -1
  39. package/types/generators/Assistant.ts +18 -0
  40. package/types/generators/LlmGgml.ts +1 -0
  41. package/types/generators/LlmMlx.ts +1 -0
  42. package/types/generators/SpeechToTextGgml.ts +1 -0
  43. package/types/subspace.ts +1 -1
  44. package/utils/data.ts +1 -1
  45. package/skills/bricks-design/LICENSE.txt +0 -180
package/compile/index.ts CHANGED
@@ -301,6 +301,162 @@ const animationTypeMap = {
301
301
  AnimationTimingConfig: 'timing',
302
302
  AnimationSpringConfig: 'spring',
303
303
  AnimationDecayConfig: 'decay',
304
+ } as const
305
+
306
+ type CompiledAnimationType = (typeof animationTypeMap)[keyof typeof animationTypeMap]
307
+ type WarningMetadata = Record<string, unknown>
308
+
309
+ const animationProperties = new Set([
310
+ 'transform.translateX',
311
+ 'transform.translateY',
312
+ 'transform.scale',
313
+ 'transform.scaleX',
314
+ 'transform.scaleY',
315
+ 'transform.rotate',
316
+ 'transform.rotateX',
317
+ 'transform.rotateY',
318
+ 'opacity',
319
+ ])
320
+
321
+ const animationComposeTypes = new Set(['parallel', 'sequence'])
322
+ const springConfigFamilies = [
323
+ ['stiffness', 'damping', 'mass'],
324
+ ['tension', 'friction'],
325
+ ['bounciness', 'speed'],
326
+ ]
327
+ const springConfigFamilyKeys = new Set(springConfigFamilies.flat())
328
+
329
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
330
+ Boolean(value) && typeof value === 'object' && !Array.isArray(value)
331
+
332
+ const hasDefinedConfigValue = (config: Record<string, unknown>, key: string) =>
333
+ config[key] !== undefined
334
+
335
+ const assertConfigValue = (
336
+ config: Record<string, unknown>,
337
+ key: string,
338
+ errorReference: string,
339
+ ) => {
340
+ if (!hasDefinedConfigValue(config, key)) {
341
+ throw new Error(`Invalid animation config ${errorReference}: missing "${key}"`)
342
+ }
343
+ }
344
+
345
+ const assertAnimationProperty = (property: unknown, errorReference: string) => {
346
+ if (typeof property !== 'string' || !animationProperties.has(property)) {
347
+ throw new Error(
348
+ `Invalid animation property${errorReference ? ` ${errorReference}` : ''}: ${String(
349
+ property,
350
+ )}`,
351
+ )
352
+ }
353
+ return property
354
+ }
355
+
356
+ const getAnimationType = (config: unknown, errorReference: string): CompiledAnimationType => {
357
+ if (!isRecord(config)) {
358
+ throw new Error(`Invalid animation config ${errorReference}: config must be an object`)
359
+ }
360
+
361
+ const animationType = animationTypeMap[config.__type as keyof typeof animationTypeMap]
362
+ if (!animationType) {
363
+ throw new Error(`Invalid animation config type ${errorReference}: ${String(config.__type)}`)
364
+ }
365
+ return animationType
366
+ }
367
+
368
+ const assertAnimationComposeType = (composeType: unknown, errorReference: string) => {
369
+ if (typeof composeType !== 'string' || !animationComposeTypes.has(composeType)) {
370
+ throw new Error(`Invalid animation compose type ${errorReference}: ${String(composeType)}`)
371
+ }
372
+ return composeType
373
+ }
374
+
375
+ const pickDefinedConfigValues = (config: Record<string, unknown>, keys: string[]) =>
376
+ keys.reduce((acc, key) => {
377
+ if (hasDefinedConfigValue(config, key)) acc[key] = config[key]
378
+ return acc
379
+ }, {})
380
+
381
+ const getDefinedConfigKeys = (config: Record<string, unknown>, keys: string[]) =>
382
+ keys.filter((key) => hasDefinedConfigValue(config, key))
383
+
384
+ const formatWarningMetadata = (metadata: WarningMetadata = {}) =>
385
+ Object.entries(metadata)
386
+ .filter(([, value]) => value !== undefined && value !== null && value !== '')
387
+ .map(([key, value]) => `${key}: ${String(value)}`)
388
+ .join(', ')
389
+
390
+ const formatWarningReference = (metadata?: WarningMetadata) => {
391
+ const metadataText = formatWarningMetadata(metadata)
392
+ return metadataText ? ` [${metadataText}]` : ''
393
+ }
394
+
395
+ const normalizeSpringConfig = (
396
+ config: Record<string, unknown>,
397
+ errorReference: string,
398
+ warningMetadata?: WarningMetadata,
399
+ ): Record<string, unknown> => {
400
+ assertConfigValue(config, 'toValue', errorReference)
401
+
402
+ const usedFamilies = springConfigFamilies.filter((keys) =>
403
+ keys.some((key) => hasDefinedConfigValue(config, key)),
404
+ )
405
+ if (usedFamilies.length <= 1) return config
406
+
407
+ const configWithoutSpringFamily = Object.entries(config).reduce((acc, [key, value]) => {
408
+ if (!springConfigFamilyKeys.has(key)) acc[key] = value
409
+ return acc
410
+ }, {})
411
+
412
+ // Match runtime normalization: physical spring values are most explicit,
413
+ // otherwise preserve BRICKS' historical tension/friction controls.
414
+ const resolvedFamily =
415
+ usedFamilies.find((keys) => keys.includes('stiffness')) ||
416
+ usedFamilies.find((keys) => keys.includes('tension')) ||
417
+ usedFamilies[0]
418
+ const resolvedFamilyKeys = getDefinedConfigKeys(config, resolvedFamily)
419
+ const droppedFamilyKeys = usedFamilies
420
+ .filter((keys) => keys !== resolvedFamily)
421
+ .flatMap((keys) => getDefinedConfigKeys(config, keys))
422
+
423
+ console.warn(
424
+ `[Warning] Resolved animation spring config${formatWarningReference(
425
+ warningMetadata,
426
+ )}: using ${resolvedFamilyKeys.join('/')}, dropping ${droppedFamilyKeys.join('/')}`,
427
+ )
428
+
429
+ return {
430
+ ...configWithoutSpringFamily,
431
+ ...pickDefinedConfigValues(config, resolvedFamily),
432
+ }
433
+ }
434
+
435
+ const compileAnimationConfig = (
436
+ animationType: CompiledAnimationType,
437
+ config: unknown,
438
+ errorReference: string,
439
+ warningMetadata?: WarningMetadata,
440
+ ) => {
441
+ if (!isRecord(config)) {
442
+ throw new Error(`Invalid animation config ${errorReference}: config must be an object`)
443
+ }
444
+
445
+ const compiledConfig = compileProperty(omit(config, '__type'), errorReference)
446
+
447
+ if (!isRecord(compiledConfig)) {
448
+ throw new Error(`Invalid animation config ${errorReference}: config must compile to an object`)
449
+ }
450
+
451
+ if (animationType === 'timing') {
452
+ assertConfigValue(compiledConfig, 'toValue', errorReference)
453
+ } else if (animationType === 'spring') {
454
+ return normalizeSpringConfig(compiledConfig, errorReference, warningMetadata)
455
+ } else if (animationType === 'decay') {
456
+ assertConfigValue(compiledConfig, 'velocity', errorReference)
457
+ }
458
+
459
+ return compiledConfig
304
460
  }
305
461
 
306
462
  const compileFrame = (frame: Canvas['items'][number]['frame']) => ({
@@ -485,6 +641,7 @@ const compileAutomationTest = (
485
641
 
486
642
  return {
487
643
  id: testId,
644
+ alias: test.alias,
488
645
  title: test.title,
489
646
  hide_short_ref: test.hideShortRef,
490
647
  timeout: test.timeout,
@@ -629,39 +786,63 @@ export const compile = async (app: Application) => {
629
786
  `(animation index: ${animationIndex}, subspace: ${subspaceId})`,
630
787
  )
631
788
 
632
- if (animation.__typename === 'Animation') {
789
+ const animationTypename = animation.__typename
790
+ if (animationTypename === 'Animation') {
633
791
  const animationDef = animation as AnimationDef
792
+ const animationErrorReference = `(animation: ${animationId}, subspace ${subspaceId})`
793
+ const animationWarningMetadata = {
794
+ animationIndex,
795
+ animationTitle: animationDef.title,
796
+ animationAlias: animationDef.alias,
797
+ animationProperty: animationDef.property,
798
+ subspaceIndex,
799
+ subspaceTitle: subspace.title,
800
+ }
801
+ const animationType = getAnimationType(animationDef.config, animationErrorReference)
634
802
  map[animationId] = {
635
803
  alias: animationDef.alias,
636
804
  title: animationDef.title,
637
805
  description: animationDef.description,
638
806
  hide_short_ref: animationDef.hideShortRef,
639
807
  animationRunType: animationDef.runType,
640
- property: animationDef.property,
641
- type: animationTypeMap[animationDef.config.__type],
642
- config: compileProperty(
643
- omit(animationDef.config, '__type'),
644
- `(animation: ${animationId}, subspace ${subspaceId})`,
808
+ property: assertAnimationProperty(animationDef.property, animationErrorReference),
809
+ type: animationType,
810
+ config: compileAnimationConfig(
811
+ animationType,
812
+ animationDef.config,
813
+ animationErrorReference,
814
+ animationWarningMetadata,
645
815
  ),
646
816
  }
647
- } else if (animation.__typename === 'AnimationCompose') {
817
+ } else if (animationTypename === 'AnimationCompose') {
648
818
  const animationDef = animation as AnimationComposeDef
819
+ const animationErrorReference = `(animation: ${animationId}, subspace ${subspaceId})`
649
820
  map[animationId] = {
650
821
  alias: animationDef.alias,
651
822
  title: animationDef.title,
652
823
  description: animationDef.description,
653
824
  hide_short_ref: animationDef.hideShortRef,
654
825
  animationRunType: animationDef.runType,
655
- compose_type: animationDef.composeType,
826
+ compose_type: assertAnimationComposeType(
827
+ animationDef.composeType,
828
+ animationErrorReference,
829
+ ),
656
830
  item_list: animationDef.items.map((item, index) => {
657
831
  const innerAnimation = item()
658
- if (!innerAnimation?.id)
659
- throw new Error(
660
- `Invalid animation index: ${index} (animation: ${innerAnimation.id}, subspace ${subspaceId})`,
661
- )
662
- return { animation_id: innerAnimation.id }
832
+ const innerAnimationId = assertEntryId(
833
+ innerAnimation?.id,
834
+ 'ANIMATION',
835
+ `(animation item index: ${index}, animation: ${animationId}, subspace ${subspaceId})`,
836
+ )
837
+ return { animation_id: innerAnimationId }
663
838
  }),
664
839
  }
840
+ } else {
841
+ throw new Error(
842
+ `Invalid animation typename (animation: ${animationId}, subspace ${subspaceId}): ${String(
843
+ animationTypename,
844
+ )}`,
845
+ )
665
846
  }
666
847
  return map
667
848
  }, {}),
@@ -1029,6 +1210,7 @@ export const compile = async (app: Application) => {
1029
1210
  )
1030
1211
 
1031
1212
  const calc: any = {
1213
+ alias: dataCalc.alias,
1032
1214
  title: dataCalc.title,
1033
1215
  description: dataCalc.description,
1034
1216
  hide_short_ref: dataCalc.hideShortRef,
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@fugood/bricks-project",
3
- "version": "2.24.2",
3
+ "version": "2.24.4",
4
4
  "main": "index.ts",
5
5
  "scripts": {
6
6
  "typecheck": "tsc --noEmit",
7
7
  "build": "bun scripts/build.js"
8
8
  },
9
9
  "dependencies": {
10
- "@fugood/bricks-cli": "^2.24.2",
10
+ "@fugood/bricks-cli": "^2.24.4",
11
11
  "@huggingface/gguf": "^0.3.2",
12
12
  "@iarna/toml": "^3.0.0",
13
13
  "@modelcontextprotocol/sdk": "^1.15.0",
package/package.json.bak CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@fugood/bricks-ctor",
3
- "version": "2.24.2",
3
+ "version": "2.24.4",
4
4
  "main": "index.ts",
5
5
  "scripts": {
6
6
  "typecheck": "tsc --noEmit",
7
7
  "build": "bun scripts/build.js"
8
8
  },
9
9
  "dependencies": {
10
- "@fugood/bricks-cli": "^2.24.2",
10
+ "@fugood/bricks-cli": "^2.24.4",
11
11
  "@huggingface/gguf": "^0.3.2",
12
12
  "@iarna/toml": "^3.0.0",
13
13
  "@modelcontextprotocol/sdk": "^1.15.0",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: bricks-ctor
3
- description: Advanced BRICKS configuration knowledge. Covers Standby Transition, Automations (E2E testing), Data Calculation (JS sandbox libraries), Local Sync, Remote Data Bank, Media Flow, and Buttress (remote inference). Triggers on multi-device sync, cloud data, media assets, AI offloading, E2E testing, or canvas transitions.
3
+ description: Advanced BRICKS configuration knowledge. Covers Standby Transition, Automations (E2E testing), Data Calculation (JS sandbox libraries), Local Sync, Remote Data Bank, Media Flow, Buttress (remote inference), and the verification toolchain (compile/preview MCP, on-device DevTools, definition-of-done gate). Triggers on multi-device sync, cloud data, media assets, AI offloading, E2E testing, canvas transitions, or verifying project work before declaring done.
4
4
  ---
5
5
 
6
6
  # BRICKS Ctor - Advanced Features
@@ -20,6 +20,7 @@ This skill covers advanced BRICKS features not in the main project instructions.
20
20
  | [Remote Data Bank](rules/remote-data-bank.md) | Cloud data sync and API access |
21
21
  | [Media Flow](rules/media-flow.md) | Media asset management |
22
22
  | [Buttress](rules/buttress.md) | Remote inference for AI generators |
23
+ | [Verification Toolchain](rules/verification-toolchain.md) | Definition of done, compile/preview MCP, on-device DevTools, Path 1/2/3 decision rule |
23
24
 
24
25
  ## Quick Reference
25
26
 
@@ -30,3 +31,4 @@ This skill covers advanced BRICKS features not in the main project instructions.
30
31
  - **AI offloading**: See [Buttress](rules/buttress.md) for GPU server delegation
31
32
  - **E2E testing**: See [Automations](rules/automations.md) for test automation
32
33
  - **Enter animations**: See [Standby Transition](rules/standby-transition.md) for canvas transitions
34
+ - **Verification before done**: See [Verification Toolchain](rules/verification-toolchain.md) for the definition-of-done gate and Path 1/2/3 decision rule
@@ -41,12 +41,13 @@ const bounce: AnimationDef = {
41
41
  toValue: 1,
42
42
  friction: 7,
43
43
  tension: 40,
44
- speed: 12,
45
- bounciness: 8,
46
44
  },
47
45
  }
48
46
  ```
49
47
 
48
+ Use one spring parameter family per config: `tension/friction`, `speed/bounciness`, or
49
+ `stiffness/damping/mass`.
50
+
50
51
  ### Decay Animation
51
52
  Velocity-based deceleration animation.
52
53
 
@@ -20,16 +20,26 @@ When mobile devices or embedded systems lack hardware for local AI inference (LL
20
20
 
21
21
  ## Client Configuration
22
22
 
23
- In generator properties, configure Buttress settings:
23
+ In generator properties, configure `buttressConnectionSettings`:
24
24
 
25
25
  | Setting | Description |
26
26
  |---------|-------------|
27
- | `Enabled` | Toggle Buttress offloading |
28
- | `URL` | Buttress server URL (e.g., `http://192.168.1.100:2080`) |
29
- | `Fallback Type` | Action if Buttress unavailable: `use-local` or `no-op` |
30
- | `Strategy` | Execution preference |
27
+ | `enabled` | Toggle Buttress offloading |
28
+ | `autoDiscoverType` | `auto` (LAN auto-discovery, recommended) or `manual` (typed URL) |
29
+ | `url` | Server URL (`manual` mode only — leave empty for `auto`) |
30
+ | `fallbackType` | If Buttress is unreachable: `use-local` runs locally, `no-op` blocks the load |
31
+ | `strategy` | Execution preference (see below) |
31
32
 
32
- ### Strategy Options
33
+ The launcher provides the access token automatically — there is **no user-facing access-token setting**. In `auto` mode the launcher discovers servers bound to the device's workspace and uses its workspace-scoped JWT; in `manual` mode it sends the same JWT only when the typed URL belongs to a server bound to the same workspace (verified against the server's `/buttress/info`).
34
+
35
+ ### `autoDiscoverType` options
36
+
37
+ | Mode | When to use | What the device does |
38
+ |------|-------------|----------------------|
39
+ | `auto` | LAN deployment with a workspace-bound buttress-server | Listens for UDP announcements and picks the strongest server bound to its workspace; reconnects automatically when the workspace flips |
40
+ | `manual` | Internet-reachable server, mixed networks, or a public/unbound server | Connects to the typed `url` directly; sends a token only when the server is bound to the same workspace |
41
+
42
+ ### Strategy options
33
43
 
34
44
  | Strategy | Description |
35
45
  |----------|-------------|
@@ -37,7 +47,11 @@ In generator properties, configure Buttress settings:
37
47
  | `prefer-buttress` | Use Buttress if available, fallback to local |
38
48
  | `prefer-best` | Auto-select based on capability comparison |
39
49
 
40
- ## Generator Configuration Example
50
+ ## Generator Configuration Examples
51
+
52
+ ### Auto-discovery (recommended)
53
+
54
+ Workspace-bound launcher + workspace-bound buttress-server on the same LAN. No URL needed — discovery picks the highest-scoring server that runs the requested backend.
41
55
 
42
56
  ```typescript
43
57
  import { makeId } from 'bricks-ctor'
@@ -53,7 +67,7 @@ const llmGenerator: GeneratorLLM = {
53
67
  contextSize: 8192,
54
68
  buttressConnectionSettings: {
55
69
  enabled: true,
56
- url: 'http://192.168.1.100:2080',
70
+ autoDiscoverType: 'auto',
57
71
  fallbackType: 'use-local',
58
72
  strategy: 'prefer-best',
59
73
  },
@@ -63,6 +77,44 @@ const llmGenerator: GeneratorLLM = {
63
77
  }
64
78
  ```
65
79
 
80
+ ### Manual URL
81
+
82
+ Use when the server isn't on the launcher's broadcast domain (cross-subnet, internet, behind a reverse proxy) or you want to point at a specific public/unbound server.
83
+
84
+ ```typescript
85
+ buttressConnectionSettings: {
86
+ enabled: true,
87
+ autoDiscoverType: 'manual',
88
+ url: 'http://192.168.1.100:2080',
89
+ fallbackType: 'use-local',
90
+ strategy: 'prefer-best',
91
+ }
92
+ ```
93
+
94
+ > ⚠️ With `fallbackType: 'no-op'` the generator refuses to load locally if Buttress is unreachable. Use `'use-local'` whenever the device can also run the model standalone.
95
+
96
+ ## Integrating a discovered buttress-server
97
+
98
+ The ctor-desktop "Local Devices" panel (and the `bricks buttress scan` CLI) can hand you a server's identity, workspace, and announced generator caps. When the user asks to integrate one:
99
+
100
+ 1. **Confirm workspace match.** The discovery message says whether the server's workspace matches this project's. If it doesn't, do NOT add the integration — instruct the user to run `bricks buttress unbind && bricks buttress bind` from this workspace's owner CLI on the server host. The launcher won't trust a cross-workspace server.
101
+
102
+ 2. **Map announced types to generator templates.** For each `generators[].type` in the announce, target the matching templateKey (create the generator if absent):
103
+
104
+ | Announced type | templateKey | Default model |
105
+ |----------------|------------------------------|---------------|
106
+ | `ggml-llm` | `GENERATOR_LLM` | `ggml-org/gemma-3-12b-it-qat-GGUF` (≥20 GB usable) or `ggml-org/gpt-oss-20b-GGUF` (~12 GB usable) |
107
+ | `mlx-llm` | `GENERATOR_MLX_LLM` | `mlx-community/Qwen3-4B-4bit` (or a larger 4-bit quant if `usableBytes` allows) |
108
+ | `ggml-stt` | `GENERATOR_SPEECH_INFERENCE` | `BricksDisplay/whisper-ggml` (filename `ggml-small-q8_0.bin`) |
109
+
110
+ Pick a model that fits the announced `usableBytes`. Set `contextSize` to match.
111
+
112
+ 3. **Use the canonical auto-discovery config** from "Auto-discovery (recommended)" above. Don't switch to manual mode just because the server is on the LAN — auto mode picks the workspace-bound server automatically and rebinds when the device's workspace flips.
113
+
114
+ ### Real-device caveat
115
+
116
+ Buttress LAN auto-discovery uses native UDP via `react-native-jsi-udp`. The iOS Simulator silently fails to bind UDP, so auto mode is a no-op there. With `fallbackType: 'use-local'` the generator falls back to local inference in the simulator — dev/test stays functional. **Validate the buttress path itself only when deploying to a real Foundation device; don't gate the build on simulator validation.**
117
+
66
118
  ## Server Setup
67
119
 
68
120
  ### Requirements
@@ -98,6 +150,43 @@ bricks-buttress --config ./config.toml
98
150
  |----------|-------------|
99
151
  | `HF_TOKEN` | Hugging Face token for model downloads |
100
152
  | `ENABLE_OPENAI_COMPAT_ENDPOINT` | Set to `1` for OpenAI-compatible API |
153
+ | `BRICKS_BUTTRESS_STATE_DIR` | Override the workspace state directory (default `~/.bricks-cli/buttress`) |
154
+
155
+ ## Bind the Server to a Workspace
156
+
157
+ To use `autoDiscoverType: 'auto'` — and to require workspace-scoped JWT auth — pair the buttress-server with a BRICKS workspace using the `bricks buttress` CLI commands. An unbound server stays in legacy public mode and accepts any LAN client.
158
+
159
+ ### One-time bind
160
+
161
+ ```bash
162
+ # Log into the cloud bricks-server with the workspace owner's account
163
+ bricks auth login
164
+
165
+ # Pair the buttress-server running on this machine with the active workspace
166
+ bricks buttress bind
167
+
168
+ # Restart bricks-buttress so it picks up the new state.json
169
+ ```
170
+
171
+ `bricks buttress bind` writes `~/.bricks-cli/buttress/state.json` containing the workspace id, the issuer's Ed25519 public key, and a stable `serverId` (defaults to `buttress-<machineId>`). Override location with `--state-dir` or `$BRICKS_BUTTRESS_STATE_DIR`. For headless setups, use `bricks buttress bind --print` to emit state.json to stdout.
172
+
173
+ ### Verify and manage
174
+
175
+ ```bash
176
+ # Show local binding + workspace-side bound list
177
+ bricks buttress status
178
+
179
+ # Discover buttress-servers on the LAN (with version, auth state, generator caps)
180
+ bricks buttress scan
181
+
182
+ # Issue a long-lived workspace access token (CI / ctor agents that already hold a workspace token)
183
+ bricks buttress issue-token --ttl 86400
184
+
185
+ # Detach this server from the workspace
186
+ bricks buttress unbind
187
+ ```
188
+
189
+ After binding, launchers in the same workspace will auto-discover this server; manual-mode generators will only send their access token if the server's reported workspace matches their own.
101
190
 
102
191
  ## Server Configuration (TOML)
103
192
 
@@ -0,0 +1,170 @@
1
+ # Verification Toolchain
2
+
3
+ Three runtime targets, one decision rule. Verify against the cheapest path that proves what you need to prove.
4
+
5
+ ## Definition of done — the hard gate
6
+
7
+ Before the agent is allowed to claim work is complete, **all** of the following must be true:
8
+
9
+ 1. **Compile clean.** Latest source passes `compile` with no errors.
10
+ 2. **Every Canvas screenshot captured.** A rendered screenshot of every Canvas in the deliverable, captured via `preview` MCP (`responseImage: true`) at the target hardware resolution and orientation. Canvases gated behind events are reached via Automation runs (`testId` / `testTitleLike` with `brick_press` / `wait_until_canvas_change` cases). Single-Canvas Subspaces still require a captured screenshot.
11
+ 3. **Every screenshot reviewed by the agent.** The agent must read each captured screenshot back through the host's image-reading capability — saving the file is not the same as seeing it. The model that produced the Canvas must also have seen the rendered result, or the verification gate has not passed.
12
+ 4. **Reference comparison report.** If the user supplied any reference material (Figma / website / HTML / screenshot / PDF / brand book / competitor), produce a short delta report per Canvas:
13
+ - What matches the reference.
14
+ - What intentionally diverges, and why (deployment constraint, BRICKS-runtime semantic, system commitment).
15
+ - What unintentionally diverges, and the planned fix.
16
+ 5. **Path appropriate to deployment executed.** Path 1 by default. Path 2 (on-device) when the deployment depends on real hardware behaviour, or whenever the brief touches payment, identity, peripherals, or LocalSync.
17
+ 6. **Console clean.** No 404s on media, no Data-key-undefined warnings, no Generator init failures, no React-style warnings — every console line is either zero or accept-with-a-comment.
18
+
19
+ What does **not** count as done:
20
+
21
+ - A single hero-Canvas screenshot.
22
+ - "Looks roughly like the reference" with no per-Canvas comparison.
23
+ - "Compiled successfully" with no rendered preview.
24
+ - A claim of done with no screenshots in evidence.
25
+ - Captured screenshots saved to disk but never read back by the agent.
26
+
27
+ If any item is unmet, the work is mid-iteration. Say so explicitly to the user; offer a precise list of what remains.
28
+
29
+ ## Path 1 — Electron preview (no device required)
30
+
31
+ The default loop. Always available; deterministic; no device wear; safe for side-effecting flows because nothing real fires.
32
+
33
+ ### `bricks-ctor` MCP — `compile`
34
+
35
+ Typecheck + compile the project. Gate every iteration on this; everything below assumes a clean compile.
36
+
37
+ Agent invocation: call the MCP tool `compile` exposed by the `bricks-ctor` MCP server registered for the project. No arguments.
38
+
39
+ ### `bricks-ctor` MCP — `preview`
40
+
41
+ Launches headless Electron, takes a screenshot, optionally runs a named Automation test by id or partial title.
42
+
43
+ Arguments:
44
+ - `delay` (ms before screenshot) — default 3000. Increase if Standby Transitions are still in flight.
45
+ - `width`, `height` — screenshot dimensions in px.
46
+ - `responseImage: true` — return base64 jpeg as MCP image content (recommended for visual sanity).
47
+ - `testId` or `testTitleLike` — run a project Automation before the screenshot. Use `testTitleLike` for fuzzy matches (case-insensitive partial title).
48
+
49
+ Returns text log + (if `responseImage`) base64 image content.
50
+
51
+ ### `bun preview` (project script)
52
+
53
+ Sustained dev session — watches `subspaces/`, recompiles on save, exposes CDP at `localhost:19852` (configurable via `--cdp-port`), writes `.bricks/devtools.json` with port/pid for downstream tooling.
54
+
55
+ Useful flags:
56
+ - `--screenshot` + `--screenshot-delay/width/height/path` — drive screenshot capture without keeping the window open.
57
+ - `--show-menu` — surface the Electron menu (debugging).
58
+ - `--test-id` / `--test-title-like` — run an Automation in this preview.
59
+ - `--no-keep-open` — exit after one screenshot.
60
+ - `--no-cdp` — disable CDP server (rare; default is to expose it).
61
+ - `--clear-cache` — reset cached state between runs.
62
+
63
+ For ad-hoc CDP inspection against this local preview, connect any CDP client to `localhost:19852` — Chrome DevTools front-end works directly. For an agent-friendly CLI over CDP (screenshot, brick tree/query, input emulation, storage reads, runtime eval, network capture), the `bricks-cli` skill documents the `bricks devtools` command surface — read that skill if it is installed in this workspace. If it is not installed, run `bricks --help` and `bricks devtools --help`; the CLI's own help output is authoritative.
64
+
65
+ ### Project Automations
66
+
67
+ E2E tests authored in TypeScript inside the project (`AutomationTest` / `TestCase`). Test cases include:
68
+
69
+ - `brick_press` — synthesize a press on a Brick.
70
+ - `wait_until_canvas_change` — assert navigation.
71
+ - `assert_property` — assert a Data value equals expected.
72
+ - `wait_property_update` — wait for a Data value to change.
73
+ - `match_screenshot` — visual regression with stored reference image.
74
+
75
+ Trigger types: `launch` (runs on app start), `anytime` (manual), `cron` (scheduled).
76
+
77
+ Important: the automation map id must be `'AUTOMATION_MAP_DEFAULT'` (not a generated id) — the preview test runner reads from `automationMap['AUTOMATION_MAP_DEFAULT']?.map`.
78
+
79
+ Run a single test from the agent: `bricks-ctor` MCP `preview` with `testId` or `testTitleLike`.
80
+
81
+ ## Path 2 — Real device with DevTools enabled
82
+
83
+ Required when the deployment depends on hardware behaviour the Electron preview cannot reproduce: physical orientation/DPI, real touch hardware (IR overlays, multi-touch), peripherals (camera, BLE, MQTT, NFC, payment, printer, sensors), watchdog cycles on the actual launcher build, fleet-managed reboot semantics, LocalSync across multiple devices, the actual color/luminance of the panel.
84
+
85
+ **Always Path 2** when the brief touches: payment, identity capture, real-time peripheral data, multi-device LocalSync, certified hardware.
86
+
87
+ ### One-time manual setup (the agent cannot do this)
88
+
89
+ Ask the user to:
90
+
91
+ 1. On the device, open **Settings** → advanced settings.
92
+ 2. Toggle **Chrome DevTools** on. The device starts a DevTools server on the local network on port `19851` (auto-increments if taken).
93
+ 3. Confirm **Enable LAN Discovery** is on (default) so the device is discoverable.
94
+ 4. Note the device passcode if displayed.
95
+
96
+ Requires BRICKS Foundation **≥ 2.24** for CDP commands.
97
+
98
+ ### Endpoints exposed once enabled
99
+
100
+ | Endpoint | URL shape | Use |
101
+ |---|---|---|
102
+ | Web UI | `http://<ip>:19851` | DevTools landing in a browser |
103
+ | CDP | `http://<ip>:19851/devtools-frontend/inspector.html?ws=<ip>:19851/ws/<passcode>` | Chrome DevTools front-end |
104
+ | MCP | `http://<ip>:19851/mcp` | MCP endpoint (Bearer-token auth with passcode) |
105
+ | MCP SSE | `http://<ip>:19851/sse` | Same, SSE transport |
106
+ | Info | `http://<ip>:19851/devtools/info` | Device / server metadata |
107
+
108
+ ### Driving the device
109
+
110
+ Once DevTools is on, ask the user how they want to drive the verification — Chrome DevTools front-end, an MCP client they already run, or screenshot export from their own tooling — and follow their lead. Capture every Canvas screenshot through whichever path the user picks; the verification gate is about the evidence, not the tool.
111
+
112
+ For agent-driven CDP/MCP work against the device (`bricks devtools …` with `-a <ip> --passcode <pc>`, plus bridging the device MCP endpoint into an MCP client), the same `bricks-cli` skill referenced in Path 1 covers the on-device case — read it if installed. If not installed, run `bricks --help` and `bricks devtools --help` for the authoritative command listing.
113
+
114
+ ### Real-device side-effects warning
115
+
116
+ Real devices fire real peripherals. Payment terminals charge. MQTT broadcasts on shared topics. BLE advertises to bystanders. Use a **staging** device for verification cycles; never iterate on a production-deployed device unless the user explicitly approves.
117
+
118
+ ## Path 3 — Remote debugging via BRICKS Controller (off-LAN)
119
+
120
+ Out of scope for the standard verification loop. It exists for ops scenarios where the device is not on the same network. If the user asks for it, point them at the BRICKS Controller documentation rather than attempting it as part of verification.
121
+
122
+ ## Decision rule
123
+
124
+ ```
125
+ default Path 1.
126
+
127
+ if deployment depends on:
128
+ physical orientation / DPI / touch HW / peripherals /
129
+ watchdog on real launcher / LocalSync / certified hardware
130
+ → escalate to Path 2 before declaring done.
131
+
132
+ if brief touches payment, identity, peripherals, LocalSync
133
+ → Path 2 is mandatory, not optional.
134
+
135
+ if user is off-LAN
136
+ → Path 3 (out of scope here).
137
+ ```
138
+
139
+ ## What to actually verify (per shape)
140
+
141
+ ### Static signage (single-canvas glance loop)
142
+ - Path 1 screenshot captures the loop frame.
143
+ - Watch one full rotation in `bun preview` to see all queue items.
144
+ - Cut network mid-loop; confirm cached media plays.
145
+ - Path 2 only if the panel is OLED / has burn-in concerns or specific color profile.
146
+
147
+ ### Multi-canvas state machine (kiosk)
148
+ - Path 1: Automation walking the happy path with `brick_press` + `wait_until_canvas_change` + `assert_property` + `match_screenshot` per Canvas.
149
+ - Cancel + inactivity reset paths verified as Automations.
150
+ - Watchdog reset: kill the runtime, restart, confirm boot Canvas resets transient flow Data.
151
+ - Path 2 mandatory if payment / identity is in the flow — fire a real test transaction on staging hardware.
152
+
153
+ ### Subspace-driven composition
154
+ - Path 1: open the Subspace standalone if the preview supports it; verify in isolation.
155
+ - Embedded test: walk the host's flow; assert Outlets fire as expected via `wait_property_update`.
156
+ - Re-mount test: cause the host to re-bind the Subspace; confirm internal state resets.
157
+
158
+ ### Generator-driven reactive
159
+ - Path 1: drive the source by writing to Data from the runtime (whatever CDP path the user prefers); observe Brick re-render.
160
+ - Throttle / firehose tests: write 100 updates rapidly; confirm the canvas doesn't choke (frame budget intact).
161
+ - Disconnect: simulate source disconnect; confirm UI surfaces stale state, not blank.
162
+ - Threshold transitions: cross every Switch boundary; confirm visual reaction.
163
+ - Path 2 mandatory if the source is a real peripheral — Electron cannot fake the timing or the failure modes.
164
+
165
+ ### Multi-device LocalSync
166
+ - Path 2 mandatory. Two devices on the same LAN with DevTools on. Drive one, observe the other.
167
+
168
+ ## Browser / runtime log discipline
169
+
170
+ Throughout: the DevTools console must be clean. 404s on media, "Data key not defined" warnings, Generator init failures, React-style warnings — every one is a defect or accept-with-comment. Don't ship around them.