@zibby/workflow-templates 0.10.2 → 0.10.10
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/index.js +98 -0
- package/package.json +1 -1
- package/sentry-triage/README.md +118 -0
- package/sentry-triage/graph.mjs +120 -48
- package/sentry-triage/lib/notify.js +67 -0
- package/sentry-triage/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/sentry-triage/nodes/fetch-issue-node.js +182 -0
- package/sentry-triage/nodes/fix-node.js +338 -0
- package/sentry-triage/nodes/notify-fix-node.js +79 -0
- package/sentry-triage/nodes/open-pr-node.js +180 -0
- package/sentry-triage/package.json +2 -1
- package/sentry-triage/state.js +116 -8
package/index.js
CHANGED
|
@@ -737,6 +737,104 @@ export const TEMPLATES = {
|
|
|
737
737
|
],
|
|
738
738
|
},
|
|
739
739
|
},
|
|
740
|
+
|
|
741
|
+
// ── chat-ops-bot: per-turn chat assistant over your integrations + memory ─
|
|
742
|
+
// v0: one inbound message → one graph run → a reply. Externalized per-thread
|
|
743
|
+
// memory + connected-integration auto-discovery. Live Slack/Lark inbound and
|
|
744
|
+
// Zibby control-plane operation are NOT wired yet — be honest about scope.
|
|
745
|
+
'zibby-copilot': {
|
|
746
|
+
name: 'zibby-copilot',
|
|
747
|
+
displayName: 'Zibby Copilot',
|
|
748
|
+
description: 'The official first-party Zibby assistant — chat to operate your whole Zibby workspace through the Zibby control-plane (MCP): run, deploy, and trigger agents, install templates from the marketplace, and act across your connected integrations + memory. (v0 — today you trigger it with a message and it replies; live Slack/Lark inbound + full control-plane operation are rolling out.)',
|
|
749
|
+
path: join(__dirname, 'zibby-copilot'),
|
|
750
|
+
defaultSlug: 'zibby-copilot',
|
|
751
|
+
deps: { zod: '^3.23.0', '@zibby/skills': '^0.1.25' },
|
|
752
|
+
features: [
|
|
753
|
+
'One inbound message = one graph run (a single chat turn)',
|
|
754
|
+
'Externalized per-thread memory (conversation-store), keyed by threadKey — runtime stays stateless',
|
|
755
|
+
'Auto-discovers your connected integrations each turn and exposes their tools to the model — no redeploy',
|
|
756
|
+
'Exposes the chat-memory skill for cross-thread recall',
|
|
757
|
+
'v0 — live Slack/Lark inbound wiring is NOT included; trigger it directly with { message, threadKey }',
|
|
758
|
+
'v0 — Zibby control-plane operation (create/trigger/deploy agents + apps) is a documented seam, not yet live',
|
|
759
|
+
],
|
|
760
|
+
marketplace: {
|
|
761
|
+
slug: 'zibby-copilot',
|
|
762
|
+
// Hosting runtime marker. 'chat' = a Slack/Lark-style chat agent that
|
|
763
|
+
// runs as a Lambda (one inbound message = one chat turn), NOT a Fargate
|
|
764
|
+
// workflow. The frontend's isChatRuntime() helper reads this to hide the
|
|
765
|
+
// Fargate-only controls (CLI/schedule/webhook triggers, warm agent,
|
|
766
|
+
// Activity tab) and point the Logs tab at the Lambda runtime log group.
|
|
767
|
+
// Absent ⇒ a normal Fargate workflow (the default, unchanged).
|
|
768
|
+
runtime: 'chat',
|
|
769
|
+
tagline: 'The official Zibby copilot — run, deploy, and manage your workspace by just asking. (v0)',
|
|
770
|
+
iconPrompt: [
|
|
771
|
+
'Flat geometric vector illustration with subtle clean gradients, in the spirit of Linear / Notion / Stripe iconography — crisp, no painterly textures, reads clearly at 64×64.',
|
|
772
|
+
'Subject: a single rounded chat speech-bubble as the focal element, with a small friendly robot/ops face suggested inside it via two simple dot eyes and a tiny gear motif on its forehead — signalling a chat bot that operates things. A small cyan spark sits at the bubble\'s tail to hint at a live reply.',
|
|
773
|
+
'Palette: the bubble in a violet-to-indigo gradient (#7C3AED → #4F46E5), the gear + spark accents in soft cyan (#22D3EE), all on a deep navy (#0B0F1A) rounded square (1024×1024) with a faint radial glow centered behind the bubble.',
|
|
774
|
+
'Composition: bubble centered and dominant, gear + eyes as small secondary accents, plenty of breathing room. Mood: friendly, helpful, conversational-but-capable.',
|
|
775
|
+
'NO text, NO letters, NO photo-realism, NO sleek 3D render, NO trademarked Slack / Lark / Zibby logos.',
|
|
776
|
+
].join('\n'),
|
|
777
|
+
tags: ['Notifications', 'Support'],
|
|
778
|
+
capabilities: [
|
|
779
|
+
'Runs one chat turn per inbound message and returns a reply',
|
|
780
|
+
'Remembers the thread via an externalized conversation-store (no warm session needed)',
|
|
781
|
+
'Auto-discovers connected integrations and offers their tools to the model each turn',
|
|
782
|
+
'Exposes the chat-memory skill for cross-thread recall',
|
|
783
|
+
'v0: trigger it directly with { message, threadKey } — live Slack/Lark inbound + Zibby control-plane operation are coming',
|
|
784
|
+
],
|
|
785
|
+
conversationStarters: [
|
|
786
|
+
'Ask the bot a question and get a reply with your connected tools',
|
|
787
|
+
'Continue a thread — it remembers the earlier turns by threadKey',
|
|
788
|
+
'Have it use a connected integration to answer',
|
|
789
|
+
'Pick up where we left off in this conversation',
|
|
790
|
+
],
|
|
791
|
+
},
|
|
792
|
+
},
|
|
793
|
+
|
|
794
|
+
// ── flaky-test-fixer: collect flaky tests from CI → stabilize → fix PR ─
|
|
795
|
+
'flaky-test-fixer': {
|
|
796
|
+
name: 'flaky-test-fixer',
|
|
797
|
+
displayName: 'Flaky Test Fixer',
|
|
798
|
+
description: 'Collects flaky tests from CI (CircleCI Insights in v1), has a coding agent diagnose the root cause of the flakiness (timing/race, test-order/shared-state, randomness, network, time deps, un-awaited async) and make the smallest correct fix behind a test-gate, then opens a fix PR/MR. RSpec / npm / pytest auto-detected. Stops at the PR — a human reviews and merges.',
|
|
799
|
+
path: join(__dirname, 'flaky-test-fixer'),
|
|
800
|
+
defaultSlug: 'flaky-test-fixer',
|
|
801
|
+
deps: { zod: '^3.23.0', axios: '^1.6.0', '@zibby/skills': '^0.1.26' },
|
|
802
|
+
features: [
|
|
803
|
+
'Graph: collect_flaky (CircleCI Insights) → has_flaky? → fix (agent + git + test-gate) → open_pr → notify',
|
|
804
|
+
'Pluggable CI adapter seam (lib/ci-adapters.js) — CircleCI in v1, more providers drop in without graph changes',
|
|
805
|
+
'Flaky-specific fix prompt: diagnose the root cause class, smallest correct fix, or quarantine + say so (never a fake fix)',
|
|
806
|
+
'Inline test-gate with ONE retry; RSpec (bundle exec rspec) / npm / pytest auto-detected, or TEST_COMMAND override',
|
|
807
|
+
'Opens a GitHub PR or GitLab MR — body lists the flaky tests + diagnosis + diff; never auto-merges',
|
|
808
|
+
'CircleCI token is a SECRET in the ENV tab (CIRCLECI_TOKEN), not an input field',
|
|
809
|
+
'Self-contained (INLINE) — deploy one agent, no sub-graph dependency',
|
|
810
|
+
],
|
|
811
|
+
marketplace: {
|
|
812
|
+
slug: 'flaky-test-fixer',
|
|
813
|
+
tagline: 'Find the flaky tests in CI, stabilize them, open the fix PR.',
|
|
814
|
+
iconPrompt: [
|
|
815
|
+
'A premium, hi-fi app icon for "Flaky Test Fixer" — a workflow that finds non-deterministic ("flaky") tests in CI and stabilizes them.',
|
|
816
|
+
'Visual style: 3D-rendered hero object floating in space, in the style of Apple Vision Pro icons, Linear\'s changelog hero illustrations, or a Stripe product render. Glossy, dimensional, with subtle reflections and a soft rim-light.',
|
|
817
|
+
'Subject: a glossy 3D green test-tube / vial tilted in three-quarter perspective, its liquid mid-transition from an unstable flickering amber (a couple of jittery bubbles escaping near the top) to a calm, settled emerald-green at the base — visually reading "flaky → stabilized". A small violet healing loop-arrow (#7553FF) curves around the tube, and a tiny green checkmark spark glints where the loop closes, signalling the verified fix.',
|
|
818
|
+
'Background: a deep midnight-navy gradient (#0F172A at the top, #1E1B4B at the bottom) with a single soft violet glow behind the vial and a scatter of faint star-like specks. Square format, 1024×1024.',
|
|
819
|
+
'Composition: vial centered and slightly raised, loop-arrow wrapping behind-and-around it, subtle drop shadow on the canvas. Mood: clean, premium, reassuring — "it just settles the chaos".',
|
|
820
|
+
'NO text, NO letters, NO numbers, NO flat sticker style, NO mascot face, NO magnifying glass, NO bug/insect imagery, NO trademarked CircleCI / GitHub / GitLab logos — this one is DEEP and 3D-rendered.',
|
|
821
|
+
].join('\n'),
|
|
822
|
+
tags: ['Testing', 'Bug Triage', 'Incidents'],
|
|
823
|
+
capabilities: [
|
|
824
|
+
'Pulls flaky tests from CircleCI\'s Insights API (pluggable for more CI providers later)',
|
|
825
|
+
'Caps to maxToFix and filters by minimum flake rate per run',
|
|
826
|
+
'Coding agent diagnoses the flakiness root cause and makes the smallest correct fix',
|
|
827
|
+
'Runs the repo\'s tests with one retry; RSpec / npm / pytest auto-detected',
|
|
828
|
+
'Opens a GitHub PR or GitLab MR with the diagnosis — a human reviews and merges',
|
|
829
|
+
],
|
|
830
|
+
conversationStarters: [
|
|
831
|
+
'Find the flaky tests in my CircleCI project and open a fix PR',
|
|
832
|
+
'Stabilize the top 3 flakiest RSpec specs and run the suite before opening the PR',
|
|
833
|
+
'Only attempt tests that flake more than 20% of the time',
|
|
834
|
+
'Open a flaky-fix MR on GitLab and ping #eng when it\'s up',
|
|
835
|
+
],
|
|
836
|
+
},
|
|
837
|
+
},
|
|
740
838
|
};
|
|
741
839
|
|
|
742
840
|
export class TemplateFactory {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zibby/workflow-templates",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.10",
|
|
4
4
|
"description": "Built-in workflow templates for Zibby — browser-test-automation, code-analysis, generate-test-cases, notify-slack, notify-lark, notify-notion, sentry-triage.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# sentry-triage
|
|
2
|
+
|
|
3
|
+
One deployed agent, **two scenarios**, selected by an entry decision on
|
|
4
|
+
`trigger` (the same entry-branch shape as `github-code-review`):
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
«route» (decision on state.trigger)
|
|
8
|
+
├─ 'fix' → fetch_issue → fix → «pr_decision» ─┬─ open_pr → notify → End
|
|
9
|
+
│ └─ skip ───→ notify → End
|
|
10
|
+
└─ else → fetch_issues → «has_issues» ─┬─ classify → dispatch_alerts → End
|
|
11
|
+
└─ no issues ─────────────────→ End
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
- **`trigger: 'triage'`** (default, scheduled): list new Sentry issues →
|
|
15
|
+
LLM-classify by severity → post a human-voice digest to Slack/Lark.
|
|
16
|
+
- **`trigger: 'fix'`** (webhook / manual): pull ONE issue → clone the repo, have
|
|
17
|
+
a coding agent make the smallest correct fix with an **inline test-gate** →
|
|
18
|
+
open a PR/MR → ping chat with the result.
|
|
19
|
+
|
|
20
|
+
The fix scenario is **self-contained** — the clone/fix/PR logic is brought IN as
|
|
21
|
+
this template's own nodes (adapted from the `code-fix` template). It does **not**
|
|
22
|
+
dispatch a `code-fix` sub-graph, so deploying *only* this agent works (a
|
|
23
|
+
sub-graph would need the child separately deployed → `SUBGRAPH_NOT_FOUND`).
|
|
24
|
+
|
|
25
|
+
## TRIAGE scenario (default)
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
fetch_issues (deterministic + Sentry) → recent unresolved/unassigned issues
|
|
29
|
+
↓
|
|
30
|
+
has_issues (decision) → End early if Sentry returned nothing
|
|
31
|
+
↓
|
|
32
|
+
classify (LLM) → NOISE … CRITICAL, with reasoning
|
|
33
|
+
↓
|
|
34
|
+
dispatch_alerts (LLM + Slack/Lark) → one batched, deduped digest
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Runs on a schedule (hourly is typical). Trigger payload is just the lookback:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{ "sinceMinutes": 60 }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## FIX scenario
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
fetch_issue (deterministic + Sentry) → ONE full issue: culprit, error type/value,
|
|
47
|
+
filename, latest-event stacktrace, permalink
|
|
48
|
+
↓
|
|
49
|
+
fix (agent + git + test-gate) → clone the repo, make the SMALLEST correct
|
|
50
|
+
fix, run the repo's tests with ONE retry,
|
|
51
|
+
commit + push zibby/sentry-fix-<id>-<rand>
|
|
52
|
+
↓ (only if a branch was pushed)
|
|
53
|
+
open_pr (deterministic) → open a GitHub PR or GitLab MR
|
|
54
|
+
↓
|
|
55
|
+
notify (optional chat) → "🛠 Auto-fixed Sentry <id> → PR <url>"
|
|
56
|
+
or "⚠️ Couldn't auto-fix … needs a human"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Triggering a fix
|
|
60
|
+
|
|
61
|
+
Set `trigger: "fix"` and pass the issue id + repo. From a Sentry **issue alert**
|
|
62
|
+
webhook, or manually:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"trigger": "fix",
|
|
67
|
+
"issueId": "1234567890",
|
|
68
|
+
"repoUrl": "https://github.com/acme/web"
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
- `issueId` — required when `trigger:"fix"` (numeric id or shortId). Enforced in
|
|
73
|
+
`fetch_issue` (so a triage run never has to carry it).
|
|
74
|
+
- `repoUrl` — the repo to fix. Falls back to the `REPO_URL` ENV var if omitted.
|
|
75
|
+
|
|
76
|
+
The git token is **runner-injected** from the project's connected GitHub/GitLab
|
|
77
|
+
integration — it's spliced into the clone/push URL and used to open the PR/MR.
|
|
78
|
+
|
|
79
|
+
### Test-gate
|
|
80
|
+
|
|
81
|
+
After the agent's edit, `fix` runs the repo's test command. A non-zero exit
|
|
82
|
+
feeds the captured output back into the prompt for **one** re-edit, then stops.
|
|
83
|
+
No test command detectable → the change isn't gated (a human still reviews the
|
|
84
|
+
PR). There is no in-engine approval gate — the open PR/MR *is* the approval; we
|
|
85
|
+
never auto-merge.
|
|
86
|
+
|
|
87
|
+
## ENV-tab config
|
|
88
|
+
|
|
89
|
+
| Var | Scenario | Meaning |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `SLACK_CHANNEL` | both | Slack channel id `C012345` or `#name`. Set ONE chat target (triage digest; fix notify). |
|
|
92
|
+
| `LARK_RECEIVE_ID` | both | Lark `oc_…` chat id, `ou_…` open id, or email. |
|
|
93
|
+
| `SEVERITY_THRESHOLD` | triage | `NOISE\|LOW\|MEDIUM\|HIGH\|CRITICAL` (default `MEDIUM`) — digest skip-floor. |
|
|
94
|
+
| `SLACK_MENTIONS` / `LARK_MENTIONS` | triage | JSON array — appended to CRITICAL alerts only. |
|
|
95
|
+
| `DISPATCH_RULES` | triage | Free-form natural-language routing overrides. |
|
|
96
|
+
| `ROUTING_PREFER_AUTHOR` / `ROUTING_HIGH_SEVERITY_GROUP` | triage | Author-DM + escalation-group routing. |
|
|
97
|
+
| `REPO_URL` | fix | Repo to fix (used when the `repoUrl` input is absent). |
|
|
98
|
+
| `TEST_COMMAND` | fix | Override the auto-detected test command (`npm test`, `pytest -q`, …). |
|
|
99
|
+
| `PR_BASE` | fix | Base branch for the PR/MR (default `main`). |
|
|
100
|
+
|
|
101
|
+
## Required integrations
|
|
102
|
+
|
|
103
|
+
| Integration | Why |
|
|
104
|
+
|---|---|
|
|
105
|
+
| **Sentry** (required) | both scenarios — list issues (triage) / fetch one issue (fix). |
|
|
106
|
+
| **GitHub OR GitLab** | the fix scenario clones + pushes + opens a PR/MR. Surfaced as an optional connect (the `git` meta-skill clones public repos anonymously), but a token is needed in practice for private repos + to push/open the PR. |
|
|
107
|
+
| **Slack OR Lark** | the triage digest (required OR-group). The fix-scenario ping reuses the same target and degrades gracefully if neither is set. |
|
|
108
|
+
|
|
109
|
+
## Reused vs new
|
|
110
|
+
|
|
111
|
+
- **Reused**: the entire TRIAGE scenario (`fetch_issues` / `classify` /
|
|
112
|
+
`dispatch_alerts`) is unchanged. The fix scenario's clone/edit/test-gate/push
|
|
113
|
+
mechanics + GitHub PR creation are adapted from the `code-fix` template
|
|
114
|
+
(`fix-code-node` + `create-pr-node`); the chat-notify helper mirrors
|
|
115
|
+
`github-code-review/lib/notify.js`.
|
|
116
|
+
- **New**: the entry `route` decision (triage vs fix), the single-issue
|
|
117
|
+
`fetch_issue` (with latest-event stacktrace hydration), the inline-clone fix
|
|
118
|
+
node, and **GitLab MR creation** in `open_pr` (code-fix was GitHub-only).
|
package/sentry-triage/graph.mjs
CHANGED
|
@@ -1,35 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* sentry-triage — parent workflow.
|
|
2
|
+
* sentry-triage — MULTI-SCENARIO parent workflow.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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.
|
|
4
|
+
* ONE deployed agent, TWO scenarios, selected by an entry decision on
|
|
5
|
+
* state.trigger (mirrors github-code-review's entry `route`):
|
|
10
6
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* wins (faster, free, no
|
|
17
|
-
* hallucinated queries).
|
|
18
|
-
* ↓
|
|
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.
|
|
25
|
-
* ↓
|
|
26
|
-
* dispatch_alerts (LLM agent + SKILLS.CHAT_NOTIFY) → one human-voice digest;
|
|
27
|
-
* routing is the user's
|
|
28
|
-
* (DISPATCH_RULES) to own.
|
|
7
|
+
* «route» (decision on state.trigger)
|
|
8
|
+
* ├─ 'fix' → fetch_issue → fix → «pr_decision» ─┬─ open_pr → notify → End
|
|
9
|
+
* │ └─ skip ───→ notify → End
|
|
10
|
+
* └─ else → fetch_issues → «has_issues» ─┬─ classify → dispatch_alerts → End
|
|
11
|
+
* └─ no issues ─────────────────→ End
|
|
29
12
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
13
|
+
* ── TRIAGE scenario (default — UNCHANGED scheduled hourly triage) ──
|
|
14
|
+
* fetch_issues (deterministic + SKILLS.SENTRY) — pull recent unresolved/
|
|
15
|
+
* unassigned issues + suspect
|
|
16
|
+
* commits.
|
|
17
|
+
* has_issues (decision) — short-circuit to End when
|
|
18
|
+
* Sentry returned nothing.
|
|
19
|
+
* classify (LLM agent) — label NOISE…CRITICAL.
|
|
20
|
+
* dispatch_alerts (LLM agent + SKILLS.CHAT_NOTIFY)— one human-voice digest.
|
|
21
|
+
*
|
|
22
|
+
* ── FIX scenario (NEW — INLINE, self-contained: NO sub-graph dispatch) ──
|
|
23
|
+
* fetch_issue (deterministic + SKILLS.SENTRY) — pull ONE full issue
|
|
24
|
+
* (state.issueId): culprit,
|
|
25
|
+
* metadata, latest stacktrace.
|
|
26
|
+
* fix (agent + SKILLS.GIT + inline — clone the repo (repoUrl /
|
|
27
|
+
* test-gate) REPO_URL), make the smallest
|
|
28
|
+
* correct fix, run tests with
|
|
29
|
+
* ONE retry, commit + push a
|
|
30
|
+
* zibby/sentry-fix-<id>-<rand>
|
|
31
|
+
* branch (github OR gitlab).
|
|
32
|
+
* pr_decision (decision) — open_pr only when a branch was
|
|
33
|
+
* pushed; else straight to notify.
|
|
34
|
+
* open_pr (deterministic) — open a GitHub PR / GitLab MR.
|
|
35
|
+
* notify (OPTIONAL chat, custom-execute) — Slack/Lark "auto-fixed → PR"
|
|
36
|
+
* or "couldn't fix — needs a
|
|
37
|
+
* human". Graceful (never throws).
|
|
38
|
+
*
|
|
39
|
+
* WHY INLINE (not a code-fix sub-graph): sub-graph dispatch
|
|
40
|
+
* ({ workflow:'code-fix' }) requires the child to be SEPARATELY deployed →
|
|
41
|
+
* SUBGRAPH_NOT_FOUND for a customer who deploys only this agent. The clone/fix/
|
|
42
|
+
* PR logic is adapted IN from code-fix's nodes so deploying this one agent works.
|
|
43
|
+
*
|
|
44
|
+
* Integrations: SENTRY required (both scenarios). The fix scenario's `fix` node
|
|
45
|
+
* declares SKILLS.GIT → "GitHub OR GitLab" required for that scenario.
|
|
46
|
+
* chat_notify is required via the triage dispatcher (OR-group Slack/Lark); the
|
|
47
|
+
* fix-scenario notify keeps its post optional (calls the handler directly).
|
|
33
48
|
*/
|
|
34
49
|
|
|
35
50
|
import { WorkflowAgent, WorkflowGraph } from '@zibby/core';
|
|
@@ -38,11 +53,35 @@ import { fetchIssuesNode } from './nodes/fetch-issues-node.js';
|
|
|
38
53
|
import { classifyNode } from './nodes/classify-node.js';
|
|
39
54
|
import { dispatchNode } from './nodes/dispatch-node.js';
|
|
40
55
|
|
|
56
|
+
import { fetchIssueNode } from './nodes/fetch-issue-node.js';
|
|
57
|
+
import { fixNode } from './nodes/fix-node.js';
|
|
58
|
+
import { openPRNode } from './nodes/open-pr-node.js';
|
|
59
|
+
import { notifyFixNode } from './nodes/notify-fix-node.js';
|
|
60
|
+
|
|
41
61
|
import {
|
|
42
62
|
sentryTriageInputSchema,
|
|
43
63
|
sentryTriageContextSchema,
|
|
44
64
|
} from './state.js';
|
|
45
65
|
|
|
66
|
+
// Entry decision — trigger:'fix' routes to the auto-fix branch; everything else
|
|
67
|
+
// (incl. the absent/default 'triage') runs the scheduled triage. Returns the
|
|
68
|
+
// real entry-node id of each branch so the runtime routes directly to it.
|
|
69
|
+
function routeByTrigger(state) {
|
|
70
|
+
return state?.trigger === 'fix' ? 'fetch_issue' : 'fetch_issues';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// TRIAGE: skip everything when Sentry returned nothing this window.
|
|
74
|
+
function routeHasIssues(state) {
|
|
75
|
+
return (state?.fetch_issues?.issues || []).length === 0 ? 'END' : 'classify';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// FIX: open a PR/MR only when `fix` actually pushed a branch; otherwise skip
|
|
79
|
+
// straight to notify (which posts the "couldn't fix" message).
|
|
80
|
+
function routeFixToPR(state) {
|
|
81
|
+
const ok = state?.fix?.success === true && !!state?.fix?.branch;
|
|
82
|
+
return ok ? 'open_pr' : 'notify';
|
|
83
|
+
}
|
|
84
|
+
|
|
46
85
|
export class SentryTriageAgent extends WorkflowAgent {
|
|
47
86
|
buildGraph() {
|
|
48
87
|
const graph = new WorkflowGraph();
|
|
@@ -50,42 +89,75 @@ export class SentryTriageAgent extends WorkflowAgent {
|
|
|
50
89
|
.setInputSchema(sentryTriageInputSchema)
|
|
51
90
|
.setContextSchema(sentryTriageContextSchema);
|
|
52
91
|
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
// its labeled edges so the logic lives in one place.)
|
|
56
|
-
const routeHasIssues = (state) =>
|
|
57
|
-
(state?.fetch_issues?.issues || []).length === 0 ? 'END' : 'classify';
|
|
92
|
+
// Entry decision — auto-renders as a Condition diamond (ConditionalNode).
|
|
93
|
+
graph.addConditionalNode('route', { condition: routeByTrigger });
|
|
58
94
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
// comes OUT of this, not hung off the fetch_issues work node.
|
|
95
|
+
// ── TRIAGE branch ────────────────────────────────────────────────
|
|
96
|
+
graph.addNode('fetch_issues', fetchIssuesNode);
|
|
62
97
|
graph.addConditionalNode('has_issues', { condition: routeHasIssues });
|
|
63
|
-
graph.addNode('classify',
|
|
98
|
+
graph.addNode('classify', classifyNode);
|
|
64
99
|
graph.addNode('dispatch_alerts', dispatchNode);
|
|
65
100
|
|
|
66
|
-
|
|
101
|
+
// ── FIX branch ───────────────────────────────────────────────────
|
|
102
|
+
graph.addNode('fetch_issue', fetchIssueNode);
|
|
103
|
+
graph.addNode('fix', fixNode);
|
|
104
|
+
graph.addConditionalNode('pr_decision', { condition: routeFixToPR });
|
|
105
|
+
graph.addNode('open_pr', openPRNode);
|
|
106
|
+
graph.addNode('notify', notifyFixNode);
|
|
107
|
+
|
|
108
|
+
graph.setEntryPoint('route');
|
|
109
|
+
|
|
110
|
+
// Entry diamond: fix → fetch_issue …; triage → fetch_issues …
|
|
111
|
+
graph.addConditionalEdges('route', routeByTrigger, {
|
|
112
|
+
labels: { fetch_issue: 'fix', fetch_issues: 'triage' },
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// TRIAGE flow (unchanged).
|
|
67
116
|
graph.addEdge('fetch_issues', 'has_issues');
|
|
68
|
-
// Short-circuit when Sentry returned nothing for this window. The empty-list
|
|
69
|
-
// case is the common idle path, and running classify + dispatch on empty
|
|
70
|
-
// input wastes two Claude calls per run — at hourly cadence across many
|
|
71
|
-
// tenants that adds up. Routing to END at the graph level (vs short-circuit
|
|
72
|
-
// inside each prompt) skips the model round-trips entirely.
|
|
73
117
|
graph.addConditionalEdges('has_issues', routeHasIssues, {
|
|
74
118
|
labels: { classify: 'has issues', END: 'no issues' },
|
|
75
119
|
});
|
|
76
|
-
graph.addEdge('classify',
|
|
120
|
+
graph.addEdge('classify', 'dispatch_alerts');
|
|
77
121
|
graph.addEdge('dispatch_alerts', 'END');
|
|
78
122
|
|
|
123
|
+
// FIX flow.
|
|
124
|
+
graph.addEdge('fetch_issue', 'fix');
|
|
125
|
+
graph.addConditionalEdges('fix', routeFixToPR, {
|
|
126
|
+
labels: { open_pr: 'pushed a fix', notify: 'no fix' },
|
|
127
|
+
});
|
|
128
|
+
graph.addEdge('open_pr', 'notify');
|
|
129
|
+
graph.addEdge('notify', 'END');
|
|
130
|
+
|
|
79
131
|
return graph;
|
|
80
132
|
}
|
|
81
133
|
|
|
82
134
|
async onComplete(result) {
|
|
83
|
-
const
|
|
84
|
-
|
|
135
|
+
const state = result?.state || {};
|
|
136
|
+
|
|
137
|
+
// FIX scenario: log the fix outcome.
|
|
138
|
+
if (state.trigger === 'fix') {
|
|
139
|
+
const issue = state.fetch_issue?.issue || {};
|
|
140
|
+
const fix = state.fix || {};
|
|
141
|
+
const pr = state.open_pr || {};
|
|
142
|
+
const notify = state.notify_fix || {};
|
|
143
|
+
console.log(
|
|
144
|
+
`[sentry-triage] complete — mode=fix ` +
|
|
145
|
+
`issue=${issue.shortId || issue.id || 'n/a'} ` +
|
|
146
|
+
`branch=${fix.branch || '(none)'} ` +
|
|
147
|
+
`tested=${fix.tested === true} testsPassed=${fix.testsPassed === true} ` +
|
|
148
|
+
`pr_url=${pr.pr_url || '(none)'} ` +
|
|
149
|
+
`notify=${notify.sent ? notify.target : (notify.skipped ? 'skipped' : 'no')}`,
|
|
150
|
+
);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// TRIAGE scenario (unchanged).
|
|
155
|
+
const s = state.dispatch_alerts?.summary || {};
|
|
156
|
+
const classifications = state.classify?.classifications || [];
|
|
85
157
|
const noise = classifications.filter((c) => c.severity === 'NOISE').length;
|
|
86
|
-
const fetched =
|
|
158
|
+
const fetched = state.fetch_issues?.issues?.length || 0;
|
|
87
159
|
console.log(
|
|
88
|
-
`[sentry-triage] complete — fetched=${fetched}, noise=${noise}, ` +
|
|
160
|
+
`[sentry-triage] complete — mode=triage fetched=${fetched}, noise=${noise}, ` +
|
|
89
161
|
`sent=${s.sent || 0}, skipped=${s.skipped || 0}, failed=${s.failed || 0}`,
|
|
90
162
|
);
|
|
91
163
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional chat notification — provider-agnostic, best-effort, NEVER throws.
|
|
3
|
+
*
|
|
4
|
+
* Used by the FIX scenario's `notify` node to post a one-line "auto-fixed /
|
|
5
|
+
* couldn't fix" ping. Identical in spirit to github-code-review/lib/notify.js
|
|
6
|
+
* (templates can't import across dirs, so each ships its own copy): it calls the
|
|
7
|
+
* chat_notify meta-skill's tool handler DIRECTLY rather than declaring
|
|
8
|
+
* SKILLS.CHAT_NOTIFY on the node, so this particular post stays graceful — a
|
|
9
|
+
* missing/unconnected chat integration just yields { sent:false }, and the fix
|
|
10
|
+
* (PR already opened) is unaffected.
|
|
11
|
+
*
|
|
12
|
+
* Slack is preferred when both targets are set. The string literals
|
|
13
|
+
* 'slack'/'lark' ALSO double as the signal the workflow executor's usage-scan
|
|
14
|
+
* keys off to inject SLACK_BOT_TOKEN / LARK_APP_ID|SECRET into the task env.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
function parseToolResult(raw) {
|
|
18
|
+
if (raw == null) return null;
|
|
19
|
+
if (typeof raw === 'object') return raw;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(raw);
|
|
22
|
+
} catch {
|
|
23
|
+
return { error: 'non-JSON tool result' };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Post to Slack (slackChannel) or Lark (larkReceiveId). Returns
|
|
29
|
+
* { sent, target?, detail? } and never throws.
|
|
30
|
+
*
|
|
31
|
+
* @param {{ slackChannel?: string, larkReceiveId?: string, text: string }} args
|
|
32
|
+
* @returns {Promise<{ sent: boolean, target?: string, detail?: string }>}
|
|
33
|
+
*/
|
|
34
|
+
export async function postChatNotification({ slackChannel, larkReceiveId, text } = {}) {
|
|
35
|
+
const channel = slackChannel && String(slackChannel).trim();
|
|
36
|
+
const receiveId = larkReceiveId && String(larkReceiveId).trim();
|
|
37
|
+
if (!channel && !receiveId) return { sent: false, detail: 'no notify target' };
|
|
38
|
+
if (!text || !String(text).trim()) return { sent: false, detail: 'empty message' };
|
|
39
|
+
|
|
40
|
+
if (channel) {
|
|
41
|
+
try {
|
|
42
|
+
const { chatNotifySkill } = await import('@zibby/skills');
|
|
43
|
+
const data = parseToolResult(
|
|
44
|
+
await chatNotifySkill.handleToolCall('slack_post_message', { channel, text }),
|
|
45
|
+
);
|
|
46
|
+
if (data && !data.error && (data.ok || data.ts)) {
|
|
47
|
+
return { sent: true, target: `slack:${channel}` };
|
|
48
|
+
}
|
|
49
|
+
return { sent: false, target: `slack:${channel}`, detail: (data && data.error) || 'slack post failed' };
|
|
50
|
+
} catch (e) {
|
|
51
|
+
return { sent: false, target: `slack:${channel}`, detail: `slack post error (non-fatal): ${e?.message || e}` };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const { chatNotifySkill } = await import('@zibby/skills');
|
|
57
|
+
const data = parseToolResult(
|
|
58
|
+
await chatNotifySkill.handleToolCall('lark_send_message', { receive_id: receiveId, text }),
|
|
59
|
+
);
|
|
60
|
+
if (data && !data.error && (data.ok || data.message_id || data.data)) {
|
|
61
|
+
return { sent: true, target: `lark:${receiveId}` };
|
|
62
|
+
}
|
|
63
|
+
return { sent: false, target: `lark:${receiveId}`, detail: (data && data.error) || 'lark send failed' };
|
|
64
|
+
} catch (e) {
|
|
65
|
+
return { sent: false, target: `lark:${receiveId}`, detail: `lark send error (non-fatal): ${e?.message || e}` };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":"4.1.7","results":[[":nodes/__tests__/dispatch.test.js",{"duration":8.644457999999986,"failed":false}],[":__tests__/graph.integration.test.js",{"duration":8.647458000000029,"failed":false}],[":nodes/__tests__/open-pr.test.js",{"duration":10.167542000000026,"failed":false}],[":nodes/__tests__/fix.test.js",{"duration":12.375999999999976,"failed":false}],[":nodes/__tests__/notify-fix.test.js",{"duration":6.512499999999989,"failed":false}]]}
|