@hailer/mcp 1.1.13 → 1.1.15

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 (176) hide show
  1. package/.claude/.context-watchdog.json +1 -0
  2. package/.claude/.session-checked +1 -0
  3. package/.claude/CLAUDE.md +370 -0
  4. package/.claude/agents/agent-ada-skill-builder.md +94 -0
  5. package/.claude/agents/agent-alejandro-function-fields.md +342 -0
  6. package/.claude/agents/agent-bjorn-config-audit.md +103 -0
  7. package/.claude/agents/agent-builder-agent-creator.md +130 -0
  8. package/.claude/agents/agent-code-simplifier.md +53 -0
  9. package/.claude/agents/agent-dmitri-activity-crud.md +159 -0
  10. package/.claude/agents/agent-giuseppe-app-builder.md +208 -0
  11. package/.claude/agents/agent-gunther-mcp-tools.md +39 -0
  12. package/.claude/agents/agent-helga-workflow-config.md +204 -0
  13. package/.claude/agents/agent-igor-activity-mover-automation.md +125 -0
  14. package/.claude/agents/agent-ingrid-doc-templates.md +261 -0
  15. package/.claude/agents/agent-ivan-monolith.md +154 -0
  16. package/.claude/agents/agent-kenji-data-reader.md +86 -0
  17. package/.claude/agents/agent-lars-code-inspector.md +102 -0
  18. package/.claude/agents/agent-marco-mockup-builder.md +110 -0
  19. package/.claude/agents/agent-marcus-api-documenter.md +323 -0
  20. package/.claude/agents/agent-marketplace-publisher.md +280 -0
  21. package/.claude/agents/agent-marketplace-reviewer.md +309 -0
  22. package/.claude/agents/agent-permissions-handler.md +208 -0
  23. package/.claude/agents/agent-simple-writer.md +48 -0
  24. package/.claude/agents/agent-svetlana-code-review.md +171 -0
  25. package/.claude/agents/agent-tanya-test-runner.md +333 -0
  26. package/.claude/agents/agent-ui-designer.md +100 -0
  27. package/.claude/agents/agent-viktor-sql-insights.md +212 -0
  28. package/.claude/agents/agent-web-search.md +55 -0
  29. package/.claude/agents/agent-yevgeni-discussions.md +45 -0
  30. package/.claude/agents/agent-zara-zapier.md +159 -0
  31. package/.claude/agents/ragnar.md +68 -0
  32. package/.claude/commands/app-squad.md +135 -0
  33. package/.claude/commands/audit-squad.md +158 -0
  34. package/.claude/commands/autoplan.md +563 -0
  35. package/.claude/commands/cleanup-squad.md +98 -0
  36. package/.claude/commands/config-squad.md +106 -0
  37. package/.claude/commands/crud-squad.md +87 -0
  38. package/.claude/commands/data-squad.md +97 -0
  39. package/.claude/commands/debug-squad.md +303 -0
  40. package/.claude/commands/doc-squad.md +65 -0
  41. package/.claude/commands/handoff.md +137 -0
  42. package/.claude/commands/health.md +49 -0
  43. package/.claude/commands/help.md +29 -0
  44. package/.claude/commands/help:agents.md +151 -0
  45. package/.claude/commands/help:commands.md +78 -0
  46. package/.claude/commands/help:faq.md +79 -0
  47. package/.claude/commands/help:plugins.md +50 -0
  48. package/.claude/commands/help:skills.md +93 -0
  49. package/.claude/commands/help:tools.md +75 -0
  50. package/.claude/commands/hotfix-squad.md +112 -0
  51. package/.claude/commands/integration-squad.md +82 -0
  52. package/.claude/commands/janitor-squad.md +167 -0
  53. package/.claude/commands/learn-auto.md +120 -0
  54. package/.claude/commands/learn.md +120 -0
  55. package/.claude/commands/mcp-list.md +27 -0
  56. package/.claude/commands/onboard-squad.md +140 -0
  57. package/.claude/commands/plan-workspace.md +732 -0
  58. package/.claude/commands/prd.md +130 -0
  59. package/.claude/commands/project-status.md +82 -0
  60. package/.claude/commands/publish.md +138 -0
  61. package/.claude/commands/recap.md +69 -0
  62. package/.claude/commands/restore.md +64 -0
  63. package/.claude/commands/review-squad.md +152 -0
  64. package/.claude/commands/save.md +24 -0
  65. package/.claude/commands/stats.md +19 -0
  66. package/.claude/commands/swarm.md +210 -0
  67. package/.claude/commands/tool-builder.md +39 -0
  68. package/.claude/commands/ws-pull.md +44 -0
  69. package/.claude/skills/SDK-activity-patterns/SKILL.md +428 -0
  70. package/.claude/skills/SDK-document-templates/SKILL.md +1033 -0
  71. package/.claude/skills/SDK-function-fields/SKILL.md +542 -0
  72. package/.claude/skills/SDK-generate-skill/SKILL.md +92 -0
  73. package/.claude/skills/SDK-init-skill/SKILL.md +127 -0
  74. package/.claude/skills/SDK-insight-queries/SKILL.md +787 -0
  75. package/.claude/skills/SDK-ws-config-skill/SKILL.md +1139 -0
  76. package/.claude/skills/agent-structure/SKILL.md +98 -0
  77. package/.claude/skills/api-documentation-patterns/SKILL.md +474 -0
  78. package/.claude/skills/chrome-mcp-reference/SKILL.md +370 -0
  79. package/.claude/skills/delegation-routing/SKILL.md +202 -0
  80. package/.claude/skills/frontend-design/SKILL.md +254 -0
  81. package/.claude/skills/hailer-activity-mover/SKILL.md +213 -0
  82. package/.claude/skills/hailer-api-client/SKILL.md +518 -0
  83. package/.claude/skills/hailer-app-builder/SKILL.md +1440 -0
  84. package/.claude/skills/hailer-apps-pictures/SKILL.md +269 -0
  85. package/.claude/skills/hailer-design-system/SKILL.md +231 -0
  86. package/.claude/skills/hailer-monolith-automations/SKILL.md +686 -0
  87. package/.claude/skills/hailer-permissions-system/SKILL.md +121 -0
  88. package/.claude/skills/hailer-project-protocol/SKILL.md +488 -0
  89. package/.claude/skills/hailer-rest-api/SKILL.md +61 -0
  90. package/.claude/skills/hailer-rest-api/hailer-activities.md +184 -0
  91. package/.claude/skills/hailer-rest-api/hailer-admin.md +473 -0
  92. package/.claude/skills/hailer-rest-api/hailer-calendar.md +256 -0
  93. package/.claude/skills/hailer-rest-api/hailer-feed.md +249 -0
  94. package/.claude/skills/hailer-rest-api/hailer-insights.md +195 -0
  95. package/.claude/skills/hailer-rest-api/hailer-messaging.md +276 -0
  96. package/.claude/skills/hailer-rest-api/hailer-workflows.md +283 -0
  97. package/.claude/skills/insight-join-patterns/SKILL.md +174 -0
  98. package/.claude/skills/integration-patterns/SKILL.md +421 -0
  99. package/.claude/skills/json-only-output/SKILL.md +72 -0
  100. package/.claude/skills/lsp-setup/SKILL.md +160 -0
  101. package/.claude/skills/mcp-direct-tools/SKILL.md +153 -0
  102. package/.claude/skills/optional-parameters/SKILL.md +72 -0
  103. package/.claude/skills/publish-hailer-app/SKILL.md +221 -0
  104. package/.claude/skills/testing-patterns/SKILL.md +630 -0
  105. package/.claude/skills/tool-builder/SKILL.md +250 -0
  106. package/.claude/skills/tool-parameter-usage/SKILL.md +126 -0
  107. package/.claude/skills/tool-response-verification/SKILL.md +92 -0
  108. package/.claude/skills/zapier-hailer-patterns/SKILL.md +581 -0
  109. package/.opencode/agent/agent-ada-skill-builder.md +35 -0
  110. package/.opencode/agent/agent-alejandro-function-fields.md +39 -0
  111. package/.opencode/agent/agent-bjorn-config-audit.md +36 -0
  112. package/.opencode/agent/agent-builder-agent-creator.md +39 -0
  113. package/.opencode/agent/agent-code-simplifier.md +31 -0
  114. package/.opencode/agent/agent-dmitri-activity-crud.md +40 -0
  115. package/.opencode/agent/agent-giuseppe-app-builder.md +37 -0
  116. package/.opencode/agent/agent-gunther-mcp-tools.md +39 -0
  117. package/.opencode/agent/agent-helga-workflow-config.md +204 -0
  118. package/.opencode/agent/agent-igor-activity-mover-automation.md +46 -0
  119. package/.opencode/agent/agent-ingrid-doc-templates.md +39 -0
  120. package/.opencode/agent/agent-ivan-monolith.md +46 -0
  121. package/.opencode/agent/agent-kenji-data-reader.md +53 -0
  122. package/.opencode/agent/agent-lars-code-inspector.md +28 -0
  123. package/.opencode/agent/agent-marco-mockup-builder.md +42 -0
  124. package/.opencode/agent/agent-marcus-api-documenter.md +53 -0
  125. package/.opencode/agent/agent-marketplace-publisher.md +44 -0
  126. package/.opencode/agent/agent-marketplace-reviewer.md +42 -0
  127. package/.opencode/agent/agent-permissions-handler.md +50 -0
  128. package/.opencode/agent/agent-simple-writer.md +45 -0
  129. package/.opencode/agent/agent-svetlana-code-review.md +39 -0
  130. package/.opencode/agent/agent-tanya-test-runner.md +57 -0
  131. package/.opencode/agent/agent-ui-designer.md +56 -0
  132. package/.opencode/agent/agent-viktor-sql-insights.md +34 -0
  133. package/.opencode/agent/agent-web-search.md +42 -0
  134. package/.opencode/agent/agent-yevgeni-discussions.md +37 -0
  135. package/.opencode/agent/agent-zara-zapier.md +53 -0
  136. package/.opencode/commands/app-squad.md +135 -0
  137. package/.opencode/commands/audit-squad.md +158 -0
  138. package/.opencode/commands/autoplan.md +563 -0
  139. package/.opencode/commands/cleanup-squad.md +98 -0
  140. package/.opencode/commands/config-squad.md +106 -0
  141. package/.opencode/commands/crud-squad.md +87 -0
  142. package/.opencode/commands/data-squad.md +97 -0
  143. package/.opencode/commands/debug-squad.md +303 -0
  144. package/.opencode/commands/doc-squad.md +65 -0
  145. package/.opencode/commands/handoff.md +137 -0
  146. package/.opencode/commands/health.md +49 -0
  147. package/.opencode/commands/help-agents.md +151 -0
  148. package/.opencode/commands/help-commands.md +32 -0
  149. package/.opencode/commands/help-faq.md +29 -0
  150. package/.opencode/commands/help-plugins.md +28 -0
  151. package/.opencode/commands/help-skills.md +7 -0
  152. package/.opencode/commands/help-tools.md +40 -0
  153. package/.opencode/commands/help.md +28 -0
  154. package/.opencode/commands/hotfix-squad.md +112 -0
  155. package/.opencode/commands/integration-squad.md +82 -0
  156. package/.opencode/commands/janitor-squad.md +167 -0
  157. package/.opencode/commands/learn-auto.md +120 -0
  158. package/.opencode/commands/learn.md +120 -0
  159. package/.opencode/commands/mcp-list.md +27 -0
  160. package/.opencode/commands/onboard-squad.md +140 -0
  161. package/.opencode/commands/plan-workspace.md +732 -0
  162. package/.opencode/commands/prd.md +131 -0
  163. package/.opencode/commands/project-status.md +82 -0
  164. package/.opencode/commands/publish.md +138 -0
  165. package/.opencode/commands/recap.md +69 -0
  166. package/.opencode/commands/restore.md +64 -0
  167. package/.opencode/commands/review-squad.md +152 -0
  168. package/.opencode/commands/save.md +24 -0
  169. package/.opencode/commands/stats.md +19 -0
  170. package/.opencode/commands/swarm.md +210 -0
  171. package/.opencode/commands/tool-builder.md +39 -0
  172. package/.opencode/commands/ws-pull.md +44 -0
  173. package/.opencode/opencode.json +21 -0
  174. package/package.json +1 -1
  175. package/scripts/postinstall.cjs +64 -0
  176. package/scripts/test-hal-tools.ts +154 -0
