@zibby/workflow-templates 0.10.4 → 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 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.4",
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).
@@ -1,35 +1,50 @@
1
1
  /**
2
- * sentry-triage — parent workflow. Hourly Sentry issue triage.
2
+ * sentry-triage — MULTI-SCENARIO parent workflow.
3
3
  *
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.
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
- * 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).
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
- * 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.
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
- // Route OUT of the decision: skip everything when Sentry returned nothing
54
- // this window, else classify. (Shared by the decision node's condition and
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
- graph.addNode('fetch_issues', fetchIssuesNode);
60
- // Explicit decision node → renders as a clean Condition diamond. The branch
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', classifyNode);
98
+ graph.addNode('classify', classifyNode);
64
99
  graph.addNode('dispatch_alerts', dispatchNode);
65
100
 
66
- graph.setEntryPoint('fetch_issues');
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', 'dispatch_alerts');
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 s = result?.state?.dispatch_alerts?.summary || {};
84
- const classifications = result?.state?.classify?.classifications || [];
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 = result?.state?.fetch_issues?.issues?.length || 0;
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}]]}