@zibby/workflow-templates 0.2.1 → 0.4.1

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 (38) hide show
  1. package/browser-test-automation/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  2. package/browser-test-automation/package.json +1 -0
  3. package/code-analysis/graph.js +5 -4
  4. package/code-analysis/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  5. package/code-analysis/nodes/analyze-ticket-node.js +9 -2
  6. package/code-analysis/nodes/generate-code-node.js +27 -11
  7. package/code-analysis/nodes/setup-node.js +50 -130
  8. package/code-analysis/nodes/utils/get-repo-path.js +32 -0
  9. package/code-analysis/prompts/setup.md +71 -0
  10. package/code-analysis/state.js +16 -0
  11. package/generate-test-cases/graph.mjs +11 -1
  12. package/generate-test-cases/nodes/setup-node.js +32 -130
  13. package/generate-test-cases/prompts/setup.md +50 -0
  14. package/generate-test-cases/state.js +12 -0
  15. package/index.js +136 -0
  16. package/notify-lark/README.md +88 -0
  17. package/notify-lark/graph.mjs +43 -0
  18. package/notify-lark/icon.png +0 -0
  19. package/notify-lark/nodes/notify-lark-node.js +290 -0
  20. package/notify-lark/package.json +18 -0
  21. package/notify-lark/state.js +75 -0
  22. package/notify-slack/README.md +94 -0
  23. package/notify-slack/graph.mjs +51 -0
  24. package/notify-slack/icon.png +0 -0
  25. package/notify-slack/nodes/notify-slack-node.js +238 -0
  26. package/notify-slack/package.json +18 -0
  27. package/notify-slack/state.js +93 -0
  28. package/package.json +12 -3
  29. package/sentry-triage/graph.mjs +81 -0
  30. package/sentry-triage/icon.png +0 -0
  31. package/sentry-triage/nodes/classify-node.js +38 -0
  32. package/sentry-triage/nodes/dispatch-alerts-node.js +191 -0
  33. package/sentry-triage/nodes/fetch-issues-node.js +52 -0
  34. package/sentry-triage/nodes/filter-noise-node.js +112 -0
  35. package/sentry-triage/package.json +18 -0
  36. package/sentry-triage/prompts/classify.md +76 -0
  37. package/sentry-triage/prompts/fetch-issues.md +66 -0
  38. package/sentry-triage/state.js +134 -0
@@ -1,142 +1,44 @@
1
1
  /**
2
- * Setup Node - Clone repositories and initialize git baseline
3
- * Used by: analysisGraph, implementationGraph
2
+ * Setup Node — LLM-driven workspace bootstrap.
3
+ *
4
+ * Mirrors code-analysis/setup-node.js. The agent receives the `git`
5
+ * skill (git_checkout, git_list_repos, git_explore) plus Bash, and is
6
+ * prompted to clone each repo from state.repos and initialize a
7
+ * baseline commit at state.workspace for diff tracking. The output
8
+ * lands at state.setup.{clonedRepos[], baselineCommit} where downstream
9
+ * nodes read it.
10
+ *
11
+ * Prompt is loaded by graph.mjs from prompts/setup.md so users can
12
+ * customize setup behavior without editing this file.
4
13
  */
5
14
 
6
- import { spawn } from 'child_process';
7
- import { join } from 'path';
8
- import { z } from 'zod';
15
+ import { readFileSync, existsSync } from 'fs';
16
+ import { dirname, join } from 'path';
17
+ import { fileURLToPath } from 'url';
18
+ import { z, SKILLS } from '@zibby/core';
19
+
20
+ const __dirname = dirname(fileURLToPath(import.meta.url));
21
+ const promptPath = join(__dirname, '..', 'prompts', 'setup.md');
22
+ const setupPrompt = existsSync(promptPath)
23
+ ? readFileSync(promptPath, 'utf-8')
24
+ : '';
9
25
 
