@zibby/workflow-templates 0.7.1 → 0.9.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.
- package/browser-test-automation/icon.png +0 -0
- package/code-analysis/icon.png +0 -0
- package/generate-test-cases/icon.png +0 -0
- package/index.js +353 -3
- package/notify-lark/icon.png +0 -0
- package/notify-lark/package.json +2 -1
- package/notify-notion/icon.png +0 -0
- package/notify-slack/icon.png +0 -0
- package/notify-slack/package.json +2 -1
- package/package.json +4 -1
- package/pipeline-supervisor/README.md +51 -0
- package/pipeline-supervisor/graph.mjs +75 -0
- package/pipeline-supervisor/icon.png +0 -0
- package/pipeline-supervisor/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/pipeline-supervisor/nodes/notify-node.js +162 -0
- package/pipeline-supervisor/nodes/propose-node.js +91 -0
- package/pipeline-supervisor/nodes/scan-pipelines-node.js +316 -0
- package/pipeline-supervisor/package.json +19 -0
- package/pipeline-supervisor/state.js +151 -0
- package/sentry-triage/graph.mjs +25 -18
- package/sentry-triage/icon.png +0 -0
- package/sentry-triage/nodes/dispatch-node.js +120 -59
- package/browser-test-automation/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/code-analysis/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pipeline-supervisor — input + context schemas.
|
|
3
|
+
*
|
|
4
|
+
* "Zibby managing Zibby." A scheduled workflow that watches the project's
|
|
5
|
+
* OTHER pipelines, finds the ones that are failing / slow / repeatedly
|
|
6
|
+
* erroring, and posts a human-reviewable improvement proposal to Slack or
|
|
7
|
+
* Lark. v1 is strictly READ + PROPOSE + NOTIFY — it never edits another
|
|
8
|
+
* workflow's graph. That's the safe L3 starting point; the auto-PATCH step
|
|
9
|
+
* is a clearly-marked TODO in propose-node.js, deliberately NOT implemented.
|
|
10
|
+
*
|
|
11
|
+
* Trigger payload (inputSchema) carries the three per-run dials a human
|
|
12
|
+
* would actually want to tune at schedule time: how far back to look, how
|
|
13
|
+
* bad a pipeline has to be before we flag it, and an optional name filter.
|
|
14
|
+
* Everything else (the chat destination, the supervisor's read credential)
|
|
15
|
+
* is deploy-time ENV-tab config:
|
|
16
|
+
*
|
|
17
|
+
* Required:
|
|
18
|
+
* ZIBBY_PAT Personal access token (zby_pat_…) the supervisor
|
|
19
|
+
* uses to READ this project's executions across
|
|
20
|
+
* ALL pipelines. The Fargate-injected
|
|
21
|
+
* PROJECT_API_TOKEN is a PROJECT token (authType
|
|
22
|
+
* 'project', no userId) and the /executions +
|
|
23
|
+
* /jobs + /all read routes all require a user
|
|
24
|
+
* identity — so they 401 for a project token.
|
|
25
|
+
* A user PAT is the credential that works. See
|
|
26
|
+
* nodes/scan-pipelines-node.js for the full
|
|
27
|
+
* auth rationale.
|
|
28
|
+
* SLACK_CHANNEL channel id "C012345" or "#name" ─┐ set
|
|
29
|
+
* LARK_RECEIVE_ID oc_… chat id, ou_… open id, or email ─┘ ONE
|
|
30
|
+
*
|
|
31
|
+
* Optional:
|
|
32
|
+
* SUPERVISOR_PROJECT_ID Project UUID to supervise. Defaults to the
|
|
33
|
+
* running project (PROJECT_ID env, injected by the
|
|
34
|
+
* executor) — i.e. the supervisor watches its own
|
|
35
|
+
* project's other pipelines. Set this to point it
|
|
36
|
+
* at a DIFFERENT project the PAT owner can access.
|
|
37
|
+
* SLACK_MENTIONS JSON array — appended to the proposal card.
|
|
38
|
+
* LARK_MENTIONS JSON array — appended to the proposal card.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { z } from 'zod';
|
|
42
|
+
|
|
43
|
+
export const pipelineSupervisorInputSchema = z.object({
|
|
44
|
+
lookbackHours: z.number().int().min(1).max(720).default(24)
|
|
45
|
+
.describe('How many hours of execution history to scan across pipelines (1–720, default 24).'),
|
|
46
|
+
|
|
47
|
+
// A pipeline with >= this fraction of recent runs failing is "problem".
|
|
48
|
+
// 0.4 = flag anything failing 2 in 5 or worse. Tunable so a noisy team
|
|
49
|
+
// can raise it (only page on near-total breakage) or a strict team can
|
|
50
|
+
// lower it (catch flakiness early).
|
|
51
|
+
minFailRate: z.number().min(0).max(1).default(0.4)
|
|
52
|
+
.describe('Minimum failure rate (0–1) for a pipeline to be flagged as a problem. Default 0.4 = failing ≥40% of recent runs.'),
|
|
53
|
+
|
|
54
|
+
// Optional name filter. When set, only pipelines whose workflow type /
|
|
55
|
+
// slug matches one of these strings (case-insensitive substring) are
|
|
56
|
+
// considered — lets you supervise just "the deploy ones" without noise
|
|
57
|
+
// from every test run. Omit to consider every pipeline in the project.
|
|
58
|
+
targetWorkflowTypes: z.array(z.string().min(1)).optional()
|
|
59
|
+
.describe('Optional: only supervise pipelines whose workflow type/slug matches one of these (case-insensitive substring). Omit to scan all.'),
|
|
60
|
+
|
|
61
|
+
// Cap on how many distinct pipelines we'll fetch per-run job/log detail
|
|
62
|
+
// for. The scan lists executions cheaply; deep per-pipeline log reads are
|
|
63
|
+
// the expensive part, so we bound them. 25 covers any realistic project.
|
|
64
|
+
maxPipelines: z.number().int().min(1).max(100).default(25)
|
|
65
|
+
.describe('Max number of distinct pipelines to analyze in one run (1–100, default 25).'),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export const pipelineSupervisorContextSchema = z.object({
|
|
69
|
+
workspace: z.string().optional()
|
|
70
|
+
.describe('Workspace path — runner-injected; the supervisor doesn\'t need it but graph.run requires it.'),
|
|
71
|
+
|
|
72
|
+
// scan_pipelines — DETERMINISTIC. Pulls recent executions via the Zibby
|
|
73
|
+
// REST API (PAT-authed) and rolls them up per pipeline into a health
|
|
74
|
+
// summary the proposer reasons over.
|
|
75
|
+
scan_pipelines: z.object({
|
|
76
|
+
projectId: z.string().optional(),
|
|
77
|
+
lookbackHours: z.number().optional(),
|
|
78
|
+
scannedAt: z.string().optional(),
|
|
79
|
+
totalExecutions: z.number().optional(),
|
|
80
|
+
pipelines: z.array(z.object({
|
|
81
|
+
// A "pipeline" = one workflow type/slug within the project. Executions
|
|
82
|
+
// are grouped by their workflow identity.
|
|
83
|
+
workflowType: z.string(),
|
|
84
|
+
workflowUuid: z.string().optional(),
|
|
85
|
+
total: z.number(),
|
|
86
|
+
failed: z.number(),
|
|
87
|
+
succeeded: z.number(),
|
|
88
|
+
running: z.number(),
|
|
89
|
+
failRate: z.number(),
|
|
90
|
+
// Median wall-clock duration (ms) of completed runs — the "slow" signal.
|
|
91
|
+
medianDurationMs: z.number().optional(),
|
|
92
|
+
// The single worst recent run, for the proposer to cite a concrete
|
|
93
|
+
// example ("failed on step Y at 14:02").
|
|
94
|
+
worstRun: z.object({
|
|
95
|
+
executionId: z.string().optional(),
|
|
96
|
+
status: z.string().optional(),
|
|
97
|
+
durationMs: z.number().optional(),
|
|
98
|
+
failedStep: z.string().optional(),
|
|
99
|
+
errorSummary: z.string().optional(),
|
|
100
|
+
startedAt: z.string().optional(),
|
|
101
|
+
}).optional(),
|
|
102
|
+
// Whether this pipeline crossed minFailRate (or the slow threshold).
|
|
103
|
+
flagged: z.boolean(),
|
|
104
|
+
flagReason: z.string().optional(),
|
|
105
|
+
})),
|
|
106
|
+
}).optional(),
|
|
107
|
+
|
|
108
|
+
// propose_improvements — LLM. Reads the per-pipeline health summary and
|
|
109
|
+
// emits one concrete, reviewable improvement proposal per flagged pipeline.
|
|
110
|
+
propose_improvements: z.object({
|
|
111
|
+
proposals: z.array(z.object({
|
|
112
|
+
workflowType: z.string(),
|
|
113
|
+
problem: z.string(),
|
|
114
|
+
// The kind of change suggested — constrained so the UI / future
|
|
115
|
+
// auto-PATCH step can route on it. Maps to the four moves in the brief.
|
|
116
|
+
changeKind: z.enum([
|
|
117
|
+
'add_test_gate',
|
|
118
|
+
'tweak_prompt',
|
|
119
|
+
'add_human_approval_gate',
|
|
120
|
+
'drop_redundant_step',
|
|
121
|
+
'other',
|
|
122
|
+
]),
|
|
123
|
+
suggestion: z.string(),
|
|
124
|
+
evidence: z.string().optional(),
|
|
125
|
+
confidence: z.number().min(0).max(1).optional(),
|
|
126
|
+
})),
|
|
127
|
+
}).optional(),
|
|
128
|
+
|
|
129
|
+
// notify — LLM + SKILLS.CHAT_NOTIFY. Posts ONE review card summarizing
|
|
130
|
+
// the proposals to the configured Slack or Lark destination.
|
|
131
|
+
notify: z.object({
|
|
132
|
+
dispatched: z.array(z.object({
|
|
133
|
+
status: z.enum(['sent', 'skipped', 'failed']),
|
|
134
|
+
recipient: z.object({
|
|
135
|
+
kind: z.enum(['channel', 'user_dm', 'usergroup']).nullish(),
|
|
136
|
+
id: z.string().nullish(),
|
|
137
|
+
label: z.string().nullish(),
|
|
138
|
+
}).nullish(),
|
|
139
|
+
proposalCount: z.number().nullish(),
|
|
140
|
+
messageTs: z.string().nullish(), // Slack
|
|
141
|
+
messageId: z.string().nullish(), // Lark
|
|
142
|
+
detail: z.string().nullish(),
|
|
143
|
+
})),
|
|
144
|
+
summary: z.object({
|
|
145
|
+
total: z.number(),
|
|
146
|
+
sent: z.number(),
|
|
147
|
+
skipped: z.number(),
|
|
148
|
+
failed: z.number(),
|
|
149
|
+
}),
|
|
150
|
+
}).optional(),
|
|
151
|
+
});
|
package/sentry-triage/graph.mjs
CHANGED
|
@@ -1,28 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* sentry-triage — parent workflow. Hourly Sentry issue triage.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Agent-driven first. Nodes are LLM agents by default — because nobody
|
|
5
|
+
* hand-edits these in practice, they point an AGENT at the prompt and say
|
|
6
|
+
* "make billing always critical" / "page #oncall after 9pm". A deterministic
|
|
7
|
+
* for-loop can't be told that in English; an agent can. We drop to
|
|
8
|
+
* deterministic ONLY where it genuinely makes sense — a pure mechanical step
|
|
9
|
+
* with zero judgment and nothing a customer would ever want to tune.
|
|
5
10
|
*
|
|
6
|
-
* fetch_issues (
|
|
11
|
+
* fetch_issues (deterministic + SKILLS.SENTRY) → pull recent unresolved/
|
|
12
|
+
* unassigned issues + suspect
|
|
13
|
+
* commits. Pure API pull, no
|
|
14
|
+
* judgment, nothing to tune →
|
|
15
|
+
* the one place a for-loop
|
|
16
|
+
* wins (faster, free, no
|
|
17
|
+
* hallucinated queries).
|
|
7
18
|
* ↓
|
|
8
|
-
* classify (LLM
|
|
19
|
+
* classify (LLM agent) → label NOISE…CRITICAL. Stays
|
|
20
|
+
* an agent BECAUSE the rubric
|
|
21
|
+
* is customer-tunable, and
|
|
22
|
+
* they tune it by having an
|
|
23
|
+
* agent edit this prompt — not
|
|
24
|
+
* by touching code.
|
|
9
25
|
* ↓
|
|
10
|
-
* dispatch_alerts (LLM + SKILLS.CHAT_NOTIFY)
|
|
11
|
-
*
|
|
26
|
+
* dispatch_alerts (LLM agent + SKILLS.CHAT_NOTIFY) → one human-voice digest;
|
|
27
|
+
* routing is the user's
|
|
28
|
+
* (DISPATCH_RULES) to own.
|
|
12
29
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* - LLM dispatch can BATCH related issues (5 errors in /checkout/ →
|
|
17
|
-
* 1 consolidated message) and DE-DUP near-duplicates. A
|
|
18
|
-
* deterministic for-loop can't.
|
|
19
|
-
* - outputSchema enforcement guarantees every above-threshold issue
|
|
20
|
-
* either gets a "sent" record or an explicit "failed/skipped" —
|
|
21
|
-
* no silent drops.
|
|
22
|
-
*
|
|
23
|
-
* Customize prompts: each node's prompt lives in its own module under
|
|
24
|
-
* nodes/. Override per-deploy by editing the file or by passing a
|
|
25
|
-
* custom prompt string via inputSchema (planned).
|
|
30
|
+
* Also: LLM dispatch BATCHES related issues into one message and DE-DUPs —
|
|
31
|
+
* a for-loop can't. outputSchema enforcement → every above-threshold issue
|
|
32
|
+
* gets a "sent" or explicit "skipped/failed" record; no silent drops.
|
|
26
33
|
*/
|
|
27
34
|
|
|
28
35
|
import { WorkflowAgent, WorkflowGraph } from '@zibby/core';
|
package/sentry-triage/icon.png
CHANGED
|
Binary file
|
|
@@ -61,16 +61,22 @@ const DispatchedRecordSchema = z.object({
|
|
|
61
61
|
issueIds: z.array(z.string()).describe('IDs grouped into this message; usually 1, more when batched.'),
|
|
62
62
|
severity: z.enum(SEVERITY_LEVELS),
|
|
63
63
|
status: z.enum(['sent', 'skipped', 'failed']),
|
|
64
|
+
// Every field below a skipped/failed record can't populate is nullish
|
|
65
|
+
// (not optional) on purpose: the LLM emits an explicit `null` rather than
|
|
66
|
+
// omitting the key, and `.optional()` rejects null → ZodError → the whole
|
|
67
|
+
// dispatch node fails even though it did exactly the right thing (skip
|
|
68
|
+
// below-threshold). So recipient (no send → null), messageTs/messageId (no
|
|
69
|
+
// message id), and detail all tolerate null.
|
|
64
70
|
// Who actually received this message. Helps post-hoc auditing of
|
|
65
71
|
// routing decisions ("why did the agent send this to @sarah?").
|
|
66
72
|
recipient: z.object({
|
|
67
|
-
kind: z.enum(['channel', 'user_dm', 'usergroup']).
|
|
68
|
-
id: z.string().
|
|
69
|
-
label: z.string().
|
|
70
|
-
}).
|
|
71
|
-
messageTs: z.string().
|
|
72
|
-
messageId: z.string().
|
|
73
|
-
detail: z.string().
|
|
73
|
+
kind: z.enum(['channel', 'user_dm', 'usergroup']).nullish(),
|
|
74
|
+
id: z.string().nullish(),
|
|
75
|
+
label: z.string().nullish(),
|
|
76
|
+
}).nullish(),
|
|
77
|
+
messageTs: z.string().nullish(), // Slack
|
|
78
|
+
messageId: z.string().nullish(), // Lark
|
|
79
|
+
detail: z.string().nullish(),
|
|
74
80
|
});
|
|
75
81
|
|
|
76
82
|
const DispatchAlertsOutputSchema = z.object({
|
|
@@ -85,9 +91,23 @@ const DispatchAlertsOutputSchema = z.object({
|
|
|
85
91
|
|
|
86
92
|
const SEVERITY_ORDER = ['NOISE', 'LOW', 'MEDIUM', 'HIGH', 'CRITICAL'];
|
|
87
93
|
|
|
94
|
+
// Turn the trigger's sinceMinutes into a phrase a human would actually say,
|
|
95
|
+
// so the digest can open with the time span ("past hour", "last 3 days")
|
|
96
|
+
// instead of leaving the reader to guess how much history this covers.
|
|
97
|
+
function humanWindow(min) {
|
|
98
|
+
const m = Number(min);
|
|
99
|
+
if (!m || m < 1) return 'recently';
|
|
100
|
+
if (m < 90) return `the past ${Math.round(m)} minutes`;
|
|
101
|
+
if (m < 1440) return `the past ${Math.round(m / 60)} hours`;
|
|
102
|
+
const days = Math.round(m / 1440);
|
|
103
|
+
return days === 1 ? 'the past day' : `the past ${days} days`;
|
|
104
|
+
}
|
|
105
|
+
|
|
88
106
|
const DISPATCH_PROMPT = (state = {}) => {
|
|
89
107
|
const issues = state?.fetch_issues?.issues || [];
|
|
90
108
|
const classifications = state?.classify?.classifications || [];
|
|
109
|
+
const windowLabel = humanWindow(state?.sinceMinutes);
|
|
110
|
+
const fetchedAt = state?.fetch_issues?.fetchedAt || '';
|
|
91
111
|
|
|
92
112
|
const threshold = process.env.SEVERITY_THRESHOLD || 'MEDIUM';
|
|
93
113
|
const slackChannel = process.env.SLACK_CHANNEL || '';
|
|
@@ -97,8 +117,8 @@ const DISPATCH_PROMPT = (state = {}) => {
|
|
|
97
117
|
const dispatchRules = process.env.DISPATCH_RULES || '';
|
|
98
118
|
|
|
99
119
|
// ── No-op short-circuit ─────────────────────────────────────────
|
|
100
|
-
//
|
|
101
|
-
//
|
|
120
|
+
// The three "nothing to do this run" cases — keep the run green without a
|
|
121
|
+
// model round-trip or forcing channel setup.
|
|
102
122
|
const minSeverityRank = SEVERITY_ORDER.indexOf(threshold);
|
|
103
123
|
const aboveThreshold = minSeverityRank < 0
|
|
104
124
|
? classifications
|
|
@@ -147,50 +167,95 @@ Return this exact JSON envelope and call no tools:
|
|
|
147
167
|
try { mentions = JSON.parse(mentionsRaw); } catch { mentions = []; }
|
|
148
168
|
if (!Array.isArray(mentions)) mentions = [];
|
|
149
169
|
|
|
150
|
-
// ── Routing policy
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
170
|
+
// ── Routing policy ──────────────────────────────────────────────
|
|
171
|
+
// Routing is the USER'S to own. We give the agent only the bare facts it
|
|
172
|
+
// always needs; everything past that is policy:
|
|
173
|
+
// - If DISPATCH_RULES is set, those rules ARE the policy. Hand over the
|
|
174
|
+
// facts + the rules and get out of the way — do NOT also stack the
|
|
175
|
+
// built-in author-DM / usergroup defaults on top. The user chose to
|
|
176
|
+
// drive this themselves; don't fight them.
|
|
177
|
+
// - With no DISPATCH_RULES, fall back to sensible built-in defaults
|
|
178
|
+
// (channel post + opt-in author-DM / escalation from env vars).
|
|
179
|
+
const facts = [`Skip anything classified below ${threshold}.`];
|
|
180
|
+
// Only mention a channel when one is actually configured — never render a
|
|
181
|
+
// "post here" line pointing at an empty value.
|
|
182
|
+
if (channelId) facts.push(`Channel configured: ${JSON.stringify(channelId)} (${provider}).`);
|
|
183
|
+
facts.push(`Post with the \`${postTool}\` tool.`);
|
|
184
|
+
|
|
185
|
+
let policyLines;
|
|
186
|
+
let overrideBlock = '';
|
|
187
|
+
if (dispatchRules) {
|
|
188
|
+
policyLines = facts.concat([
|
|
189
|
+
'Past the facts above, follow YOUR rules below — who gets paged / DM\'d, where, when, what to suppress. They override anything the built-in defaults would have implied.',
|
|
190
|
+
]);
|
|
191
|
+
overrideBlock = `\n\n# Your routing rules (authoritative)\n${dispatchRules.trim()}\n`;
|
|
166
192
|
} else {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
193
|
+
policyLines = [...facts];
|
|
194
|
+
if (channelId) policyLines.push('Every alert at minimum goes to the channel above.');
|
|
195
|
+
if (preferAuthor) {
|
|
196
|
+
policyLines.push(
|
|
197
|
+
`Author-DM: when an issue has \`suspectCommits[0].authorEmail\`, FIRST call \`${lookupTool}({ email })\`. ` +
|
|
198
|
+
`If \`ok:true\`, DM that user (their id as the recipient) AND still post the channel for team visibility. ` +
|
|
199
|
+
`If \`ok:false\` or no email, channel-only.`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
if (highSevGroup) {
|
|
203
|
+
policyLines.push(
|
|
204
|
+
provider === 'slack'
|
|
205
|
+
? `On CRITICAL/HIGH, mention the Slack usergroup ${JSON.stringify(highSevGroup)} in the channel message. ` +
|
|
206
|
+
`Handle (@…): call \`slack_list_usergroups\` once to resolve → id, mention as \`<!subteam^ID>\`. Id (S…): use \`<!subteam^${highSevGroup}>\`.`
|
|
207
|
+
: `On CRITICAL/HIGH, also send to ${JSON.stringify(highSevGroup)} (Lark receive_id).`
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
if (mentions.length > 0) policyLines.push(`CRITICAL messages prepend: ${JSON.stringify(mentions.join(' '))}`);
|
|
182
211
|
}
|
|
183
212
|
|
|
184
|
-
const policyBlock =
|
|
213
|
+
const policyBlock = policyLines.map((l, i) => `${i + 1}. ${l}`).join('\n');
|
|
214
|
+
|
|
215
|
+
// Slack → a Block Kit card (rich, scannable, clickable View buttons). Lark →
|
|
216
|
+
// the human-voice text digest (no Block Kit there). The JUDGMENT is identical
|
|
217
|
+
// either way — the one-line read, the grouping, what's urgent — only the
|
|
218
|
+
// rendering differs by provider.
|
|
219
|
+
const writeGuide = provider === 'slack'
|
|
220
|
+
? `# How to report it — like a human on-call, in TWO messages
|
|
221
|
+
Don't cram everything into one card. Report the way a sharp on-call engineer actually would: FIRST a quick human heads-up so people get the situation in ONE glance, THEN the detailed board. So you call \`slack_post_message\` TWICE.
|
|
222
|
+
|
|
223
|
+
## Message 1 — greeting + the headline (text only, NO blocks)
|
|
224
|
+
\`slack_post_message({ channel, text })\` with just text. Open with a greeting ("👋 Hey team"). Say the ONE thing that matters most — the headline STORY, not a count ("billing/auth is broken in 3 spots, almost certainly one deploy"). Page plainly if something needs it ("on-call should grab these now"). End pointing down at the list ("Full breakdown 👇"). 2–4 sentences, sounds like a person typed it — THIS is the "one glance and you get it" message.
|
|
225
|
+
Example:
|
|
226
|
+
"👋 *Hey team — Sentry triage, ${windowLabel}.* Headline: *billing/auth is broken in 3 places* (formatSubscription, webhook timeout, Unauthorized) — almost certainly one bad deploy, on-call should grab these now. Plus a cluster of undefined-ref fatals (module/import wiring) and the usual synthetic test noise. Full breakdown 👇"
|
|
227
|
+
|
|
228
|
+
## Message 2 — the Block Kit board (blocks)
|
|
229
|
+
Then \`slack_post_message({ channel, text, blocks })\` — the scannable card. \`text\` = one-line fallback. \`blocks\`, real Block Kit objects only:
|
|
230
|
+
1. \`header\` — title with the window:
|
|
231
|
+
{ "type": "header", "text": { "type": "plain_text", "text": "🚨 Sentry Triage — ${windowLabel}", "emoji": true } }
|
|
232
|
+
2. \`context\` — ONE-line read of the window (counts + the shape; your call):
|
|
233
|
+
{ "type": "context", "elements": [{ "type": "mrkdwn", "text": "*${aboveThreshold.length}* at *${threshold}+* of ${issues.length} · <your one-line read of what's going on>" }] }
|
|
234
|
+
3. Per group — group by ROOT CAUSE or severity (your call; the header says what connects them). A divider, a section header, then per issue a section (with a View button) FOLLOWED BY a one-line context note:
|
|
235
|
+
{ "type": "divider" }
|
|
236
|
+
{ "type": "section", "text": { "type": "mrkdwn", "text": "🔴 *CRITICAL — billing/auth, page on-call*" } }
|
|
237
|
+
{ "type": "section", "text": { "type": "mrkdwn", "text": "*<title>* — <where> · <the one metric that matters>" }, "accessory": { "type": "button", "text": { "type": "plain_text", "text": "View →", "emoji": true }, "url": "<permalink>", "action_id": "v_<issueId>" } }
|
|
238
|
+
{ "type": "context", "elements": [{ "type": "mrkdwn", "text": "↳ <one short useful detail>" }] }
|
|
239
|
+
The context line is grey small text under the issue — ~6-12 words of a CONCRETE fact pulled straight from the issue's data, NEVER a vague guess. Use what's actually there: the error value (\`metadata.value\`, e.g. "Cannot read 'plan' of undefined"), the culprit fn/file, a real suspect commit ("a1b2c3 by sarah@"), or first/last-seen spread ("first seen 3d ago, 1.6k/h now"). BANNED — filler with no information: "could exacerbate latency", "potential breach attempt", "needs attention", "affects capacity". If you don't have a concrete fact for an issue, OMIT its context line entirely. Never speculate to fill space.
|
|
240
|
+
4. final \`context\` — what needs a human now vs. just FYI.
|
|
185
241
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
242
|
+
Rules:
|
|
243
|
+
- TWO slack_post_message calls: the text heads-up FIRST, then the blocks card. Both go to the same channel.
|
|
244
|
+
- header text is plain_text; section & context text is mrkdwn (*bold*, \`code\`, <url|label>).
|
|
245
|
+
- One tight line per issue in the section text; the button carries the link — don't also inline it.
|
|
246
|
+
- Group dots: 🔴 CRITICAL · 🟠 HIGH · 🟡 MEDIUM. Mention a suspect commit only if there genuinely is one.
|
|
247
|
+
- Below-threshold (skipped) issues do NOT appear in the blocks at all.
|
|
248
|
+
- Real Block Kit types only (header / section / divider / context + button accessory) — don't invent types.`
|
|
249
|
+
: `# How to write it — talk like a human, not a report generator
|
|
250
|
+
You're a teammate dropping a note in the channel, not a dashboard. Open with a real sentence about ${windowLabel} (time span baked in). Group issues that are the same story (same file/area/deploy) and SAY why they're connected. Per issue: what broke, the one number that matters, and the link. End straight: what needs a human now vs. FYI. No "*[SEVERITY]*" form blocks, no "no suspect commits" filler.
|
|
251
|
+
|
|
252
|
+
Example tone:
|
|
253
|
+
Over ${windowLabel} it's been mostly quiet, but billing's having a bad time — three errors on the checkout/subscription path, almost certainly the same deploy: \`formatSubscription is not a function\` (BillingPage, 1 user), \`POST /billing/webhook\` timing out (6×), \`countWorkflowExecutionsInPeriod\` 15× in usage-limiter. <links> Whoever shipped the billing refactor should roll back. Rest is synthetic test traffic — ignoring it.`;
|
|
189
254
|
|
|
190
255
|
// ── Prompt body ─────────────────────────────────────────────────
|
|
191
256
|
return `You are the dispatch_alerts node of a Sentry triage workflow. Post chat alerts using the **${postTool}** tool (and the lookup helpers below for author routing).
|
|
192
257
|
|
|
193
|
-
#
|
|
258
|
+
# Routing
|
|
194
259
|
${policyBlock}${overrideBlock}
|
|
195
260
|
|
|
196
261
|
# Severity scale
|
|
@@ -201,18 +266,13 @@ ${SEVERITY_LEVELS.join(' < ')}
|
|
|
201
266
|
- \`${lookupTool}\` — resolve an email to a user id. **Important**: this can return \`{ ok: false }\` — handle that by falling back to channel-only, don't retry with variations of the email.
|
|
202
267
|
${provider === 'slack' ? '- `slack_list_usergroups` / `slack_get_usergroup_members` — expand @group → user ids.' : ''}
|
|
203
268
|
|
|
204
|
-
#
|
|
205
|
-
-
|
|
206
|
-
-
|
|
269
|
+
# Context for THIS run — weave it in, don't make the reader guess
|
|
270
|
+
- Time span: every issue below is unresolved + unassigned from **${windowLabel}**${fetchedAt ? ` (pulled ${fetchedAt})` : ''}. OPEN with the span so people know what they're looking at — "Past hour was quiet…", "Over the last 30 days…". Never leave it out; a reader who doesn't know the window can't judge urgency.
|
|
271
|
+
- Volume: ${aboveThreshold.length} issue(s) at or above ${threshold}, out of ${issues.length} fetched. Mention the count only if it helps the read.
|
|
207
272
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
12 users hit /checkout — likely regression on r1234.
|
|
212
|
-
📍 handleCheckout(checkout.ts) · 47 events
|
|
213
|
-
Suspect commit: a1b2c3d4 by sarah@acme.com — "refactor checkout state"
|
|
214
|
-
https://sentry.io/.../1234/
|
|
215
|
-
\`\`\`
|
|
273
|
+
${writeGuide}
|
|
274
|
+
|
|
275
|
+
A standalone CRITICAL that should page someone can get its OWN message. Routing still applies: digest → channel; if author-DM is on and an issue has a known author, also DM that person a short note about just theirs; mention the escalation group on CRITICAL/HIGH.
|
|
216
276
|
|
|
217
277
|
# Output (outputSchema-enforced)
|
|
218
278
|
Return ONE record per dispatch call you actually made (or skipped/failed). \`issueIds\` is an array — batched messages carry every issue in the group. \`recipient\` records who got the message (channel id, user id, or usergroup id) so the audit trail shows the routing decision.
|
|
@@ -233,7 +293,7 @@ Return ONE record per dispatch call you actually made (or skipped/failed). \`iss
|
|
|
233
293
|
|
|
234
294
|
# Issues + classifications + suspect commits
|
|
235
295
|
|
|
236
|
-
Each entry
|
|
296
|
+
Each entry is a Sentry issue with the classify agent's \`classification\` (severity + reasoning) and any suspect commits Sentry's GitHub integration could blame. **An empty \`suspectCommits\` array means the team hasn't set up Sentry's GitHub integration OR the file wasn't touched in the last 14 days** — just don't mention a commit in that case.
|
|
237
297
|
|
|
238
298
|
\`\`\`json
|
|
239
299
|
${JSON.stringify(
|
|
@@ -247,9 +307,10 @@ ${JSON.stringify(
|
|
|
247
307
|
\`\`\`
|
|
248
308
|
|
|
249
309
|
# Rules
|
|
250
|
-
-
|
|
310
|
+
- The digest is usually ONE channel message → ONE \`sent\` record whose \`issueIds\` lists every issue you mentioned in it. An extra author-DM or a standalone-CRITICAL message each get their own record too.
|
|
311
|
+
- Skipped (below-threshold) issues: roll them into a single \`skipped\` record (issueIds = all of them) — no chat call, no per-issue noise — so the run record stays complete without bloating it.
|
|
251
312
|
- DON'T invent severities, issue IDs, or email addresses. Only use what's in the data block above.
|
|
252
|
-
- DON'T
|
|
313
|
+
- DON'T pad the digest. If the hour is quiet, a two-line message is the right answer — don't manufacture structure.
|
|
253
314
|
- DO post if in doubt — under-paging is worse than over-paging.
|
|
254
315
|
`;
|
|
255
316
|
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":"4.1.5","results":[[":__tests__/preflight-early-exit.test.mjs",{"duration":6.5747499999999945,"failed":false}]]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":"4.1.5","results":[[":nodes/__tests__/middleware.integration.test.js",{"duration":0,"failed":true}],[":nodes/__tests__/finalizeNode.test.js",{"duration":8.396791000000007,"failed":false}]]}
|