@@ -0,0 +1,1440 @@
1
+ ---
2
+ name: hailer-app-builder
3
+ description: Patterns for building Hailer apps with @hailer/app-sdk
4
+ version: 1.3.1
5
+ triggers:
6
+ - build app
7
+ - hailer app
8
+ - app sdk
9
+ ---
10
+
11
+ # Hailer App Builder Skill
12
+
13
+ Patterns and templates for building Hailer apps with @hailer/app-sdk.
14
+
15
+ <critical-rules>
16
+ ## CRITICAL: Scaffolding and Data Sources
17
+
18
+ **ALWAYS use scaffold_hailer_app MCP tool** to create new apps. Never manually create the project structure.
19
+
20
+ ### scaffold_hailer_app - One-Shot Full Setup
21
+
22
+ This tool does EVERYTHING in one call:
23
+ - Scaffolds project from `@hailer/create-app@beta` template (Vite + React + TypeScript)
24
+ - Runs `npm install`
25
+ - Configures CORS in `vite.config.ts`
26
+ - **Reuses existing localhost dev app** if one exists, otherwise creates a new one (with auto-generated icon)
27
+ - Shares app with entire workspace
28
+ - Adds appId to `manifest.json`
29
+ - Starts dev server on port 3000
30
+
31
+ **NEVER call `create_app` separately during scaffolding — it creates duplicates.** The scaffold tool handles dev app creation/reuse internally.
32
+
33
+ **Publishing (when ready):** Call `publish_hailer_app` — it builds, packages with correct tar structure (`package/dist/manifest.json`), uploads to S3, and auto-updates app URL to production.
34
+
35
+ **You're customizing a working starter app**, not building from scratch.
36
+
37
+ ### create_app - Entry Only (No Local Files)
38
+
39
+ Use `mcp__hailer__create_app` when you:
40
+ - Need a production app entry pointing to a deployed URL
41
+ - Want to register an external/existing app in Hailer
42
+ - Already have app code and just need the Hailer entry
43
+
44
+ ```
45
+ mcp__hailer__create_app({
46
+ name: "Production App",
47
+ url: "https://app.example.com"
48
+ })
49
+ ```
50
+
51
+ **scaffold = full development setup**
52
+ **create_app = just the Hailer entry/frame**
53
+
54
+ **For project data structure (workflows, fields, phases):**
55
+ - READ workspace/ TypeScript files directly (fields.ts, phases.ts, enums.ts)
56
+ - Do NOT use MCP tools for data structure queries
57
+ - The SDK pull provides all needed type information locally
58
+
59
+ ```typescript
60
+ // For app constants, read from local workspace files:
61
+ // - workspace/enums.ts (IDs)
62
+ // - workspace/[Workflow]_[id]/fields.ts (field definitions)
63
+ // - workspace/[Workflow]_[id]/phases.ts (phase definitions)
64
+ ```
65
+ </critical-rules>
66
+
67
+ <local-dev-flow>
68
+ ## Development Flow
69
+
70
+ **Default: Local development.** `scaffold_hailer_app` handles everything automatically:
71
+ 1. Creates local project files
72
+ 2. Reuses existing localhost dev app (or creates one if none exists) at `http://localhost:3000`
73
+ 3. Shares the app with the workspace
74
+ 4. Starts the dev server
75
+
76
+ **Do NOT call `create_app` during scaffolding.** The scaffold tool handles it.
77
+
78
+ After scaffolding, run `npm run dev` and test inside Hailer iframe.
79
+
80
+ **Publishing: Only when user explicitly asks.** Load the `publish-hailer-app` skill, which covers manifest validation, file upload via `publish_hailer_app`, and URL switch from localhost to production via `update_app`.
81
+
82
+ ### Manual Local Dev App (Rare Cases)
83
+
84
+ Only needed if:
85
+ - You have existing code without a dev app entry
86
+ - The scaffold's dev app was deleted
87
+
88
+ ```
89
+ mcp__hailer__create_app({
90
+ name: "Local Dev",
91
+ url: "http://localhost:3000",
92
+ description: "Local development testing"
93
+ })
94
+ ```
95
+
96
+ Or manually in Hailer UI: Apps → Create App → URL: http://localhost:3000
97
+ </local-dev-flow>
98
+
99
+ <sdk-setup>
100
+ ## Hook Import (CRITICAL)
101
+
102
+ ```typescript
103
+ // CORRECT - local default import
104
+ import useHailer from './hailer/use-hailer';
105
+
106
+ // WRONG - will fail build
107
+ import { useHailer } from '@hailer/app-sdk';
108
+ ```
109
+
110
+ ## Hook Usage
111
+
112
+ ```typescript
113
+ function App() {
114
+ const { inside, hailer } = useHailer();
115
+
116
+ // CORRECT dependency array
117
+ useEffect(() => {
118
+ // fetch data
119
+ }, [inside]); // Use [inside] NOT [hailer]
120
+
121
+ // Early return AFTER hooks
122
+ if (!inside) return <Text>Open this app inside Hailer</Text>;
123
+
124
+ return <Box>...</Box>;
125
+ }
126
+ ```
127
+ </sdk-setup>
128
+
129
+ <usehailer-fix>
130
+ ## CRITICAL: Replace Scaffold's useHailer Hook
131
+
132
+ **The scaffold generates a buggy useHailer hook.** After scaffolding, ALWAYS replace `src/hailer/use-hailer.ts` with this shared-state implementation:
133
+
134
+ ### Why the Scaffold's Hook is Broken
135
+
136
+ The scaffold creates a hook using per-component `useState`:
137
+
138
+ ```typescript
139
+ // ❌ BUGGY - each component gets its own state
140
+ function useHailer() {
141
+ const [inside, setInside] = useState(false); // Each component gets separate copy!
142
+
143
+ useEffect(() => {
144
+ hailer.init({ config: () => setInside(true) }); // Only updates THIS component
145
+ }, []);
146
+
147
+ return { inside, hailer };
148
+ }
149
+ ```
150
+
151
+ **Result:** App.tsx sees `inside: true`, but child pages (Dashboard, Settings) still see `inside: false`.
152
+
153
+ ### The Fix: Shared State with useSyncExternalStore
154
+
155
+ Replace `src/hailer/use-hailer.ts` with:
156
+
157
+ ```typescript
158
+ import { useSyncExternalStore } from 'react';
159
+ import HailerApi from '@hailer/app-sdk';
160
+
161
+ // Types
162
+ interface HailerState {
163
+ inside: boolean;
164
+ hailer: ReturnType<typeof HailerApi> | null;
165
+ config: Record<string, unknown> | null;
166
+ }
167
+
168
+ declare global {
169
+ interface Window {
170
+ __hailerStore?: {
171
+ state: HailerState;
172
+ listeners: Set<() => void>;
173
+ subscribe: (listener: () => void) => () => void;
174
+ getSnapshot: () => HailerState;
175
+ setState: (newState: Partial<HailerState>) => void;
176
+ };
177
+ }
178
+ }
179
+
180
+ // Initialize store once on window
181
+ function getStore() {
182
+ if (!window.__hailerStore) {
183
+ window.__hailerStore = {
184
+ state: { inside: false, hailer: null, config: null },
185
+ listeners: new Set(),
186
+ subscribe(listener) {
187
+ this.listeners.add(listener);
188
+ return () => this.listeners.delete(listener);
189
+ },
190
+ getSnapshot() {
191
+ return this.state;
192
+ },
193
+ setState(newState) {
194
+ this.state = { ...this.state, ...newState };
195
+ this.listeners.forEach((l) => l());
196
+ },
197
+ };
198
+
199
+ // Initialize SDK once
200
+ const api = HailerApi({
201
+ config: (inside, cfg) => { // SDK passes (inside: boolean, config: object)
202
+ window.__hailerStore!.setState({
203
+ inside: inside,
204
+ config: cfg?.fields ?? null,
205
+ });
206
+ },
207
+ error: (err) => {
208
+ console.error('Hailer SDK error:', err);
209
+ },
210
+ });
211
+ window.__hailerStore.setState({ hailer: api });
212
+ }
213
+ return window.__hailerStore;
214
+ }
215
+
216
+ export default function useHailer() {
217
+ const store = getStore();
218
+ const state = useSyncExternalStore(
219
+ store.subscribe.bind(store),
220
+ store.getSnapshot.bind(store)
221
+ );
222
+
223
+ return {
224
+ inside: state.inside,
225
+ hailer: state.hailer!,
226
+ config: state.config,
227
+ };
228
+ }
229
+ ```
230
+
231
+ ### Why This Works
232
+
233
+ 1. **Single store on `window`** - All components share one source of truth
234
+ 2. **SDK initialized once** - No duplicate callbacks
235
+ 3. **useSyncExternalStore** - React 18's official pattern for external state
236
+ 4. **All components update together** - When `inside` changes, every subscriber re-renders
237
+
238
+ ### Giuseppe Rule
239
+
240
+ After `scaffold_hailer_app`, ALWAYS replace `src/hailer/use-hailer.ts` with the shared-state version above.
241
+ </usehailer-fix>
242
+
243
+ <sdk-api>
244
+ ## Activity API
245
+
246
+ ```typescript
247
+ // List activities from workflow phase
248
+ const activities = await hailer.activity.list(workflowId, phaseId, options?);
249
+
250
+ // Get single activity
251
+ const activity = await hailer.activity.get(activityId);
252
+
253
+ // Create activities
254
+ const created = await hailer.activity.create(workflowId, activities[], options?);
255
+
256
+ // Update activities (returns count)
257
+ const count = await hailer.activity.update(activities[], options);
258
+
259
+ // Remove activities (returns count)
260
+ const count = await hailer.activity.remove(activityIds[]);
261
+ ```
262
+
263
+ ### ActivityListOptions
264
+
265
+ ```typescript
266
+ interface ActivityListOptions {
267
+ sortBy?: 'name' | 'created' | 'updated' | 'following' | 'owner' | 'team' | 'completedOn' | 'priority';
268
+ sortOrder?: 'asc' | 'desc';
269
+ limit?: number;
270
+ skip?: number;
271
+ includeUsers?: boolean;
272
+ includeTeams?: boolean;
273
+ includeHistory?: boolean;
274
+ filters?: any;
275
+ }
276
+ ```
277
+
278
+ ### ActivityCreateOptions
279
+
280
+ ```typescript
281
+ interface ActivityCreateOptions {
282
+ teamId?: string; // Assign to team
283
+ fileIds?: string[]; // Attach files (use ui.files.uploadFile first)
284
+ followerIds?: string[]; // Add followers
285
+ location?: {
286
+ label?: string;
287
+ type: 'area' | 'point' | 'polyline';
288
+ data: [{ lat: number; lng: number }];
289
+ };
290
+ discussionId?: string; // Link to discussion
291
+ phaseId?: string; // Initial phase (defaults to first)
292
+ returnDocument?: boolean; // Return full activity after create
293
+ ignoreRequired?: boolean; // Skip required field validation
294
+ }
295
+ ```
296
+
297
+ ### Activity Interface
298
+
299
+ ```typescript
300
+ interface Activity {
301
+ _id: string;
302
+ name: string;
303
+ process: string;
304
+ currentPhase: string;
305
+ fields?: Record<string, { value: unknown }>;
306
+ files?: string[];
307
+ followers?: string[];
308
+ created?: number;
309
+ updated?: number;
310
+ updatedBy?: string;
311
+ priority?: number;
312
+ location?: {
313
+ type: 'point' | 'area' | 'polyline';
314
+ label: string | null;
315
+ data: Array<{ lat: number; lng: number }>;
316
+ };
317
+ }
318
+ ```
319
+
320
+ ### Phase Transitions (Moving Activities Between Phases)
321
+
322
+ **CRITICAL:** The SDK does NOT have a `hailer.activity.move()` method. To move activities between phases, use `hailer.activity.update()` with the `phaseId` parameter.
323
+
324
+ ```typescript
325
+ // CORRECT - Use activity.update() with phaseId
326
+ await hailer.activity.update([
327
+ {
328
+ _id: activityId,
329
+ phaseId: newPhaseId, // Move to different phase
330
+ },
331
+ ], {});
332
+
333
+ // ❌ WRONG - activity.move() DOES NOT EXIST
334
+ // await hailer.activity.move(activityId, newPhaseId); // This method doesn't exist in the SDK!
335
+ ```
336
+
337
+ **Example: Move multiple activities to new phase**
338
+ ```typescript
339
+ const activityIds = ['id1', 'id2', 'id3'];
340
+ const targetPhaseId = 'phaseId789';
341
+
342
+ await hailer.activity.update(
343
+ activityIds.map(id => ({
344
+ _id: id,
345
+ phaseId: targetPhaseId,
346
+ })),
347
+ {}
348
+ );
349
+ ```
350
+
351
+ ### CRITICAL: activity.list() Requires Valid phaseId
352
+
353
+ **Problem:** `hailer.activity.list(workflowId, '', options)` fails - empty phaseId not allowed.
354
+
355
+ **Solution:** Query all phases in parallel:
356
+ ```typescript
357
+ const ALL_PHASES = ['phaseId1', 'phaseId2', 'phaseId3'];
358
+
359
+ const phaseResults = await Promise.all(
360
+ ALL_PHASES.map((phaseId) =>
361
+ hailer.activity.list(workflowId, phaseId, { limit: 500 }).catch(() => [])
362
+ )
363
+ );
364
+ const allActivities = phaseResults.flat();
365
+ ```
366
+
367
+ ### Phase Selection: Which Phases to Fetch
368
+
369
+ **Don't blindly fetch all phases.** Consider what the user needs to see:
370
+
371
+ | App Type | Phases to Fetch | Rationale |
372
+ |----------|-----------------|-----------|
373
+ | Sales dashboard | Active only | Don't show internal drafts |
374
+ | Product manager view | Draft + Active | Need to see work-in-progress |
375
+ | Archive browser | Archived only | Historical data |
376
+ | Kanban board | All except Archived | Full workflow visibility |
377
+
378
+ **Example: Role-based phase selection**
379
+ ```typescript
380
+ // Define phases per user role
381
+ const PHASE_CONFIG = {
382
+ sales: ['activePhaseId'],
383
+ manager: ['draftPhaseId', 'activePhaseId'],
384
+ admin: ['draftPhaseId', 'activePhaseId', 'archivedPhaseId'],
385
+ };
386
+
387
+ // Fetch based on role
388
+ const userRole = 'sales'; // from app config or user check
389
+ const phases = PHASE_CONFIG[userRole] || PHASE_CONFIG.sales;
390
+
391
+ const results = await Promise.all(
392
+ phases.map(phaseId => hailer.activity.list(workflowId, phaseId))
393
+ );
394
+ ```
395
+
396
+ **Document phase selection in PRD:**
397
+ ```markdown
398
+ ## Data Access
399
+ - **Product Browser**: Shows Draft + Active phases (managers need WIP visibility)
400
+ - **Public Dashboard**: Active only (no internal data exposed)
401
+ - **Insights**: Active only (accurate counts, no duplicates from drafts)
402
+ ```
403
+
404
+ ## Kanban API
405
+
406
+ ```typescript
407
+ // List activities grouped by phase (kanban view)
408
+ const kanban = await hailer.activity.kanban.list(workflowId, options?);
409
+ // Returns: { map: { [phaseId]: { activities: [...], meta: { count, ... } } } }
410
+
411
+ // Load single activity in kanban format
412
+ const item = await hailer.activity.kanban.load(activityId);
413
+ // Returns: { activity: {...}, process: string, phase: string }
414
+
415
+ // Update activity priority
416
+ await hailer.activity.kanban.updatePriority(activityId, priority);
417
+ ```
418
+
419
+ ### KanbanListOptions
420
+
421
+ ```typescript
422
+ interface ActivityKanbanListOptions {
423
+ filter: {
424
+ user?: { uid: string; field: string };
425
+ account?: string;
426
+ team?: string;
427
+ dates?: { field: string; start: number; end: number };
428
+ };
429
+ search?: string;
430
+ limit?: number;
431
+ skip?: number;
432
+ phase?: string;
433
+ includeUsers?: boolean;
434
+ includeTeams?: boolean;
435
+ }
436
+ ```
437
+
438
+ ## UI API
439
+
440
+ ### Activity UI
441
+
442
+ ```typescript
443
+ // Open activity in sidebar
444
+ await hailer.ui.activity.open(activityId, options?);
445
+
446
+ type ActivityTabTypes = 'detail' | 'discussion' | 'files' | 'location' | 'linkedFrom' | 'options';
447
+
448
+ // Open with specific tab
449
+ await hailer.ui.activity.open(activityId, { tab: 'files' });
450
+
451
+ // Open create form (returns created activity or null if cancelled)
452
+ const result = await hailer.ui.activity.create(workflowId, {
453
+ name?: string,
454
+ fields?: { [fieldId]: value },
455
+ location?: { type: 'point', data: [{ lat, lng }] }
456
+ });
457
+
458
+ // Bulk edit multiple activities
459
+ await hailer.ui.activity.editMultiple(activityIds[]);
460
+ ```
461
+
462
+ **Note:** `hailer.openSidebar()` does NOT exist - use `hailer.ui.activity.open()`.
463
+
464
+ ### File Upload
465
+
466
+ ```typescript
467
+ // Upload file to Hailer (returns file ID)
468
+ const fileId = await hailer.ui.files.uploadFile(
469
+ file, // File object from <input type="file">
470
+ filename, // Desired filename
471
+ { isPublic?: boolean }
472
+ );
473
+
474
+ // Then attach to activity on create:
475
+ await hailer.activity.create(workflowId, [{ name: 'Doc' }], { fileIds: [fileId] });
476
+ ```
477
+
478
+ ### Snackbar (Toast Notifications)
479
+
480
+ ```typescript
481
+ // Show notification
482
+ await hailer.ui.snackbar.open(text, buttonLabel, duration?);
483
+
484
+ // Examples
485
+ hailer.ui.snackbar.open('Saved!', 'OK');
486
+ hailer.ui.snackbar.open('Deleted', 'Undo', 5000);
487
+ ```
488
+
489
+ ### Insight UI
490
+
491
+ ```typescript
492
+ await hailer.ui.insight.create(workspaceId?); // Open create dialog
493
+ await hailer.ui.insight.edit(insightId); // Open edit dialog
494
+ await hailer.ui.insight.delete(insightId); // Open delete confirmation
495
+ await hailer.ui.insight.permission(insightId); // Open permissions dialog
496
+ ```
497
+
498
+ ## Insight API
499
+
500
+ ```typescript
501
+ // Get insight data (SQL query results)
502
+ const data = await hailer.insight.data(insightId, { update?: true });
503
+
504
+ // List all insights
505
+ const insights = await hailer.insight.list();
506
+
507
+ // Update insight
508
+ const updated = await hailer.insight.update(insightId, partialUpdate);
509
+ ```
510
+
511
+ **Response structure:**
512
+ ```typescript
513
+ interface InsightData {
514
+ columns: string[];
515
+ rows: any[][];
516
+ }
517
+
518
+ interface InsightDoc {
519
+ _id: string;
520
+ name: string;
521
+ // ... other insight properties
522
+ }
523
+ ```
524
+
525
+ ## Workflow API
526
+
527
+ ```typescript
528
+ // List all workflows
529
+ const workflows = await hailer.workflow.list();
530
+
531
+ // Get single workflow with full schema
532
+ const workflow = await hailer.workflow.get(workflowId);
533
+ ```
534
+
535
+ ## User API
536
+
537
+ ```typescript
538
+ // Get current logged-in user
539
+ const me = await hailer.user.current();
540
+ // Returns: { _id, email, firstname, lastname, ... }
541
+
542
+ // Get specific user by ID
543
+ const user = await hailer.user.get(userId);
544
+
545
+ // List all workspace users (returns object keyed by user ID)
546
+ const users = await hailer.user.list();
547
+ ```
548
+
549
+ ### Getting User Info for Personalized Greeting
550
+
551
+ **The config callback does NOT include user info:**
552
+ ```typescript
553
+ // ❌ WRONG - config only has { fields: {} }
554
+ const { config } = useHailer();
555
+ const userName = config?.userName; // undefined!
556
+ ```
557
+
558
+ **Use hailer.user.current() instead:**
559
+ ```typescript
560
+ const { inside, hailer } = useHailer();
561
+ const [userName, setUserName] = useState<string>('');
562
+
563
+ useEffect(() => {
564
+ if (!inside) return;
565
+
566
+ hailer.user.current().then(user => {
567
+ setUserName(user.firstname || 'there');
568
+ });
569
+ }, [inside]);
570
+
571
+ return <Heading>Hello, {userName}!</Heading>;
572
+ ```
573
+
574
+ ## Workspace API
575
+
576
+ ```typescript
577
+ // Get current workspace
578
+ const workspace = await hailer.workspace.current();
579
+
580
+ // List workspaces (personal apps only)
581
+ const workspaces = await hailer.workspace.list();
582
+ ```
583
+ </sdk-api>
584
+
585
+ <field-patterns>
586
+ ## Extracting Field Values
587
+
588
+ ```typescript
589
+ // Fields are optional and nested
590
+ interface Activity {
591
+ fields?: Record<string, { value: unknown }>;
592
+ }
593
+
594
+ // Safe extraction helper
595
+ function getFieldValue<T>(activity: Activity, fieldId: string, defaultValue: T): T {
596
+ return (activity.fields?.[fieldId]?.value as T) ?? defaultValue;
597
+ }
598
+
599
+ // Usage
600
+ const name = getFieldValue(activity, 'fieldId123', '');
601
+ const count = getFieldValue(activity, 'fieldId456', 0);
602
+ const date = getFieldValue(activity, 'fieldId789', null);
603
+ ```
604
+
605
+ ## Field Types
606
+
607
+ ```typescript
608
+ // Text field
609
+ const text = activity.fields?.['fieldId']?.value as string;
610
+
611
+ // Number field
612
+ const num = activity.fields?.['fieldId']?.value as number;
613
+
614
+ // Date field (timestamp)
615
+ const date = activity.fields?.['fieldId']?.value as number;
616
+ const formatted = new Date(date).toLocaleDateString();
617
+
618
+ // Enum/Select field
619
+ const status = activity.fields?.['fieldId']?.value as string;
620
+
621
+ // ActivityLink field (reference to another activity)
622
+ // IMPORTANT: ActivityLink has nested structure, not direct values
623
+ interface ActivityLinkValue {
624
+ _id: string;
625
+ name: string;
626
+ }
627
+ const linked = activity.fields?.['fieldId']?.value as ActivityLinkValue;
628
+ const linkedId = linked?._id; // Get linked activity ID
629
+ const linkedName = linked?.name || 'Unknown'; // Get display name
630
+
631
+ // User field
632
+ interface UserValue {
633
+ _id: string;
634
+ firstname: string;
635
+ lastname: string;
636
+ }
637
+ const user = activity.fields?.['fieldId']?.value as UserValue;
638
+ const userName = user ? `${user.firstname} ${user.lastname}` : 'Unknown';
639
+ ```
640
+
641
+ ## Finnish Date Parsing
642
+
643
+ Insights and some fields return Finnish date strings (`dd.mm.yyyy`) instead of timestamps. These can't be sorted directly.
644
+
645
+ ```typescript
646
+ // ❌ WRONG - Number("03.02.2026") returns NaN
647
+ dates.sort((a, b) => Number(a) - Number(b));
648
+
649
+ // ✅ CORRECT - Parse Finnish dates to Date objects
650
+ function parseFinnishDate(dateStr: string): Date | null {
651
+ // Matches "03.02.2026" or "03.02.2026 10:00"
652
+ const match = dateStr.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})(?:\s+(\d{1,2}):(\d{2}))?/);
653
+ if (!match) return null;
654
+
655
+ const [, day, month, year, hours = '0', minutes = '0'] = match;
656
+ return new Date(
657
+ parseInt(year),
658
+ parseInt(month) - 1, // JS months are 0-indexed
659
+ parseInt(day),
660
+ parseInt(hours),
661
+ parseInt(minutes)
662
+ );
663
+ }
664
+
665
+ // Sorting Finnish dates
666
+ items.sort((a, b) => {
667
+ const dateA = parseFinnishDate(a.dateField);
668
+ const dateB = parseFinnishDate(b.dateField);
669
+ if (!dateA || !dateB) return 0;
670
+ return dateA.getTime() - dateB.getTime();
671
+ });
672
+
673
+ // Formatting back to Finnish
674
+ function formatFinnishDate(date: Date): string {
675
+ return date.toLocaleDateString('fi-FI'); // "3.2.2026"
676
+ }
677
+
678
+ function formatFinnishDateTime(date: Date): string {
679
+ return date.toLocaleString('fi-FI', {
680
+ day: 'numeric',
681
+ month: 'numeric',
682
+ year: 'numeric',
683
+ hour: '2-digit',
684
+ minute: '2-digit',
685
+ }); // "3.2.2026 klo 10.00"
686
+ }
687
+ ```
688
+
689
+ **When to use:**
690
+ - Insight data returns formatted dates (not timestamps)
691
+ - Text fields containing dates
692
+ - Displaying dates to Finnish users
693
+ </field-patterns>
694
+
695
+ <component-templates>
696
+ ## Activity Table
697
+
698
+ ```typescript
699
+ import { Table, Thead, Tbody, Tr, Th, Td, Box, Spinner, Text } from '@chakra-ui/react';
700
+
701
+ interface Activity {
702
+ _id: string;
703
+ name: string;
704
+ fields?: Record<string, { value: unknown }>;
705
+ }
706
+
707
+ interface Props {
708
+ activities: Activity[];
709
+ loading: boolean;
710
+ columns: { fieldId: string; label: string }[];
711
+ }
712
+
713
+ function ActivityTable({ activities, loading, columns }: Props) {
714
+ if (loading) return <Spinner />;
715
+ if (activities.length === 0) return <Text>No data</Text>;
716
+
717
+ return (
718
+ <Table variant="simple" size="sm">
719
+ <Thead>
720
+ <Tr>
721
+ <Th>Name</Th>
722
+ {columns.map(col => (
723
+ <Th key={col.fieldId}>{col.label}</Th>
724
+ ))}
725
+ </Tr>
726
+ </Thead>
727
+ <Tbody>
728
+ {activities.map(activity => (
729
+ <Tr key={activity._id}>
730
+ <Td>{activity.name}</Td>
731
+ {columns.map(col => (
732
+ <Td key={col.fieldId}>
733
+ {String(activity.fields?.[col.fieldId]?.value ?? '-')}
734
+ </Td>
735
+ ))}
736
+ </Tr>
737
+ ))}
738
+ </Tbody>
739
+ </Table>
740
+ );
741
+ }
742
+ ```
743
+
744
+ ## Activity Card
745
+
746
+ ```typescript
747
+ import { Box, Heading, Text, VStack, useColorModeValue } from '@chakra-ui/react';
748
+
749
+ interface Props {
750
+ activity: Activity;
751
+ fieldId: string;
752
+ fieldLabel: string;
753
+ }
754
+
755
+ function ActivityCard({ activity, fieldId, fieldLabel }: Props) {
756
+ const bg = useColorModeValue('white', 'gray.700');
757
+ const borderColor = useColorModeValue('gray.200', 'gray.600');
758
+
759
+ return (
760
+ <Box
761
+ p={4}
762
+ bg={bg}
763
+ borderRadius="md"
764
+ border="1px"
765
+ borderColor={borderColor}
766
+ >
767
+ <VStack align="start" spacing={2}>
768
+ <Heading size="sm">{activity.name}</Heading>
769
+ <Text fontSize="sm" color="gray.500">
770
+ {fieldLabel}: {String(activity.fields?.[fieldId]?.value ?? '-')}
771
+ </Text>
772
+ </VStack>
773
+ </Box>
774
+ );
775
+ }
776
+ ```
777
+
778
+ ## Stats Card
779
+
780
+ ```typescript
781
+ import { Stat, StatLabel, StatNumber, StatHelpText, Box, useColorModeValue } from '@chakra-ui/react';
782
+
783
+ interface Props {
784
+ label: string;
785
+ value: number | string;
786
+ helpText?: string;
787
+ }
788
+
789
+ function StatsCard({ label, value, helpText }: Props) {
790
+ const bg = useColorModeValue('white', 'gray.700');
791
+
792
+ return (
793
+ <Box p={4} bg={bg} borderRadius="md" shadow="sm">
794
+ <Stat>
795
+ <StatLabel>{label}</StatLabel>
796
+ <StatNumber>{value}</StatNumber>
797
+ {helpText && <StatHelpText>{helpText}</StatHelpText>}
798
+ </Stat>
799
+ </Box>
800
+ );
801
+ }
802
+ ```
803
+ </component-templates>
804
+
805
+ <app-template>
806
+ ## Full App Template
807
+
808
+ ```typescript
809
+ import { useEffect, useState } from 'react';
810
+ import {
811
+ Box,
812
+ Heading,
813
+ Text,
814
+ Spinner,
815
+ Table,
816
+ Thead,
817
+ Tbody,
818
+ Tr,
819
+ Th,
820
+ Td,
821
+ VStack,
822
+ useColorModeValue,
823
+ } from '@chakra-ui/react';
824
+ import useHailer from './hailer/use-hailer';
825
+
826
+ // Field IDs from workflow schema (provided by orchestrator)
827
+ const FIELDS = {
828
+ NAME_FIELD: 'fieldId123',
829
+ STATUS_FIELD: 'fieldId456',
830
+ } as const;
831
+
832
+ interface Activity {
833
+ _id: string;
834
+ name: string;
835
+ fields?: Record<string, { value: unknown }>;
836
+ }
837
+
838
+ function App() {
839
+ const { inside, hailer } = useHailer();
840
+ const [activities, setActivities] = useState<Activity[]>([]);
841
+ const [loading, setLoading] = useState(true);
842
+ const [error, setError] = useState<string | null>(null);
843
+
844
+ const bg = useColorModeValue('gray.50', 'gray.800');
845
+
846
+ // Fetch data when inside Hailer
847
+ useEffect(() => {
848
+ if (!inside) return;
849
+
850
+ async function fetchData() {
851
+ try {
852
+ setLoading(true);
853
+ const data = await hailer.activity.list(
854
+ 'workflowId', // Replace with actual workflow ID
855
+ 'phaseId', // Replace with actual phase ID
856
+ { limit: 100 }
857
+ );
858
+ setActivities(data);
859
+ } catch (err) {
860
+ setError(err instanceof Error ? err.message : 'Failed to load data');
861
+ } finally {
862
+ setLoading(false);
863
+ }
864
+ }
865
+
866
+ fetchData();
867
+ }, [inside]); // IMPORTANT: [inside] not [hailer]
868
+
869
+ // Early return AFTER hooks
870
+ if (!inside) {
871
+ return (
872
+ <Box p={8} textAlign="center">
873
+ <Text>Please open this app inside Hailer</Text>
874
+ </Box>
875
+ );
876
+ }
877
+
878
+ if (loading) {
879
+ return (
880
+ <Box p={8} textAlign="center">
881
+ <Spinner size="xl" />
882
+ </Box>
883
+ );
884
+ }
885
+
886
+ if (error) {
887
+ return (
888
+ <Box p={8} textAlign="center">
889
+ <Text color="red.500">{error}</Text>
890
+ </Box>
891
+ );
892
+ }
893
+
894
+ return (
895
+ <Box p={4} bg={bg} minH="100vh">
896
+ <VStack spacing={4} align="stretch">
897
+ <Heading size="lg">Dashboard</Heading>
898
+
899
+ <Table variant="simple" size="sm">
900
+ <Thead>
901
+ <Tr>
902
+ <Th>Name</Th>
903
+ <Th>Status</Th>
904
+ </Tr>
905
+ </Thead>
906
+ <Tbody>
907
+ {activities.map(activity => (
908
+ <Tr key={activity._id}>
909
+ <Td>{activity.name}</Td>
910
+ <Td>{String(activity.fields?.[FIELDS.STATUS_FIELD]?.value ?? '-')}</Td>
911
+ </Tr>
912
+ ))}
913
+ </Tbody>
914
+ </Table>
915
+ </VStack>
916
+ </Box>
917
+ );
918
+ }
919
+
920
+ export default App;
921
+ ```
922
+ </app-template>
923
+
924
+ <theme-patterns>
925
+ ## Hailer Theme Colors
926
+
927
+ ```typescript
928
+ // Dark mode support - ALWAYS use useColorModeValue
929
+ const bg = useColorModeValue('white', 'gray.700');
930
+ const borderColor = useColorModeValue('gray.200', 'gray.600');
931
+ const textColor = useColorModeValue('gray.800', 'white');
932
+ const mutedColor = useColorModeValue('gray.500', 'gray.400');
933
+
934
+ // Valid color tokens (DO NOT invent tokens)
935
+ // gray.50, gray.100, ..., gray.900
936
+ // white, black
937
+ // red.500, green.500, blue.500, yellow.500, purple.500
938
+ ```
939
+
940
+ ## Light/Dark Mode Safety Rules
941
+
942
+ **DON'T use these patterns:**
943
+ ```typescript
944
+ // ❌ WRONG - brand colors may not be defined in theme
945
+ color="brand.600"
946
+
947
+ // ❌ WRONG - hard-coded white fails on light backgrounds
948
+ <Text color="white">Always white</Text>
949
+
950
+ // ❌ WRONG - assumes light mode background
951
+ <Badge bg="blue.100" color="blue.800">Status</Badge>
952
+ ```
953
+
954
+ **DO use these patterns:**
955
+ ```typescript
956
+ // ✅ CORRECT - explicit colors that exist in Chakra
957
+ color="blue.600"
958
+
959
+ // ✅ CORRECT - adapts to color mode
960
+ <Text color={useColorModeValue('gray.800', 'white')}>Adapts</Text>
961
+
962
+ // ✅ CORRECT - badge adapts to mode
963
+ const badgeBg = useColorModeValue('blue.100', 'blue.700');
964
+ const badgeColor = useColorModeValue('blue.800', 'blue.100');
965
+ <Badge bg={badgeBg} color={badgeColor}>Status</Badge>
966
+ ```
967
+
968
+ **Semantic tokens (define once, use everywhere):**
969
+ ```typescript
970
+ // In theme.ts extendTheme
971
+ semanticTokens: {
972
+ colors: {
973
+ appBg: { _light: 'gray.50', _dark: 'gray.800' },
974
+ cardBg: { _light: 'white', _dark: 'gray.700' },
975
+ textPrimary: { _light: 'gray.800', _dark: 'white' },
976
+ textMuted: { _light: 'gray.500', _dark: 'gray.400' },
977
+ }
978
+ }
979
+
980
+ // Usage - no useColorModeValue needed
981
+ <Box bg="appBg"><Text color="textPrimary">Clean!</Text></Box>
982
+ ```
983
+
984
+ ## Phase-Colored Badges and Bars
985
+
986
+ When displaying phase/status colors that need to work in both modes:
987
+
988
+ ```typescript
989
+ import { useColorMode } from '@chakra-ui/react';
990
+
991
+ // Helper for phase-colored elements
992
+ function usePhaseColors(baseColor: string) {
993
+ const { colorMode } = useColorMode();
994
+
995
+ if (colorMode === 'light') {
996
+ return {
997
+ bg: `${baseColor}.100`,
998
+ color: `${baseColor}.800`,
999
+ borderColor: `${baseColor}.200`,
1000
+ };
1001
+ } else {
1002
+ return {
1003
+ bg: `${baseColor}.700`,
1004
+ color: `${baseColor}.100`,
1005
+ borderColor: `${baseColor}.600`,
1006
+ };
1007
+ }
1008
+ }
1009
+
1010
+ // Usage
1011
+ function PhaseBadge({ phase, color }: { phase: string; color: string }) {
1012
+ const colors = usePhaseColors(color);
1013
+
1014
+ return (
1015
+ <Badge bg={colors.bg} color={colors.color}>
1016
+ {phase}
1017
+ </Badge>
1018
+ );
1019
+ }
1020
+
1021
+ // Event bar with phase color
1022
+ function EventBar({ title, phaseColor }: { title: string; phaseColor: string }) {
1023
+ const colors = usePhaseColors(phaseColor);
1024
+
1025
+ return (
1026
+ <Box
1027
+ px={2}
1028
+ py={1}
1029
+ bg={colors.bg}
1030
+ color={colors.color}
1031
+ borderLeft="3px solid"
1032
+ borderLeftColor={colors.borderColor}
1033
+ borderRadius="sm"
1034
+ >
1035
+ {title}
1036
+ </Box>
1037
+ );
1038
+ }
1039
+ ```
1040
+
1041
+ **Color mapping from Hailer phases:**
1042
+ ```typescript
1043
+ // Map Hailer phase colors to Chakra color names
1044
+ const PHASE_COLOR_MAP: Record<string, string> = {
1045
+ 'blue': 'blue',
1046
+ 'green': 'green',
1047
+ 'red': 'red',
1048
+ 'yellow': 'yellow',
1049
+ 'purple': 'purple',
1050
+ 'orange': 'orange',
1051
+ 'gray': 'gray',
1052
+ };
1053
+ ```
1054
+
1055
+ ## Common UI Patterns
1056
+
1057
+ ```typescript
1058
+ // Page container
1059
+ <Box p={4} bg={useColorModeValue('gray.50', 'gray.800')} minH="100vh">
1060
+
1061
+ // Card
1062
+ <Box p={4} bg={useColorModeValue('white', 'gray.700')} borderRadius="md" shadow="sm">
1063
+
1064
+ // Section with border
1065
+ <Box p={4} border="1px" borderColor={useColorModeValue('gray.200', 'gray.600')} borderRadius="md">
1066
+ ```
1067
+ </theme-patterns>
1068
+
1069
+ <troubleshooting>
1070
+ ## Workflow Permission Errors
1071
+
1072
+ When SDK calls fail with permission/not-allowed errors, check **workflow configuration in Hailer** first.
1073
+
1074
+ **Common symptoms:**
1075
+ - `hailer.activity.move()` fails with permission error
1076
+ - Phase transitions not working
1077
+ - "Not allowed" errors on operations that should work
1078
+
1079
+ **Cause:** Features like phase transitions must be **enabled in workflow configuration** in Hailer. The SDK can't do operations that aren't configured.
1080
+
1081
+ **Debug steps:**
1082
+ 1. Check Hailer UI: Workflow Settings → Phase settings
1083
+ 2. Verify phase transitions are configured (`possibleNextPhase`)
1084
+ 3. Verify user has permission to the workflow/phase
1085
+ 4. Check if the feature (move, archive, etc.) is enabled for that phase
1086
+
1087
+ **Example:** Phase move fails because `possibleNextPhase` doesn't include the target phase.
1088
+ </troubleshooting>
1089
+
1090
+ <build-fixes>
1091
+ ## Common Build Errors
1092
+
1093
+ ### Cannot find module '@hailer/app-sdk'
1094
+ ```typescript
1095
+ // WRONG
1096
+ import { useHailer } from '@hailer/app-sdk';
1097
+
1098
+ // CORRECT
1099
+ import useHailer from './hailer/use-hailer';
1100
+ ```
1101
+
1102
+ ### has no exported member 'useHailer'
1103
+ ```typescript
1104
+ // WRONG - named import
1105
+ import { useHailer } from './hailer/use-hailer';
1106
+
1107
+ // CORRECT - default import
1108
+ import useHailer from './hailer/use-hailer';
1109
+ ```
1110
+
1111
+ ### fields possibly undefined
1112
+ ```typescript
1113
+ // WRONG
1114
+ const value = activity.fields[fieldId].value;
1115
+
1116
+ // CORRECT
1117
+ const value = activity.fields?.[fieldId]?.value;
1118
+ ```
1119
+
1120
+ ### Infinite re-render loop
1121
+ ```typescript
1122
+ // WRONG - hailer changes every render
1123
+ useEffect(() => { ... }, [hailer]);
1124
+
1125
+ // CORRECT - inside is stable
1126
+ useEffect(() => { ... }, [inside]);
1127
+ ```
1128
+
1129
+ ### Hooks order error
1130
+ ```typescript
1131
+ // WRONG - early return before hook
1132
+ if (!inside) return <Text>Error</Text>;
1133
+ const [data, setData] = useState([]); // Error!
1134
+
1135
+ // CORRECT - hooks first, then early return
1136
+ const [data, setData] = useState([]);
1137
+ if (!inside) return <Text>Error</Text>;
1138
+ ```
1139
+
1140
+ ### Hooks inside map() or conditionals
1141
+ ```typescript
1142
+ // WRONG - hook in map
1143
+ {items.map((item) => (
1144
+ <Tr _hover={{ bg: useColorModeValue('gray.50', 'gray.600') }}>
1145
+ ))}
1146
+
1147
+ // CORRECT - hook at top level
1148
+ const hoverBg = useColorModeValue('gray.50', 'gray.600');
1149
+ {items.map((item) => (
1150
+ <Tr _hover={{ bg: hoverBg }}>
1151
+ ))}
1152
+ ```
1153
+
1154
+ ### hailer.activity.move is not a function
1155
+ ```typescript
1156
+ // ❌ WRONG - activity.move() DOES NOT EXIST (common mistake)
1157
+ // await hailer.activity.move(activityId, newPhaseId); // This will fail!
1158
+
1159
+ // CORRECT - use activity.update() with phaseId
1160
+ await hailer.activity.update([
1161
+ {
1162
+ _id: activityId,
1163
+ phaseId: newPhaseId,
1164
+ },
1165
+ ], {});
1166
+ ```
1167
+
1168
+ **Explanation:** Phase transitions in Hailer are done via the `update()` method by setting the `phaseId` field. There is no separate `move()` method in the SDK.
1169
+
1170
+ ### Routing: HashRouter vs BrowserRouter
1171
+
1172
+ **Most Hailer apps don't need routing at all.** Use state-based page switching:
1173
+ ```typescript
1174
+ const [page, setPage] = useState('home');
1175
+ {page === 'home' && <HomePage />}
1176
+ {page === 'settings' && <SettingsPage />}
1177
+ ```
1178
+
1179
+ **If you need routing** (bookmarkable URLs, back button, public apps):
1180
+ ```typescript
1181
+ // ✅ USE HashRouter - works in iframe, no server config
1182
+ import { createHashRouter, RouterProvider } from 'react-router-dom';
1183
+
1184
+ const router = createHashRouter([
1185
+ { path: '/', element: <HomePage /> },
1186
+ { path: '/settings', element: <SettingsPage /> },
1187
+ ]);
1188
+
1189
+ // URLs look like: yourapp.com/#/settings
1190
+ ```
1191
+
1192
+ ```typescript
1193
+ // ⚠️ AVOID BrowserRouter in iframe apps
1194
+ // Can cause "No routes matched location /index.html" errors
1195
+ // because iframe URL contains /index.html path
1196
+ ```
1197
+
1198
+ **When to use routing:**
1199
+ - Public apps with shareable URLs
1200
+ - Complex multi-section apps (like hailer-admin)
1201
+ - Apps where back button navigation matters
1202
+ </build-fixes>
1203
+
1204
+ <sdk-crud>
1205
+ ## SDK Create/Update Formats
1206
+
1207
+ ### activity.create() Format
1208
+
1209
+ **Signature:** `hailer.activity.create(workflowId, activities[], options)`
1210
+
1211
+ **CRITICAL:** Takes array of activities, raw field values (not wrapped).
1212
+
1213
+ ```typescript
1214
+ await hailer.activity.create(WORKFLOW_ID, [
1215
+ {
1216
+ name: 'Activity name',
1217
+ fields: {
1218
+ [FIELD_ID]: 'string or number value', // NOT wrapped in { value: ... }
1219
+ [ACTIVITYLINK_FIELD]: 'linkedActivityId', // Just the ID, not { _id, name }
1220
+ },
1221
+ },
1222
+ ], {});
1223
+ ```
1224
+
1225
+ **Common mistakes:**
1226
+ - Passing single object instead of array
1227
+ - Wrapping field values in `{ value: ... }`
1228
+ - Passing `{ _id, name }` for activitylinks instead of just ID
1229
+
1230
+ ### activity.update() Format
1231
+
1232
+ **Signature:** `hailer.activity.update(activities[], options)`
1233
+
1234
+ ```typescript
1235
+ await hailer.activity.update([
1236
+ {
1237
+ _id: 'activityId',
1238
+ name: 'New name', // optional
1239
+ fields: {
1240
+ [FIELD_ID]: newValue,
1241
+ },
1242
+ phaseId: 'newPhaseId', // optional - move to different phase
1243
+ },
1244
+ ], {});
1245
+ ```
1246
+ </sdk-crud>
1247
+
1248
+ <file-structure>
1249
+ ## Required Files
1250
+
1251
+ ```
1252
+ src/
1253
+ App.tsx # Main component (EDIT THIS)
1254
+ main.tsx # Entry point (NEVER EDIT)
1255
+ hailer/
1256
+ use-hailer.ts # SDK hook (generated)
1257
+ types/
1258
+ index.ts # Type definitions (CREATE)
1259
+ utils/
1260
+ fields.ts # Field helpers (CREATE)
1261
+ constants/
1262
+ fields.ts # Field ID constants (CREATE)
1263
+ ```
1264
+
1265
+ ## Constants File Pattern
1266
+
1267
+ ```typescript
1268
+ // src/constants/fields.ts
1269
+ export const WORKFLOW_ID = 'workflowId123';
1270
+ export const PHASE_ID = 'phaseId456';
1271
+
1272
+ export const FIELDS = {
1273
+ NAME: 'fieldId001',
1274
+ STATUS: 'fieldId002',
1275
+ DATE: 'fieldId003',
1276
+ } as const;
1277
+ ```
1278
+
1279
+ ## Types File Pattern
1280
+
1281
+ ```typescript
1282
+ // src/types/index.ts
1283
+ export interface Activity {
1284
+ _id: string;
1285
+ name: string;
1286
+ fields?: Record<string, { value: unknown }>;
1287
+ created?: number;
1288
+ updated?: number;
1289
+ }
1290
+
1291
+ export interface ActivityLinkValue {
1292
+ _id: string;
1293
+ name: string;
1294
+ }
1295
+
1296
+ export interface UserValue {
1297
+ _id: string;
1298
+ firstname: string;
1299
+ lastname: string;
1300
+ }
1301
+ ```
1302
+ </file-structure>
1303
+
1304
+ <app-manifest>
1305
+ ## App Manifest Configuration
1306
+
1307
+ The `manifest.json` file in app root defines configurable settings exposed in Hailer UI.
1308
+
1309
+ ### config.fields Structure
1310
+
1311
+ ```json
1312
+ {
1313
+ "name": "My App",
1314
+ "version": "1.0.0",
1315
+ "config": {
1316
+ "fields": {
1317
+ "defaultView": {
1318
+ "type": "string",
1319
+ "label": "Default View",
1320
+ "default": "month"
1321
+ },
1322
+ "slotMinTime": {
1323
+ "type": "string",
1324
+ "label": "Day Start Time",
1325
+ "default": "08:00"
1326
+ },
1327
+ "showWeekends": {
1328
+ "type": "boolean",
1329
+ "label": "Show Weekends",
1330
+ "default": true
1331
+ },
1332
+ "itemsPerPage": {
1333
+ "type": "number",
1334
+ "label": "Items Per Page",
1335
+ "default": 25
1336
+ },
1337
+ "enabledFeatures": {
1338
+ "type": "array",
1339
+ "label": "Enabled Features",
1340
+ "default": ["search", "export"]
1341
+ }
1342
+ }
1343
+ }
1344
+ }
1345
+ ```
1346
+
1347
+ ### Field Types
1348
+
1349
+ | Type | Description | Example Default |
1350
+ |------|-------------|-----------------|
1351
+ | `string` | Text value | `"month"` |
1352
+ | `number` | Numeric value | `25` |
1353
+ | `boolean` | True/false toggle | `true` |
1354
+ | `array` | List of values | `["a", "b"]` |
1355
+
1356
+ ### Accessing Config in App
1357
+
1358
+ Config values come through HailerApi callback when app loads inside Hailer:
1359
+
1360
+ ```typescript
1361
+ const { inside, hailer, config } = useHailer();
1362
+
1363
+ // Access configured values
1364
+ const defaultView = config?.defaultView ?? 'month';
1365
+ const showWeekends = config?.showWeekends ?? true;
1366
+ ```
1367
+
1368
+ ### Best Practices
1369
+
1370
+ **DO expose in config.fields:**
1371
+ - User-customizable options (default views, display preferences)
1372
+ - Workflow/phase IDs that vary per installation
1373
+ - Feature toggles
1374
+
1375
+ **DON'T expose in config.fields:**
1376
+ - Values hardcoded in app code (wastes config UI space)
1377
+ - Sensitive data (use environment variables)
1378
+ - Internal constants that users shouldn't change
1379
+
1380
+ **RULE:** If a config field is defined but the app ignores it, remove it from manifest.
1381
+ </app-manifest>
1382
+
1383
+ <public-api>
1384
+ ## Public API (Apps Outside Hailer)
1385
+
1386
+ For apps that run standalone (outside Hailer iframe) without authentication:
1387
+
1388
+ ```typescript
1389
+ // Public insight data
1390
+ const data = await hailer.public.insight.data(insightKey);
1391
+ const objects = await hailer.public.insight.dataAsObject(insightKey);
1392
+
1393
+ // Public forms
1394
+ const formData = await hailer.public.form.data(formsKey);
1395
+ const result = await hailer.public.form.submit(formsKey, formData);
1396
+
1397
+ // Public app config
1398
+ const config = await hailer.public.app.config();
1399
+
1400
+ // Public products
1401
+ const products = await hailer.public.product.list(filter?, options?);
1402
+ const product = await hailer.public.product.get(productId);
1403
+ ```
1404
+
1405
+ **When to use Public API:**
1406
+ - Building external-facing apps (not in Hailer iframe)
1407
+ - Public dashboards using insight keys
1408
+ - Public form submissions
1409
+ - No user authentication available
1410
+
1411
+ **Note:** Public APIs use keys (not IDs) and don't require authentication.
1412
+ </public-api>
1413
+
1414
+ <sdk-reference>
1415
+ ## SDK Type Definitions (For Edge Cases)
1416
+
1417
+ The skill covers common SDK methods. For less common APIs, read the type definitions in any Hailer app project:
1418
+
1419
+ ```
1420
+ node_modules/@hailer/app-sdk/lib/modules/
1421
+ ├── activity.d.ts - list, get, create, update, remove
1422
+ ├── activity/kanban.d.ts - kanban.list, load, updatePriority
1423
+ ├── user.d.ts - current, get, list
1424
+ ├── ui.d.ts - snackbar, activity, insight, files
1425
+ ├── ui/activity.d.ts - open, create, editMultiple
1426
+ ├── ui/snackbar.d.ts - open
1427
+ ├── ui/files.d.ts - uploadFile
1428
+ ├── insight.d.ts - data, list, update
1429
+ ├── process.d.ts - list, get (workflow API uses this)
1430
+ ├── workspace.d.ts - current, list, product methods
1431
+ ├── permission.d.ts - map
1432
+ ├── app.d.ts - config.update, product methods
1433
+ └── public/ - insight, form, product (no auth)
1434
+ ```
1435
+
1436
+ **When to check types:**
1437
+ - Method not documented here → read the relevant `.d.ts` file
1438
+ - Need method signature details → types are the source of truth
1439
+ - New SDK version → types show what's available
1440
+ </sdk-reference>