10
26
  const SetupOutputSchema = z.object({
11
- success: z.boolean(),
27
+ success: z.boolean().describe('true if all repos cloned + baseline initialized'),
12
28
  clonedRepos: z.array(z.object({
13
- name: z.string(),
14
- path: z.string(),
15
- isPrimary: z.boolean().optional()
16
- })),
17
- baselineCommit: z.string()
29
+ name: z.string().describe('Repo name (matches state.repos[].name)'),
30
+ path: z.string().describe('Absolute local path where the repo was cloned'),
31
+ isPrimary: z.boolean().optional().describe('True for the project\'s primary repo'),
32
+ })).describe('Every repo from state.repos must appear here once'),
33
+ baselineCommit: z.string().describe('git rev-parse HEAD at state.workspace after baseline commit'),
18
34
  });
19
35
 
20
36
  export const setupNode = {
21
37
  name: 'setup',
38
+ // SKILLS.GIT gives the LLM clone tools that auto-auth against GitHub
39
+ // OR GitLab — either connected integration satisfies the workflow.
40
+ skills: [SKILLS.GIT],
22
41
  outputSchema: SetupOutputSchema,
23
- execute: async (state) => {
24
- console.log('\n🔧 Setting up environment...');
25
-
26
- const { workspace, repos, githubToken } = state;
27
- const gitlabToken = process.env.GITLAB_TOKEN || '';
28
- const gitlabUrl = process.env.GITLAB_URL || '';
29
-
30
- // DEBUG: Log token status
31
- console.log(`🔑 GitHub Token: ${githubToken ? 'Present' : 'MISSING'}`);
32
- console.log(`🔑 GitLab Token: ${gitlabToken ? 'Present' : 'MISSING'}`);
33
- if (gitlabUrl) console.log(`🔑 GitLab URL: ${gitlabUrl}`);
34
-
35
- // Log environment
36
- console.log('Container: ECS Fargate');
37
- console.log('Memory: 4GB');
38
- console.log('CPU: 2 vCPU');
39
- console.log('Tools: Node.js, Git, Cursor CLI, Zibby CLI');
40
- console.log(`Working directory: ${workspace}`);
41
-
42
- // Clone repositories
43
- console.log('\n📦 Cloning repositories...');
44
-
45
- const clonedRepos = [];
46
- for (const repo of repos) {
47
- console.log(`Cloning ${repo.name}...`);
48
-
49
- const repoDir = join(workspace, repo.name);
50
-
51
- // Use token for authentication based on provider
52
- let cloneUrl = repo.url;
53
- let cloneEnv = {};
54
- const isGitlab = repo.provider === 'gitlab' || (gitlabUrl && repo.url.includes(new URL(gitlabUrl).host));
55
- const isGithub = repo.provider === 'github' || repo.url.includes('github.com');
56
-
57
- if (isGithub && githubToken) {
58
- cloneUrl = repo.url.replace('https://github.com', `https://x-access-token:${githubToken}@github.com`);
59
- cloneEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_ASKPASS: 'echo' };
60
- } else if (isGitlab && gitlabToken && gitlabUrl) {
61
- try {
62
- const gitlabHost = new URL(gitlabUrl).host;
63
- cloneUrl = repo.url.replace(`https://${gitlabHost}`, `https://oauth2:${gitlabToken}@${gitlabHost}`);
64
- } catch (e) {
65
- console.warn(`⚠️ Failed to parse GITLAB_URL: ${e.message}`);
66
- }
67
- cloneEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_ASKPASS: 'echo' };
68
- }
69
-
70
- // Shallow clone with progress output (async, non-blocking)
71
- await execCommand(
72
- `git clone --progress --depth 1 --branch ${repo.branch} "${cloneUrl}" "${repoDir}"`,
73
- workspace,
74
- cloneEnv
75
- );
76
- console.log(`✓ Cloned ${repo.name} on branch ${repo.branch}`);
77
-
78
- clonedRepos.push({
79
- name: repo.name,
80
- path: repoDir,
81
- isPrimary: repo.isPrimary
82
- });
83
- }
84
-
85
- // Initialize git in workspace for diff tracking
86
- await execCommand('git init', workspace);
87
- await execCommand('git config user.email "zibby@agent.com"', workspace);
88
- await execCommand('git config user.name "Zibby Agent"', workspace);
89
- await execCommand('git add .', workspace);
90
- await execCommand('git commit --allow-empty -m "baseline"', workspace);
91
-
92
- console.log('✅ Environment ready');
93
-
94
- const baselineCommit = await execCommand('git rev-parse HEAD', workspace);
95
-
96
- return {
97
- success: true,
98
- clonedRepos,
99
- baselineCommit: baselineCommit.trim()
100
- };
101
- }
42
+ timeout: 5 * 60 * 1000,
43
+ prompt: setupPrompt,
102
44
  };
