@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,174 @@
1
+ ---
2
+ name: insight-join-patterns
3
+ description: Correct JOIN syntax for Hailer insights with ActivityLink fields
4
+ version: 1.0.0
5
+ triggers: JOIN query errors, missing columns, NULL results in insight queries
6
+ ---
7
+
8
+ **Prerequisite:** Before using JOINs, review `SDK-insight-queries` skill for basic insight syntax and single-workflow queries.
9
+
10
+ <problem>
11
+ When joining workflows with ActivityLink fields in Hailer insights, you must:
12
+ 1. Include `_id` meta field in BOTH source definitions
13
+ 2. Join ON the activitylink field value equals target _id
14
+ 3. Use the activitylink fieldId (NOT the key) for the JOIN condition
15
+ </problem>
16
+
17
+ <rules>
18
+ - Both workflows need `{ name: 'id', meta: '_id' }` in their fields array
19
+ - JOIN condition: `source1.activityLinkFieldName = source2.id`
20
+ - Use LEFT JOIN for optional relationships (activitylink can be null)
21
+ - Use INNER JOIN only when relationship must exist
22
+ </rules>
23
+
24
+ <correct>
25
+ ```javascript
26
+ // Players workflow has "club" field (activitylink to Clubs workflow)
27
+ {
28
+ sources: [
29
+ {
30
+ name: 'p',
31
+ workflowId: '68446dc05b30685f67c6fcd4',
32
+ fields: [
33
+ { name: 'player_name', meta: 'name' },
34
+ { name: 'id', meta: '_id' }, // Required!
35
+ { name: 'club', fieldId: '684d5e45...' } // ActivityLink field
36
+ ]
37
+ },
38
+ {
39
+ name: 'c',
40
+ workflowId: '691ea936ccb6bdeebc0cbf77',
41
+ fields: [
42
+ { name: 'club_name', meta: 'name' },
43
+ { name: 'id', meta: '_id' } // Required!
44
+ ]
45
+ }
46
+ ],
47
+ query: 'SELECT p.player_name, c.club_name FROM p LEFT JOIN c ON p.club = c.id'
48
+ }
49
+ ```
50
+ </correct>
51
+
52
+ <wrong>
53
+ ```javascript
54
+ // ❌ WRONG - Missing _id in clubs source
55
+ {
56
+ sources: [
57
+ {
58
+ name: 'p',
59
+ workflowId: 'players-id',
60
+ fields: [
61
+ { name: 'player_name', meta: 'name' },
62
+ { name: 'id', meta: '_id' },
63
+ { name: 'club', fieldId: 'club-field-id' }
64
+ ]
65
+ },
66
+ {
67
+ name: 'c',
68
+ workflowId: 'clubs-id',
69
+ fields: [
70
+ { name: 'club_name', meta: 'name' }
71
+ // Missing: { name: 'id', meta: '_id' }
72
+ ]
73
+ }
74
+ ],
75
+ query: 'SELECT p.player_name, c.club_name FROM p LEFT JOIN c ON p.club = c.id'
76
+ }
77
+ // Error: "no such column: c.id"
78
+ ```
79
+ </wrong>
80
+
81
+ <examples>
82
+ ### Three-Way JOIN (Tasks -> Topics -> Projects)
83
+ ```javascript
84
+ {
85
+ sources: [
86
+ {
87
+ name: 't',
88
+ workflowId: 'tasks-workflow-id',
89
+ fields: [
90
+ { name: 'task_name', meta: 'name' },
91
+ { name: 'id', meta: '_id' },
92
+ { name: 'topic', fieldId: 'topic-field-id' }
93
+ ]
94
+ },
95
+ {
96
+ name: 'top',
97
+ workflowId: 'topics-workflow-id',
98
+ fields: [
99
+ { name: 'topic_name', meta: 'name' },
100
+ { name: 'id', meta: '_id' },
101
+ { name: 'project', fieldId: 'project-field-id' }
102
+ ]
103
+ },
104
+ {
105
+ name: 'p',
106
+ workflowId: 'projects-workflow-id',
107
+ fields: [
108
+ { name: 'project_name', meta: 'name' },
109
+ { name: 'id', meta: '_id' }
110
+ ]
111
+ }
112
+ ],
113
+ query: `
114
+ SELECT t.task_name, top.topic_name, p.project_name
115
+ FROM t
116
+ LEFT JOIN top ON t.topic = top.id
117
+ LEFT JOIN p ON top.project = p.id
118
+ `
119
+ }
120
+ ```
121
+
122
+ ### Aggregation with JOIN
123
+ ```javascript
124
+ {
125
+ sources: [
126
+ {
127
+ name: 'matches',
128
+ workflowId: 'matches-workflow-id',
129
+ fields: [
130
+ { name: 'match_date', fieldId: 'date-field-id' },
131
+ { name: 'id', meta: '_id' },
132
+ { name: 'home_team', fieldId: 'home-team-field-id' }
133
+ ]
134
+ },
135
+ {
136
+ name: 'teams',
137
+ workflowId: 'teams-workflow-id',
138
+ fields: [
139
+ { name: 'team_name', meta: 'name' },
140
+ { name: 'id', meta: '_id' }
141
+ ]
142
+ }
143
+ ],
144
+ query: `
145
+ SELECT teams.team_name, COUNT(*) as match_count
146
+ FROM matches
147
+ LEFT JOIN teams ON matches.home_team = teams.id
148
+ GROUP BY teams.team_name
149
+ `
150
+ }
151
+ ```
152
+ </examples>
153
+
154
+ <troubleshooting>
155
+ **Error: "no such column: c.id"**
156
+ - Missing `{ name: 'id', meta: '_id' }` in target workflow source
157
+
158
+ **Error: "no such column: p.club"**
159
+ - ActivityLink field not included in source fields array
160
+ - Check you used correct fieldId from `get_workflow_schema`
161
+
162
+ **NULL results for joined data**
163
+ - ActivityLink field is empty/null for some activities (expected with LEFT JOIN)
164
+ - Use INNER JOIN if you only want activities with relationships
165
+ </troubleshooting>
166
+
167
+ <checklist>
168
+ Before creating an insight with JOINs:
169
+ - [ ] Both workflow sources include `{ name: 'id', meta: '_id' }`
170
+ - [ ] ActivityLink field uses `fieldId` (NOT `key`)
171
+ - [ ] JOIN condition uses column names from sources (e.g., `p.club = c.id`)
172
+ - [ ] Using LEFT JOIN (unless relationship required)
173
+ - [ ] Tested with `preview_insight` first
174
+ </checklist>
@@ -0,0 +1,421 @@
1
+ ---
2
+ name: integration-patterns
3
+ description: Hailer integration microservice patterns - activity movers, webhooks, SCIM, Kafka consumers
4
+ version: 1.0.0
5
+ triggers: Build integration, activity mover, webhook handler, SCIM endpoint, Kafka consumer
6
+ ---
7
+
8
+ # Integration Patterns
9
+
10
+ ## Architecture Overview
11
+
12
+ ```
13
+ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
14
+ │ External │ │ Integration │ │ Hailer API │
15
+ │ System │────▶│ Service │────▶│ │
16
+ │ (Kafka/HTTP) │ │ (Node.js) │ │ (WebSocket) │
17
+ └─────────────────┘ └─────────────────┘ └─────────────────┘
18
+
19
+
20
+ ┌─────────────────┐
21
+ │ Logging & │
22
+ │ Monitoring │
23
+ └─────────────────┘
24
+ ```
25
+
26
+ ---
27
+
28
+ ## 1. Activity Mover Pattern
29
+
30
+ Monitors activities and moves linked activities when triggers fire.
31
+
32
+ ### Configuration Structure
33
+
34
+ ```typescript
35
+ interface ActivityMoverConfig {
36
+ logDiscussionId: string; // Discussion for audit logging
37
+ email: string; // Integration user email
38
+ password: string; // Or "ENV:HAILER_PASSWORD"
39
+ project: string; // Project identifier
40
+ triggers: Trigger[];
41
+ }
42
+
43
+ interface Trigger {
44
+ processId: string; // Source workflow ID
45
+ phaseId: string; // Phase that triggers action
46
+ metaDataId: string; // Hidden field for tracking ("Seen"/"Not seen")
47
+ linkedProcesses: LinkedProcess[];
48
+ }
49
+
50
+ interface LinkedProcess {
51
+ processId: string; // Target workflow ID
52
+ sourcePhases: string[]; // Move FROM these phases
53
+ targetPhase: string; // Move TO this phase
54
+ }
55
+ ```
56
+
57
+ ### Example Config
58
+
59
+ ```json
60
+ {
61
+ "logDiscussionId": "694c9536acfa30f6df13201b",
62
+ "email": "integration@workspace.com",
63
+ "password": "ENV:HAILER_PASSWORD",
64
+ "project": "orders-sync",
65
+ "triggers": [
66
+ {
67
+ "processId": "67dc1b7d3d2c9f6cf9a5468d",
68
+ "phaseId": "67dc1b7d3d2c9f6cf9a546c4",
69
+ "metaDataId": "67e697da6ada809b961c35b5",
70
+ "linkedProcesses": [
71
+ {
72
+ "processId": "67dc1b7d3d2c9f6cf9a54690",
73
+ "sourcePhases": ["67dc1b7d3d2c9f6cf9a54691", "67dc1b7d3d2c9f6cf9a54692"],
74
+ "targetPhase": "67dc1b7d3d2c9f6cf9a54695"
75
+ }
76
+ ]
77
+ }
78
+ ]
79
+ }
80
+ ```
81
+
82
+ ### Hailer Setup Requirements
83
+
84
+ 1. **Metadata field** on trigger workflow:
85
+ - Type: TEXT (hidden)
86
+ - Default value: "Not seen"
87
+ - Used to track processed activities
88
+
89
+ 2. **Link field** connecting workflows:
90
+ - Activity link from trigger workflow to target workflow
91
+
92
+ 3. **Integration user**:
93
+ - Dedicated user account
94
+ - Edit permissions on both workflows
95
+
96
+ 4. **Discussion activity**:
97
+ - For audit logging
98
+ - Integration user must have access
99
+
100
+ ### Core Logic
101
+
102
+ ```typescript
103
+ // Listen for activity updates
104
+ client.on('activities.updated', async (data) => {
105
+ for (const trigger of config.triggers) {
106
+ if (data.processId === trigger.processId && data.phaseId === trigger.phaseId) {
107
+ await processTrigger(data.activityId, trigger);
108
+ }
109
+ }
110
+ });
111
+
112
+ async function processTrigger(activityId: string, trigger: Trigger) {
113
+ // Check if already processed
114
+ const activity = await client.request('v3.activity.get', [activityId]);
115
+ if (activity.fields[trigger.metaDataId]?.value === 'Seen') return;
116
+
117
+ // Get linked activities
118
+ const linked = await client.request('v3.activity.linkedFrom.overview', [activityId]);
119
+
120
+ // Move matching activities
121
+ for (const process of trigger.linkedProcesses) {
122
+ const toMove = linked.filter(a =>
123
+ a.processId === process.processId &&
124
+ process.sourcePhases.includes(a.phaseId)
125
+ );
126
+
127
+ if (toMove.length > 0) {
128
+ await client.request('v3.activity.updateMany', [
129
+ toMove.map(a => a._id),
130
+ { phaseId: process.targetPhase }
131
+ ]);
132
+ }
133
+ }
134
+
135
+ // Mark as processed
136
+ await client.request('v3.activity.update', [activityId, {
137
+ fields: { [trigger.metaDataId]: 'Seen' }
138
+ }]);
139
+ }
140
+ ```
141
+
142
+ ---
143
+
144
+ ## 2. Webhook Handler Pattern
145
+
146
+ HTTP endpoints that trigger Hailer operations.
147
+
148
+ ### Express Setup
149
+
150
+ ```typescript
151
+ import express from 'express';
152
+ import crypto from 'crypto';
153
+
154
+ const app = express();
155
+ app.use(express.json());
156
+
157
+ // Webhook signature validation
158
+ function validateSignature(req: express.Request, secret: string): boolean {
159
+ const signature = req.headers['x-webhook-signature'];
160
+ const payload = JSON.stringify(req.body);
161
+ const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex');
162
+ return signature === expected;
163
+ }
164
+
165
+ // Webhook endpoint
166
+ app.post('/api/v1/webhook/:type', async (req, res) => {
167
+ const { type } = req.params;
168
+
169
+ if (!validateSignature(req, config.webhookSecret)) {
170
+ return res.status(401).json({ error: 'Invalid signature' });
171
+ }
172
+
173
+ try {
174
+ switch (type) {
175
+ case 'order.created':
176
+ await handleOrderCreated(req.body);
177
+ break;
178
+ case 'order.updated':
179
+ await handleOrderUpdated(req.body);
180
+ break;
181
+ default:
182
+ logger.warn({ type }, 'Unknown webhook type');
183
+ }
184
+ res.json({ status: 'ok' });
185
+ } catch (error) {
186
+ logger.error({ error, type }, 'Webhook processing failed');
187
+ res.status(500).json({ error: 'Processing failed' });
188
+ }
189
+ });
190
+ ```
191
+
192
+ ### Webhook Handler Example
193
+
194
+ ```typescript
195
+ async function handleOrderCreated(payload: OrderPayload) {
196
+ // Map external data to Hailer fields
197
+ const fields = {
198
+ [FieldIds.external_id]: payload.orderId,
199
+ [FieldIds.customer_name]: payload.customer.name,
200
+ [FieldIds.total_amount]: payload.total,
201
+ [FieldIds.order_date]: new Date(payload.createdAt).getTime()
202
+ };
203
+
204
+ // Create activity in Hailer
205
+ const result = await hailerClient.createActivity(
206
+ WorkflowIds.orders,
207
+ PhaseIds.new,
208
+ fields
209
+ );
210
+
211
+ logger.info({ orderId: payload.orderId, activityId: result._id }, 'Order created in Hailer');
212
+ }
213
+ ```
214
+
215
+ ---
216
+
217
+ ## 3. SCIM 2.0 Pattern
218
+
219
+ User provisioning from identity providers (Microsoft Entra, Okta).
220
+
221
+ ### Endpoints
222
+
223
+ | Method | Path | Description |
224
+ |--------|------|-------------|
225
+ | POST | /scim/v2/Users | Create user |
226
+ | GET | /scim/v2/Users/:id | Get user |
227
+ | PATCH | /scim/v2/Users/:id | Update user |
228
+ | DELETE | /scim/v2/Users/:id | Deactivate user |
229
+ | GET | /scim/v2/Users | List/filter users |
230
+
231
+ ### User Mapping
232
+
233
+ ```typescript
234
+ interface ScimUser {
235
+ schemas: string[];
236
+ userName: string;
237
+ name: { givenName: string; familyName: string };
238
+ emails: { value: string; primary: boolean }[];
239
+ active: boolean;
240
+ roles?: { value: string }[];
241
+ }
242
+
243
+ function mapScimToHailer(scim: ScimUser): HailerUserPayload {
244
+ return {
245
+ email: scim.emails.find(e => e.primary)?.value || scim.userName,
246
+ firstName: scim.name.givenName,
247
+ lastName: scim.name.familyName,
248
+ active: scim.active,
249
+ teams: mapRolesToTeams(scim.roles)
250
+ };
251
+ }
252
+
253
+ function mapRolesToTeams(roles?: { value: string }[]): string[] {
254
+ const teamMapping: Record<string, string> = {
255
+ 'admin': TeamIds.admins,
256
+ 'manager': TeamIds.managers,
257
+ 'user': TeamIds.users
258
+ };
259
+ return (roles || [])
260
+ .map(r => teamMapping[r.value])
261
+ .filter(Boolean);
262
+ }
263
+ ```
264
+
265
+ ### SCIM Response Format
266
+
267
+ ```typescript
268
+ function toScimUser(hailerUser: HailerUser, baseUrl: string): ScimUser {
269
+ return {
270
+ schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'],
271
+ id: hailerUser._id,
272
+ userName: hailerUser.email,
273
+ name: {
274
+ givenName: hailerUser.firstName,
275
+ familyName: hailerUser.lastName
276
+ },
277
+ emails: [{ value: hailerUser.email, primary: true }],
278
+ active: hailerUser.active,
279
+ meta: {
280
+ resourceType: 'User',
281
+ location: `${baseUrl}/scim/v2/Users/${hailerUser._id}`
282
+ }
283
+ };
284
+ }
285
+ ```
286
+
287
+ ---
288
+
289
+ ## 4. Kafka Consumer Pattern
290
+
291
+ Event-driven processing from message queues.
292
+
293
+ ### Consumer Setup
294
+
295
+ ```typescript
296
+ import { Kafka, Consumer, EachMessagePayload } from 'kafkajs';
297
+
298
+ const kafka = new Kafka({
299
+ clientId: 'hailer-integration',
300
+ brokers: config.kafkaBrokers,
301
+ ssl: {
302
+ ca: [fs.readFileSync(config.caCertPath)],
303
+ key: fs.readFileSync(config.keyPath),
304
+ cert: fs.readFileSync(config.certPath)
305
+ }
306
+ });
307
+
308
+ const consumer = kafka.consumer({ groupId: 'hailer-consumer-group' });
309
+
310
+ async function startConsumer() {
311
+ await consumer.connect();
312
+ await consumer.subscribe({ topic: 'events', fromBeginning: false });
313
+
314
+ await consumer.run({
315
+ eachMessage: async ({ topic, partition, message }: EachMessagePayload) => {
316
+ const event = JSON.parse(message.value.toString());
317
+
318
+ try {
319
+ await processEvent(event);
320
+ logger.info({ eventId: event.id, offset: message.offset }, 'Event processed');
321
+ } catch (error) {
322
+ logger.error({ error, event }, 'Event processing failed');
323
+ await sendToDeadLetterQueue(message);
324
+ }
325
+ }
326
+ });
327
+ }
328
+ ```
329
+
330
+ ### Event Processing
331
+
332
+ ```typescript
333
+ async function processEvent(event: ExternalEvent) {
334
+ switch (event.type) {
335
+ case 'student.created':
336
+ await createStudent(event.data);
337
+ break;
338
+ case 'student.updated':
339
+ await updateStudent(event.data);
340
+ break;
341
+ case 'student.deleted':
342
+ await deactivateStudent(event.data);
343
+ break;
344
+ default:
345
+ logger.warn({ eventType: event.type }, 'Unknown event type');
346
+ }
347
+ }
348
+ ```
349
+
350
+ ---
351
+
352
+ ## Common Patterns
353
+
354
+ ### Retry with Exponential Backoff
355
+
356
+ ```typescript
357
+ async function withRetry<T>(
358
+ fn: () => Promise<T>,
359
+ maxRetries: number = 3,
360
+ baseDelay: number = 1000
361
+ ): Promise<T> {
362
+ let lastError: Error;
363
+
364
+ for (let i = 0; i < maxRetries; i++) {
365
+ try {
366
+ return await fn();
367
+ } catch (error) {
368
+ lastError = error;
369
+ const delay = baseDelay * Math.pow(2, i);
370
+ logger.warn({ attempt: i + 1, delay, error: error.message }, 'Retrying...');
371
+ await sleep(delay);
372
+ }
373
+ }
374
+
375
+ throw lastError;
376
+ }
377
+ ```
378
+
379
+ ### Graceful Shutdown
380
+
381
+ ```typescript
382
+ async function gracefulShutdown(signal: string) {
383
+ logger.info({ signal }, 'Shutdown signal received');
384
+
385
+ // Stop accepting new requests
386
+ server.close();
387
+
388
+ // Disconnect from Hailer
389
+ await hailerClient.disconnect();
390
+
391
+ // Disconnect from Kafka
392
+ await consumer.disconnect();
393
+
394
+ logger.info('Graceful shutdown complete');
395
+ process.exit(0);
396
+ }
397
+
398
+ process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
399
+ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
400
+ ```
401
+
402
+ ### Health Check
403
+
404
+ ```typescript
405
+ app.get('/api/v1/health', async (req, res) => {
406
+ const checks = {
407
+ hailer: await checkHailerConnection(),
408
+ kafka: await checkKafkaConnection(),
409
+ uptime: process.uptime(),
410
+ memory: process.memoryUsage(),
411
+ lastActivity: lastActivityTimestamp
412
+ };
413
+
414
+ const healthy = checks.hailer.connected && checks.kafka.connected;
415
+ res.status(healthy ? 200 : 503).json({
416
+ status: healthy ? 'healthy' : 'unhealthy',
417
+ timestamp: new Date().toISOString(),
418
+ checks
419
+ });
420
+ });
421
+ ```
@@ -0,0 +1,72 @@
1
+ ---
2
+ name: json-only-output
3
+ description: Fix agents adding prose after JSON responses
4
+ version: 1.0.1
5
+ triggers: Agent outputs explanation text after valid JSON
6
+ ---
7
+
8
+ # JSON Only Output
9
+
10
+ <purpose>
11
+ Ensure agents output ONLY valid JSON with no prose, explanations, or commentary after the closing brace.
12
+ </purpose>
13
+
14
+ <why-this-happens>
15
+ ## Why Agents Add Prose
16
+
17
+ LLMs have a natural tendency to be helpful and explanatory. After completing a task, they want to:
18
+ - Explain what they did
19
+ - Suggest next steps
20
+ - Provide context
21
+ - Confirm their understanding
22
+
23
+ This is normally good behavior, but for **subagents** returning structured data to an orchestrator, it breaks JSON parsing.
24
+
25
+ **The `<protocol>` section isn't enough** because:
26
+ - LLMs treat protocol as "guidelines" not hard rules
27
+ - The helpful instinct overrides weak instructions
28
+ - Protocol is at the end of the prompt, less salient
29
+
30
+ **Why identity/rules work:**
31
+ - `<identity>` shapes the agent's core persona ("I output JSON. Full stop.")
32
+ - `<rules>` are processed as constraints, not suggestions
33
+ - Positioned early in prompt, higher salience
34
+ - Explicit prohibition is stronger than implicit expectation
35
+ </why-this-happens>
36
+
37
+ <patterns>
38
+ ## Pattern 1: Stop at the Closing Brace
39
+
40
+ Output JSON. Full stop. Nothing after the closing brace.
41
+
42
+ ## Pattern 2: Agent Configuration Fix
43
+
44
+ 1. Add to `<identity>`: "Output JSON. Full stop."
45
+ 2. Add to `<rules>`: **JSON ONLY** - Output closing brace, then STOP. Zero prose after JSON.
46
+ 3. Protocol section is NOT enough - agents ignore it without identity/rules reinforcement.
47
+
48
+ ## Pattern 3: Include Summary Inside JSON
49
+
50
+ If context is helpful, include it IN the JSON `summary` field, not after the JSON.
51
+ </patterns>
52
+
53
+ <examples>
54
+ ## Example 1: Correct - Pure JSON Output
55
+
56
+ ```json
57
+ {"status":"success","result":{"fields":["taskName","priority"]},"summary":"Read 2 fields"}
58
+ ```
59
+ **STOP HERE. Nothing after closing brace.**
60
+
61
+ ## Example 2: Wrong - Prose After JSON
62
+
63
+ ```json
64
+ {"status":"success","result":{"fields":["taskName","priority"]},"summary":"Read 2 fields"}
65
+ ```
66
+
67
+ The workflow has 2 fields defined in workspace/Tasks_123/fields.ts. You can now use these field IDs with dmitri for activity creation.
68
+
69
+ **Action Required**: Run `npm run pull` to refresh.
70
+
71
+ This violates JSON-only protocol by adding explanation text AFTER the valid JSON response.
72
+ </examples>