@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.
- package/.claude/.context-watchdog.json +1 -0
- package/.claude/.session-checked +1 -0
- package/.claude/CLAUDE.md +370 -0
- package/.claude/agents/agent-ada-skill-builder.md +94 -0
- package/.claude/agents/agent-alejandro-function-fields.md +342 -0
- package/.claude/agents/agent-bjorn-config-audit.md +103 -0
- package/.claude/agents/agent-builder-agent-creator.md +130 -0
- package/.claude/agents/agent-code-simplifier.md +53 -0
- package/.claude/agents/agent-dmitri-activity-crud.md +159 -0
- package/.claude/agents/agent-giuseppe-app-builder.md +208 -0
- package/.claude/agents/agent-gunther-mcp-tools.md +39 -0
- package/.claude/agents/agent-helga-workflow-config.md +204 -0
- package/.claude/agents/agent-igor-activity-mover-automation.md +125 -0
- package/.claude/agents/agent-ingrid-doc-templates.md +261 -0
- package/.claude/agents/agent-ivan-monolith.md +154 -0
- package/.claude/agents/agent-kenji-data-reader.md +86 -0
- package/.claude/agents/agent-lars-code-inspector.md +102 -0
- package/.claude/agents/agent-marco-mockup-builder.md +110 -0
- package/.claude/agents/agent-marcus-api-documenter.md +323 -0
- package/.claude/agents/agent-marketplace-publisher.md +280 -0
- package/.claude/agents/agent-marketplace-reviewer.md +309 -0
- package/.claude/agents/agent-permissions-handler.md +208 -0
- package/.claude/agents/agent-simple-writer.md +48 -0
- package/.claude/agents/agent-svetlana-code-review.md +171 -0
- package/.claude/agents/agent-tanya-test-runner.md +333 -0
- package/.claude/agents/agent-ui-designer.md +100 -0
- package/.claude/agents/agent-viktor-sql-insights.md +212 -0
- package/.claude/agents/agent-web-search.md +55 -0
- package/.claude/agents/agent-yevgeni-discussions.md +45 -0
- package/.claude/agents/agent-zara-zapier.md +159 -0
- package/.claude/agents/ragnar.md +68 -0
- package/.claude/commands/app-squad.md +135 -0
- package/.claude/commands/audit-squad.md +158 -0
- package/.claude/commands/autoplan.md +563 -0
- package/.claude/commands/cleanup-squad.md +98 -0
- package/.claude/commands/config-squad.md +106 -0
- package/.claude/commands/crud-squad.md +87 -0
- package/.claude/commands/data-squad.md +97 -0
- package/.claude/commands/debug-squad.md +303 -0
- package/.claude/commands/doc-squad.md +65 -0
- package/.claude/commands/handoff.md +137 -0
- package/.claude/commands/health.md +49 -0
- package/.claude/commands/help.md +29 -0
- package/.claude/commands/help:agents.md +151 -0
- package/.claude/commands/help:commands.md +78 -0
- package/.claude/commands/help:faq.md +79 -0
- package/.claude/commands/help:plugins.md +50 -0
- package/.claude/commands/help:skills.md +93 -0
- package/.claude/commands/help:tools.md +75 -0
- package/.claude/commands/hotfix-squad.md +112 -0
- package/.claude/commands/integration-squad.md +82 -0
- package/.claude/commands/janitor-squad.md +167 -0
- package/.claude/commands/learn-auto.md +120 -0
- package/.claude/commands/learn.md +120 -0
- package/.claude/commands/mcp-list.md +27 -0
- package/.claude/commands/onboard-squad.md +140 -0
- package/.claude/commands/plan-workspace.md +732 -0
- package/.claude/commands/prd.md +130 -0
- package/.claude/commands/project-status.md +82 -0
- package/.claude/commands/publish.md +138 -0
- package/.claude/commands/recap.md +69 -0
- package/.claude/commands/restore.md +64 -0
- package/.claude/commands/review-squad.md +152 -0
- package/.claude/commands/save.md +24 -0
- package/.claude/commands/stats.md +19 -0
- package/.claude/commands/swarm.md +210 -0
- package/.claude/commands/tool-builder.md +39 -0
- package/.claude/commands/ws-pull.md +44 -0
- package/.claude/skills/SDK-activity-patterns/SKILL.md +428 -0
- package/.claude/skills/SDK-document-templates/SKILL.md +1033 -0
- package/.claude/skills/SDK-function-fields/SKILL.md +542 -0
- package/.claude/skills/SDK-generate-skill/SKILL.md +92 -0
- package/.claude/skills/SDK-init-skill/SKILL.md +127 -0
- package/.claude/skills/SDK-insight-queries/SKILL.md +787 -0
- package/.claude/skills/SDK-ws-config-skill/SKILL.md +1139 -0
- package/.claude/skills/agent-structure/SKILL.md +98 -0
- package/.claude/skills/api-documentation-patterns/SKILL.md +474 -0
- package/.claude/skills/chrome-mcp-reference/SKILL.md +370 -0
- package/.claude/skills/delegation-routing/SKILL.md +202 -0
- package/.claude/skills/frontend-design/SKILL.md +254 -0
- package/.claude/skills/hailer-activity-mover/SKILL.md +213 -0
- package/.claude/skills/hailer-api-client/SKILL.md +518 -0
- package/.claude/skills/hailer-app-builder/SKILL.md +1440 -0
- package/.claude/skills/hailer-apps-pictures/SKILL.md +269 -0
- package/.claude/skills/hailer-design-system/SKILL.md +231 -0
- package/.claude/skills/hailer-monolith-automations/SKILL.md +686 -0
- package/.claude/skills/hailer-permissions-system/SKILL.md +121 -0
- package/.claude/skills/hailer-project-protocol/SKILL.md +488 -0
- package/.claude/skills/hailer-rest-api/SKILL.md +61 -0
- package/.claude/skills/hailer-rest-api/hailer-activities.md +184 -0
- package/.claude/skills/hailer-rest-api/hailer-admin.md +473 -0
- package/.claude/skills/hailer-rest-api/hailer-calendar.md +256 -0
- package/.claude/skills/hailer-rest-api/hailer-feed.md +249 -0
- package/.claude/skills/hailer-rest-api/hailer-insights.md +195 -0
- package/.claude/skills/hailer-rest-api/hailer-messaging.md +276 -0
- package/.claude/skills/hailer-rest-api/hailer-workflows.md +283 -0
- package/.claude/skills/insight-join-patterns/SKILL.md +174 -0
- package/.claude/skills/integration-patterns/SKILL.md +421 -0
- package/.claude/skills/json-only-output/SKILL.md +72 -0
- package/.claude/skills/lsp-setup/SKILL.md +160 -0
- package/.claude/skills/mcp-direct-tools/SKILL.md +153 -0
- package/.claude/skills/optional-parameters/SKILL.md +72 -0
- package/.claude/skills/publish-hailer-app/SKILL.md +221 -0
- package/.claude/skills/testing-patterns/SKILL.md +630 -0
- package/.claude/skills/tool-builder/SKILL.md +250 -0
- package/.claude/skills/tool-parameter-usage/SKILL.md +126 -0
- package/.claude/skills/tool-response-verification/SKILL.md +92 -0
- package/.claude/skills/zapier-hailer-patterns/SKILL.md +581 -0
- package/.opencode/agent/agent-ada-skill-builder.md +35 -0
- package/.opencode/agent/agent-alejandro-function-fields.md +39 -0
- package/.opencode/agent/agent-bjorn-config-audit.md +36 -0
- package/.opencode/agent/agent-builder-agent-creator.md +39 -0
- package/.opencode/agent/agent-code-simplifier.md +31 -0
- package/.opencode/agent/agent-dmitri-activity-crud.md +40 -0
- package/.opencode/agent/agent-giuseppe-app-builder.md +37 -0
- package/.opencode/agent/agent-gunther-mcp-tools.md +39 -0
- package/.opencode/agent/agent-helga-workflow-config.md +204 -0
- package/.opencode/agent/agent-igor-activity-mover-automation.md +46 -0
- package/.opencode/agent/agent-ingrid-doc-templates.md +39 -0
- package/.opencode/agent/agent-ivan-monolith.md +46 -0
- package/.opencode/agent/agent-kenji-data-reader.md +53 -0
- package/.opencode/agent/agent-lars-code-inspector.md +28 -0
- package/.opencode/agent/agent-marco-mockup-builder.md +42 -0
- package/.opencode/agent/agent-marcus-api-documenter.md +53 -0
- package/.opencode/agent/agent-marketplace-publisher.md +44 -0
- package/.opencode/agent/agent-marketplace-reviewer.md +42 -0
- package/.opencode/agent/agent-permissions-handler.md +50 -0
- package/.opencode/agent/agent-simple-writer.md +45 -0
- package/.opencode/agent/agent-svetlana-code-review.md +39 -0
- package/.opencode/agent/agent-tanya-test-runner.md +57 -0
- package/.opencode/agent/agent-ui-designer.md +56 -0
- package/.opencode/agent/agent-viktor-sql-insights.md +34 -0
- package/.opencode/agent/agent-web-search.md +42 -0
- package/.opencode/agent/agent-yevgeni-discussions.md +37 -0
- package/.opencode/agent/agent-zara-zapier.md +53 -0
- package/.opencode/commands/app-squad.md +135 -0
- package/.opencode/commands/audit-squad.md +158 -0
- package/.opencode/commands/autoplan.md +563 -0
- package/.opencode/commands/cleanup-squad.md +98 -0
- package/.opencode/commands/config-squad.md +106 -0
- package/.opencode/commands/crud-squad.md +87 -0
- package/.opencode/commands/data-squad.md +97 -0
- package/.opencode/commands/debug-squad.md +303 -0
- package/.opencode/commands/doc-squad.md +65 -0
- package/.opencode/commands/handoff.md +137 -0
- package/.opencode/commands/health.md +49 -0
- package/.opencode/commands/help-agents.md +151 -0
- package/.opencode/commands/help-commands.md +32 -0
- package/.opencode/commands/help-faq.md +29 -0
- package/.opencode/commands/help-plugins.md +28 -0
- package/.opencode/commands/help-skills.md +7 -0
- package/.opencode/commands/help-tools.md +40 -0
- package/.opencode/commands/help.md +28 -0
- package/.opencode/commands/hotfix-squad.md +112 -0
- package/.opencode/commands/integration-squad.md +82 -0
- package/.opencode/commands/janitor-squad.md +167 -0
- package/.opencode/commands/learn-auto.md +120 -0
- package/.opencode/commands/learn.md +120 -0
- package/.opencode/commands/mcp-list.md +27 -0
- package/.opencode/commands/onboard-squad.md +140 -0
- package/.opencode/commands/plan-workspace.md +732 -0
- package/.opencode/commands/prd.md +131 -0
- package/.opencode/commands/project-status.md +82 -0
- package/.opencode/commands/publish.md +138 -0
- package/.opencode/commands/recap.md +69 -0
- package/.opencode/commands/restore.md +64 -0
- package/.opencode/commands/review-squad.md +152 -0
- package/.opencode/commands/save.md +24 -0
- package/.opencode/commands/stats.md +19 -0
- package/.opencode/commands/swarm.md +210 -0
- package/.opencode/commands/tool-builder.md +39 -0
- package/.opencode/commands/ws-pull.md +44 -0
- package/.opencode/opencode.json +21 -0
- package/package.json +1 -1
- package/scripts/postinstall.cjs +64 -0
- 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>
|