103
-
104
- // Async version using spawn - streams output in real-time, doesn't block event loop
105
- async function execCommand(command, cwd, env = {}) {
106
- return new Promise((resolve, reject) => {
107
- const proc = spawn(command, {
108
- cwd,
109
- shell: true,
110
- env: Object.keys(env).length > 0 ? env : process.env
111
- });
112
-
113
- let stdout = '';
114
- let stderr = '';
115
-
116
- // Stream stdout as it comes (triggers middleware setInterval!)
117
- proc.stdout.on('data', (data) => {
118
- const output = data.toString();
119
- stdout += output;
120
- console.log(output.trimEnd());
121
- });
122
-
123
- // Stream stderr as it comes
124
- proc.stderr.on('data', (data) => {
125
- const output = data.toString();
126
- stderr += output;
127
- console.log(output.trimEnd());
128
- });
129
-
130
- proc.on('close', (code) => {
131
- if (code !== 0) {
132
- reject(new Error(`Command failed with exit code ${code}: ${command}`));
133
- } else {
134
- resolve(stdout || stderr || '');
135
- }
136
- });
137
-
138
- proc.on('error', (err) => {
139
- reject(new Error(`Command error: ${command} - ${err.message}`));
140
- });
141
- });
142
- }
@@ -0,0 +1,50 @@
1
+ # Setup — clone repos + initialize baseline
2
+
3
+ You are setting up the workspace for test-case generation. Two tasks:
4
+
5
+ ## 1. Clone each repo in `state.repos`
6
+
7
+ ```
8
+ state.repos = {{state.repos}}
9
+ state.workspace = {{state.workspace}}
10
+ ```
11
+
12
+ For each entry, call `git_checkout`:
13
+ - `url`: the repo's `url` field
14
+ - `branch`: the repo's `branch` field (omit if not set)
15
+ - `shallow`: true (default)
16
+
17
+ `git_checkout` auto-authenticates against GitHub or GitLab using the
18
+ project's connected integration. If one repo fails, continue with the
19
+ others and surface the failure in the final output.
20
+
21
+ ## 2. Initialize a baseline git repo at `state.workspace`
22
+
23
+ ```bash
24
+ cd {{state.workspace}}
25
+ git init
26
+ git config user.email "zibby@agent.com"
27
+ git config user.name "Zibby Agent"
28
+ git add .
29
+ git commit --allow-empty -m "baseline"
30
+ git rev-parse HEAD
31
+ ```
32
+
33
+ The `git rev-parse HEAD` output is the `baselineCommit` you'll return.
34
+
35
+ ## 3. Return JSON matching the outputSchema
36
+
37
+ ```json
38
+ {
39
+ "success": true,
40
+ "clonedRepos": [
41
+ { "name": "<repo name>", "path": "<absolute path from git_checkout>", "isPrimary": true }
42
+ ],
43
+ "baselineCommit": "<HEAD sha>"
44
+ }
45
+ ```
46
+
47
+ ---
48
+
49
+ Edit this prompt to customize setup behavior — e.g. skip baseline,
50
+ filter repos, or run post-clone tooling (`npm install`, etc).
@@ -59,6 +59,18 @@ export const generateTestCasesContextSchema = z.object({
59
59
 
60
60
  nodeConfigs: z.record(z.string(), z.any()).optional()
61
61
  .describe('Per-node configuration overrides — set at deploy time, not trigger time'),
62
+
63
+ // ── Node outputs (mid-graph, runner-populated) ────────────────────
64
+ setup: z.object({
65
+ success: z.boolean(),
66
+ clonedRepos: z.array(z.object({
67
+ name: z.string(),
68
+ path: z.string(),
69
+ isPrimary: z.boolean().optional(),
70
+ })),
71
+ baselineCommit: z.string(),
72
+ }).optional()
73
+ .describe('Output of the setup node — clone paths + baseline commit'),
62
74
  });
63
75
 
64
76
  // Derived: full runtime state. Exported for tests + tooling.
package/index.js CHANGED
@@ -165,6 +165,142 @@ export const TEMPLATES = {
165
165
  'Produce specs that an AI agent can execute end-to-end',
166
166
  ],
167
167
  },
168
+ },
169
+
170
+ // ── notify-slack: reusable notifier child workflow ────────────────
171
+ // Dispatched as a sub-graph from any parent that wants Slack alerts.
172
+ // Single-node graph, no LLM, deterministic API call.
173
+ 'notify-slack': {
174
+ name: 'notify-slack',
175
+ displayName: 'Notify Slack',
176
+ description: 'Reusable child workflow — posts a structured Block Kit alert to a Slack channel. Dispatched by other workflows (Sentry triage, autofix, incident) via sub-graph.',
177
+ path: join(__dirname, 'notify-slack'),
178
+ defaultSlug: 'alert-slack',
179
+ deps: { zod: '^3.23.0' },
180
+ features: [
181
+ 'Single-node, no LLM — deterministic ~500ms post',
182
+ 'Block Kit message with severity-coded color + emoji',
183
+ 'Optional Sentry-flavored fields (users affected, events, release)',
184
+ 'Action buttons + caller-supplied @-mentions',
185
+ 'Returns messageTs so parent can thread follow-ups',
186
+ ],
187
+ marketplace: {
188
+ slug: 'notify-slack',
189
+ tagline: 'Reusable Slack alert worker — dispatch from any workflow.',
190
+ iconPrompt: [
191
+ 'Flat geometric vector illustration with subtle clean gradients, in the spirit of Linear / Notion / Stripe iconography — crisp, no painterly textures.',
192
+ 'Subject: the iconic Slack pinwheel mark — four chunky rounded-rectangle "petals" arranged in a plus / asterisk configuration, colored with Slack\'s signature palette (top-left red #E01E5A, top-right yellow #ECB22E, bottom-right green #2EB67D, bottom-left blue #36C5F0), painted as if it\'s the focal brand mark of the icon. A small bright magenta notification dot floats in the upper-right corner of the pinwheel suggesting an incoming alert/ping.',
193
+ 'Background: deep navy (#0B0F1A) rounded square (1024×1024) with a faint radial glow centered behind the pinwheel so the colors pop without becoming oversaturated.',
194
+ 'Centered composition with the pinwheel as the dominant focal element, the notification dot as a small secondary accent in the upper-right; plenty of breathing room so the silhouette reads at 64×64 in the marketplace grid.',
195
+ 'Mood: focused, energetic, signal-not-noise — the canonical Slack-flavored notification worker.',
196
+ 'NO text, NO letters, NO photo-realism, NO sleek 3D render, NO literal Slack wordmark — the colored pinwheel shape is allowed as the brand reference.',
197
+ ].join('\n'),
198
+ category: 'Notifications',
199
+ tags: ['slack', 'notification', 'alert', 'child-workflow'],
200
+ capabilities: [
201
+ 'Severity-coded Block Kit message (low/medium/high/critical)',
202
+ 'Code snippet + action button + caller mentions',
203
+ 'Sub-graph dispatchable from any parent workflow',
204
+ 'Returns messageTs for thread replies',
205
+ ],
206
+ conversationStarters: [
207
+ 'Post a CRITICAL alert to #incidents from this workflow',
208
+ 'Send a daily summary to #dev-updates',
209
+ 'Notify @oncall when a Sentry issue exceeds threshold',
210
+ 'Forward the deploy-fail message to #ops',
211
+ ],
212
+ },
213
+ },
214
+
215
+ // ── notify-lark: same as notify-slack, Lark / Feishu variant ─────
216
+ 'notify-lark': {
217
+ name: 'notify-lark',
218
+ displayName: 'Notify Lark',
219
+ description: 'Reusable child workflow — posts a structured Interactive Card to a Lark / Feishu chat. Dispatched by other workflows via sub-graph.',
220
+ path: join(__dirname, 'notify-lark'),
221
+ defaultSlug: 'alert-lark',
222
+ deps: { zod: '^3.23.0' },
223
+ features: [
224
+ 'Single-node, no LLM',
225
+ 'Lark Interactive Card with severity template (red/orange/yellow/grey)',
226
+ 'Optional Sentry-flavored metadata fields',
227
+ 'Token cache across multiple sends in one task',
228
+ 'Returns messageId for threaded replies',
229
+ ],
230
+ marketplace: {
231
+ slug: 'notify-lark',
232
+ tagline: 'Reusable Lark / Feishu alert worker — dispatch from any workflow.',
233
+ iconPrompt: [
234
+ 'A clean, modern app icon for a Lark / Feishu notification worker.',
235
+ '',
236
+ 'Visual style: flat geometric vector with subtle gradient, complementary to the notify-slack icon (same family).',
237
+ 'Subject: a stylized speech-bubble silhouette in Lark-cyan-to-blue gradient (#00D6B9 → #1664FF) with a checkmark inside. Gentle motion lines behind it.',
238
+ 'Background: deep navy (#0B0F1A) rounded square (1024×1024).',
239
+ 'Mood: focused, professional, signal-not-noise.',
240
+ 'NO Lark / Feishu logo trademark, NO text, NO photo-realism.',
241
+ ].join('\n'),
242
+ category: 'Notifications',
243
+ tags: ['lark', 'feishu', 'notification', 'alert', 'child-workflow'],
244
+ capabilities: [
245
+ 'Severity-coded Lark Interactive Card',
246
+ 'Auto-detects receive_id_type from id prefix (chat_id / open_id / email)',
247
+ 'Sub-graph dispatchable from any parent workflow',
248
+ 'Per-process token cache for fan-out efficiency',
249
+ ],
250
+ conversationStarters: [
251
+ 'Send a CRITICAL alert to the engineering Lark group',
252
+ 'Notify the on-call group chat when Sentry issue spikes',
253
+ 'Forward deploy notifications to our Lark channel',
254
+ ],
255
+ },
256
+ },
257
+
258
+ // ── sentry-triage: parent workflow that uses notify-slack/-lark ──
259
+ 'sentry-triage': {
260
+ name: 'sentry-triage',
261
+ displayName: 'Sentry Triage Bot',
262
+ description: 'Hourly Sentry triage — pulls new issues, drops obvious noise with a regex pre-filter, classifies survivors with LLM (CRITICAL/HIGH/MEDIUM/LOW/NOISE), and fans out alerts to a notify-slack OR notify-lark child workflow.',
263
+ path: join(__dirname, 'sentry-triage'),
264
+ defaultSlug: 'sentry-triage',
265
+ deps: { zod: '^3.23.0' },
266
+ features: [
267
+ '4-node graph: fetch → filter_noise → classify → dispatch_alerts',
268
+ 'Regex noise filter before LLM cuts ~80% of classification cost',
269
+ 'LLM severity classifier with explicit rubric (rules 1-5)',
270
+ 'Sub-graph fan-out to notify-slack OR notify-lark (choose at deploy)',
271
+ 'Per-issue failure isolation — one Slack hiccup doesn\'t stall the run',
272
+ 'Configurable severityThreshold (don\'t notify on LOW noise)',
273
+ 'Cron-friendly: hourly schedule, default sinceMinutes=60',
274
+ ],
275
+ marketplace: {
276
+ slug: 'sentry-triage',
277
+ tagline: 'Filter noise, classify severity, ping the right channel — every hour.',
278
+ iconPrompt: [
279
+ 'Hand-painted storybook illustration in a warm gouache style with soft brushwork and gentle painterly texture, featuring the friendly round lighthouse mascot character with two big smiling eyes and a rosy blush on its white-and-coral-striped tower body, perched on a tiny mint-green island and clutching a small glowing purple SHIELD BADGE in front of its body — the badge is a rounded geometric emblem in Sentry\'s signature deep violet (#362D59 / #7553FF) with a stylized white "S"-mark inside it formed from overlapping rounded parallelogram shapes, painted with the same soft gouache brushstrokes as the rest of the scene so it feels integrated rather than corporate.',
280
+ 'The lighthouse lantern emits a soft golden beam that catches one glowing amber alert orb while three faded grey noise specks drift harmlessly past, reinforcing the "filter the signal, calm the noise" idea.',
281
+ 'Background is a soft sunrise gradient of pale peach at the top blending through buttercream into a gentle wash of dusty lavender at the base, tying the warm scene to the violet of the badge; a few small fluffy pastel clouds float in for friendliness.',
282
+ 'Centered composition with the purple shield badge as the immediate focal point in the lower-center, the lighthouse rising behind and slightly above it, beam angled diagonally; plenty of breathing room so the silhouette reads at 64×64 with the violet badge clearly visible at a glance.',
283
+ 'Mood is warm, reassuring, optimistic — the friendly Sentry-flavored night-watch character, NOT tactical or corporate or alarming.',
284
+ 'Soft rounded square 1024×1024 canvas with a subtle paper-grain texture.',
285
+ 'NO text, NO letters, NO photo-realism, NO sleek 3D render, NO magnifying glass, NO speech bubbles, NO dark navy or near-black backgrounds, NO bug or insect imagery, NO literal Sentry wordmark.',
286
+ ].join('\n'),
287
+ category: 'Operations',
288
+ tags: ['sentry', 'observability', 'on-call', 'triage', 'alerting'],
289
+ capabilities: [
290
+ 'Hourly scheduled triage of new Sentry issues',
291
+ 'Deterministic regex filter drops Script error / ResizeObserver / extension noise',
292
+ 'LLM severity classifier with auditable rubric',
293
+ 'Dispatches to notify-slack or notify-lark (sub-graph, ~5ms in-process)',
294
+ 'CRITICAL alerts get caller-supplied @-mentions; lower severities don\'t',
295
+ 'Configurable severity threshold per deploy',
296
+ ],
297
+ conversationStarters: [
298
+ 'Triage all new Sentry issues from the last hour',
299
+ 'Notify #sentry-alerts when severity is HIGH or above',
300
+ 'Run hourly and post a summary to our team Slack',
301
+ 'Page on-call when a CRITICAL error appears in checkout',
302
+ ],
303
+ },
168
304
  }
169
305
  };
170
306
 
@@ -0,0 +1,88 @@
1
+ # notify-lark
2
+
3
+ A reusable **child workflow** that posts an Interactive Card alert to a Lark / Feishu chat.
4
+
5
+ Companion to `notify-slack` — same provider-neutral input shape, so a parent workflow can fan out to BOTH Slack and Lark with the same `input` block (just swap `channel` → `receiveId`).
6
+
7
+ ## What it does
8
+
9
+ - Takes a provider-neutral payload (severity, title, body, optional Sentry-flavored context)
10
+ - Builds a Lark Interactive Card with severity-coded header template + emoji + action buttons
11
+ - Resolves `tenant_access_token` from the app id/secret stored in your Lark integration
12
+ - POSTs to `/open-apis/im/v1/messages` with the right `receive_id_type` inferred from the id prefix
13
+
14
+ No LLM call — single deterministic API request, typically <500ms (or <1s on cold token cache).
15
+
16
+ ## Dispatch shape (parent workflow)
17
+
18
+ ```js
19
+ graph.addNode('alert', {
20
+ workflow: 'notify-lark',
21
+ async: false,
22
+ input: (state) => ({
23
+ severity: 'critical',
24
+ title: 'Checkout: TypeError on session.user.id',
25
+ body: '**12 users** affected in the last 1h. Likely regression from `1.42.0`.',
26
+ receiveId: 'oc_abc123def456...', // chat id, open id, or email
27
+ sentryLink: 'https://sentry.io/.../1234567890/',
28
+ affectedUsers: 12,
29
+ events: 47,
30
+ release: '1.42.0',
31
+ firstSeen: '8 min ago',
32
+ codeSnippet:'src/handlers/checkout.ts:142\nconst userId = session.user.id;',
33
+ mentions: ['<at user_id="ou_oncall_group">@backend-oncall</at>'],
34
+ }),
35
+ output: 'notify_lark.messageId',
36
+ });
37
+ ```
38
+
39
+ ## receiveId formats
40
+
41
+ The `receive_id_type` query param is inferred from the prefix:
42
+
43
+ | Prefix | Type | Description |
44
+ |---|---|---|
45
+ | `oc_` | `chat_id` | Group chat or DM (most common) |
46
+ | `ou_` | `open_id` | Direct message to a specific user |
47
+ | `on_` | `union_id` | Cross-app stable user id |
48
+ | `cli_` | `app_id` | App-to-app message |
49
+ | `<email>@…` | `email` | Send to user by email |
50
+
51
+ If you don't know your chat id, DM the bot `whoami` and it'll respond with the caller's `open_id`. Group chat ids appear in the URL when you open the chat in browser.
52
+
53
+ ## Output
54
+
55
+ ```js
56
+ { delivered: true, receiveId: 'oc_abc...', receiveIdType: 'chat_id', messageId: 'om_xxxxx' }
57
+ ```
58
+
59
+ The `messageId` lets the parent post threaded replies later (e.g. incident-commander progress updates).
60
+
61
+ ## Prerequisites
62
+
63
+ Project must have the **Lark** integration connected with:
64
+ - `im:message` scope (send messages)
65
+ - `im:message.group_msg` scope (send to group chats)
66
+
67
+ ## Severity → header template
68
+
69
+ | Severity | Lark template | Emoji |
70
+ |---|---|---|
71
+ | low | `grey` | ⚪ |
72
+ | medium | `yellow` | 🟡 |
73
+ | high | `orange` | 🟠 |
74
+ | critical | `red` | 🚨 |
75
+
76
+ ## Tests
77
+
78
+ ```bash
79
+ cd packages/workflow-templates/notify-lark
80
+ npm test
81
+ ```
82
+
83
+ Tests cover:
84
+ - Card rendering for each severity
85
+ - Conditional sections (fields/body/code/actions only when input present)
86
+ - receive_id_type inference per prefix
87
+ - Tenant token caching across multiple sends
88
+ - Error mapping (invalid receive_id, scope missing, network blip)
@@ -0,0 +1,43 @@
1
+ /**
2
+ * notify-lark — single-node child workflow.
3
+ *
4
+ * Companion to notify-slack; same provider-neutral input shape so a
5
+ * parent can `dispatchSubgraph('notify-slack')` AND
6
+ * `dispatchSubgraph('notify-lark')` with the same input block, just
7
+ * swapping `channel` → `receiveId`. (See notify-slack/graph.mjs for the
8
+ * Slack-flavored example.)
9
+ *
10
+ * Returns `{ delivered, receiveId, receiveIdType, messageId }`. The
11
+ * `messageId` can be used for thread replies via Lark's reply API in
12
+ * follow-up dispatches.
13
+ */
14
+
15
+ import { WorkflowAgent, WorkflowGraph } from '@zibby/core';
16
+ import { notifyLarkNode } from './nodes/notify-lark-node.js';
17
+ import {
18
+ notifyLarkInputSchema,
19
+ notifyLarkContextSchema,
20
+ } from './state.js';
21
+
22
+ export class NotifyLarkAgent extends WorkflowAgent {
23
+ buildGraph() {
24
+ const graph = new WorkflowGraph();
25
+ graph
26
+ .setInputSchema(notifyLarkInputSchema)
27
+ .setContextSchema(notifyLarkContextSchema);
28
+
29
+ graph.addNode('notify_lark', notifyLarkNode);
30
+ graph.setEntryPoint('notify_lark');
31
+ graph.addEdge('notify_lark', 'END');
32
+
33
+ return graph;
34
+ }
35
+
36
+ async onComplete(result) {
37
+ const delivered = !!result?.state?.notify_lark?.delivered;
38
+ const mid = result?.state?.notify_lark?.messageId || '?';
39
+ console.log(`[notify-lark] ${delivered ? 'delivered' : 'failed'} (messageId=${mid})`);
40
+ }
41
+ }
42
+
43
+ export default NotifyLarkAgent;
Binary file