@sweny-ai/core 0.1.149 → 0.1.150

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.
@@ -3,6 +3,14 @@ name: Alert Triage
3
3
  workflow_type: monitor
4
4
  description: Investigate a production alert, determine root cause, create an issue, implement a fix, and notify the team
5
5
  entry: gather
6
+ # Ceiling (#365). `write` because implement and create_pr change code in the
7
+ # checkout and push a branch; every other node narrows to `read`. External
8
+ # writes (issues, comments, the PR) happen only as declared outputs, applied
9
+ # by sweny after the node, within these types and this run-wide cap.
10
+ permissions: write
11
+ safe_outputs:
12
+ allow: [issue, comment, pr]
13
+ max: 25
6
14
  nodes:
7
15
  gather:
8
16
  name: Gather Context
@@ -35,30 +43,14 @@ nodes:
35
43
  - datadog
36
44
  - betterstack
37
45
  - linear
38
- # Structural enforcement of the read-only scope above: write tools from
39
- # the github/linear skills are never registered for this node, so the
40
- # model cannot call them no matter what the prompt fails to prevent.
41
- # Driving incident: the 2026-06-08 letsoffload/permit-service scheduled
42
- # run, where gather created Linear issues OFF-2332/OFF-2333 and PRs
43
- # #101/#102 from inside this node and then died at the 50-turn cap.
44
- tools:
45
- deny:
46
- - linear_create_issue
47
- - linear_add_comment
48
- - linear_update_issue
49
- - github_create_issue
50
- - github_add_comment
51
- - github_create_pr
52
- # Same incident: gather authored code changes mid-gather. Write/Edit are
53
- # removed from the model context entirely. Bash stays available because
54
- # read-side investigation legitimately uses it (git log, grep); blocking
55
- # specific Bash invocations (git commit, gh pr create) needs the SDK's
56
- # canUseTool callback and is tracked as follow-up work (see the implement
57
- # node's note below).
58
- disallowed_tools:
59
- - Write
60
- - Edit
61
- - NotebookEdit
46
+ # Structural enforcement of the read-only scope above (#365): `read`
47
+ # registers only read skill tools, withholds external MCP servers, and
48
+ # removes every write-capable built-in (shell, file edits, subagents), on
49
+ # every harness. Driving incident: the 2026-06-08
50
+ # letsoffload/permit-service scheduled run, where gather created Linear
51
+ # issues OFF-2332/OFF-2333 and PRs #101/#102 from inside this node, and
52
+ # authored code changes, then died at the 50-turn cap.
53
+ permissions: read
62
54
  # When gather still dies early (turn cap, SDK error), proceed with the
63
55
  # partial context instead of failing the whole workflow: investigate has
64
56
  # its own tools and can re-derive what gather missed. The 2026-06-08 run
@@ -140,16 +132,9 @@ nodes:
140
132
  skills:
141
133
  - github
142
134
  - linear
143
- # Structural enforcement of the scope boundaries above: the write tools
144
- # this node is told not to call are not registered for it at all.
145
- tools:
146
- deny:
147
- - linear_create_issue
148
- - linear_add_comment
149
- - linear_update_issue
150
- - github_create_issue
151
- - github_add_comment
152
- - github_create_pr
135
+ # Structural enforcement of the scope boundaries above (#365): the node
136
+ # is read-only, so no write tool or write-capable built-in reaches it.
137
+ permissions: read
153
138
  eval:
154
139
  # The novelty check REQUIRES searching the issue tracker. If the node finishes
155
140
  # with 0 search tool calls, it means the agent skipped the novelty check entirely
@@ -225,6 +210,14 @@ nodes:
225
210
  Process ALL findings from the investigation. The findings array contains both novel and duplicate issues.
226
211
 
227
212
 
213
+ **How writes work here.** If you have the `emit_output` tool, you cannot write to the tracker directly:
214
+ request each new issue and each comment with `emit_output`, and sweny files them after this step, within
215
+ its limits. A requested issue has no identifier yet; sweny records it for the next steps. Without
216
+ `emit_output`, use the tracker's own tools (for example `linear_create_issue`, `github_create_issue`,
217
+ `linear_add_comment`, `github_add_comment`, or the configured tracker's MCP tools) and copy the
218
+ identifier they return.
219
+
220
+
228
221
  **For each NOVEL finding** (is_duplicate = false):
229
222
 
230
223
  1. **First, check if a prior triage run already created an issue for this exact bug.** Call
@@ -232,76 +225,86 @@ nodes:
232
225
  If a matching issue already exists with the same root cause:
233
226
  - Treat it as a duplicate: DO NOT create a new issue.
234
227
  - Populate `issueIdentifier`, `issueTitle`, and `issueUrl` from the existing issue.
235
- - Add a "+1" comment if appropriate (using `linear_add_comment` or `github_add_comment`).
228
+ - Add a "+1" comment if appropriate.
236
229
  - Set the `action` to `updated` in the `issues` array.
237
230
  - This prevents duplicate tickets when the same alert fires multiple times.
238
231
 
239
- 2. If no matching issue exists, create a new one with a clear, actionable title by invoking
240
- `linear_create_issue` or `github_create_issue`. You MUST actually call the tool — do NOT
241
- synthesize an identifier from an alert ID, commit SHA, or your own imagination. If the
242
- tool errors, stop and report the failure; do not invent a fallback identifier.
243
- **Use the exact tool names above** — do NOT use generic alternatives like `create_issue`
244
- or `create_pull_request` from ToolSearch. The eval check requires the specific tool
245
- names listed here.
232
+ 2. If no matching issue exists, file a new one with a clear, actionable title (type `issue`). Do NOT
233
+ synthesize an identifier from an alert ID, commit SHA, or your own imagination: a new issue's
234
+ identifier comes only from the tracker. Set `dedupe_key` to a short stable fingerprint of the root
235
+ cause, so two findings with the same cause are filed once. Set the `action` to `requested` (or
236
+ `created` when you filed it with a tracker tool and have its identifier).
246
237
 
247
238
  3. Include: root cause, severity, affected services, reproduction steps, and recommended fix.
248
239
 
249
240
  4. Apply the `sweny`, `agent`, and `triage` labels. For Linear: call `linear_list_labels` to find the
250
- IDs for labels named `sweny`, `agent`, and `triage`, then pass those IDs in `labelIds` when calling
251
- `linear_create_issue` (skip any that don't exist in the team — don't invent IDs). For GitHub: pass
252
- `labels: ["sweny", "agent", "triage"]` to `github_create_issue`.
241
+ IDs for labels named `sweny`, `agent`, and `triage`, and pass those IDs as the labels (skip any that
242
+ don't exist in the team; don't invent IDs). For Linear, the target is the team ID (`linear_list_teams`).
243
+ For GitHub: labels `["sweny", "agent", "triage"]`.
253
244
 
254
245
  5. Link to relevant commits, PRs, or existing issues.
255
246
 
256
- 6. Copy the identifier returned by the tool verbatim into the `issueIdentifier` output
257
- field. The identifier must match `^[A-Z][A-Z0-9]*-\d+$` (e.g. `OFF-1234`) — do not
258
- prefix it with anything else.
247
+ 6. When you know the identifier (an existing issue, or one a tracker tool returned), copy it verbatim
248
+ into `issueIdentifier`. It must match `^[A-Z][A-Z0-9]*-\d+$` (e.g. `OFF-1234`); do not prefix it with
249
+ anything else. Leave `issueIdentifier` unset for an issue you only requested.
259
250
 
260
251
 
261
252
  **For each DUPLICATE finding** (is_duplicate = true):
262
253
 
263
254
  1. Find the existing issue (check duplicate_of field).
264
255
 
265
- 2. Check the issue's comments — if the most recent comment is already from SWEny (contains "+1") within the last
266
- 24 hours, skip adding another comment.
256
+ 2. Check the issue's comments. If the most recent comment is already from SWEny (contains "+1") within
257
+ the last 24 hours, skip adding another comment.
267
258
 
268
- 3. Otherwise add a SHORT comment: "+1 — seen again {UTC timestamp}. {one sentence of new context}." (Keep it under
269
- 2 lines. No markdown headers, no emoji, no formatting.)
259
+ 3. Otherwise add a SHORT comment (type `comment`, `number` = the issue ID or number): "+1 seen again
260
+ {UTC timestamp}. {one sentence of new context}." (Keep it under 2 lines. No markdown headers, no emoji,
261
+ no formatting.)
270
262
 
271
- 4. If the existing issue is closed/done, reopen it.
263
+ 4. If the existing issue is closed or done: reopen it when you have a tool that can; otherwise say in
264
+ the comment that it is closed and was seen again.
272
265
 
273
266
 
274
267
  If context.issueTemplate is provided, use it as the format for new issue bodies. Otherwise use a clear structure
275
268
  with: Summary, Root Cause, Impact, Steps to Reproduce, and Recommended Fix.
276
269
 
277
270
 
278
- Use the issue tracker specified in context (e.g. Linear, Jira, or GitHub Issues) — do NOT fall back to a
279
- different tracker. Output the created/updated issue identifiers.
271
+ Use the issue tracker specified in context (e.g. Linear, Jira, or GitHub Issues). Do NOT fall back to a
272
+ different tracker. Output the issues you requested, created, or updated.
280
273
 
281
274
 
282
- **IMPORTANT — scope boundaries for this node:**
275
+ **IMPORTANT: scope boundaries for this node:**
283
276
 
284
277
  - DO NOT create branches, commits, or pull requests. The `implement` and `create_pr` nodes handle that.
285
278
 
286
- - DO NOT call `create_pull_request`, `github_create_pr`, or any PR/branch tools.
287
-
288
279
  - Your job is issue creation and duplicate management ONLY.
289
280
  skills:
290
281
  - linear
291
282
  - github
292
- # Structural enforcement: this node manages issues, never PRs.
283
+ # #365: this node reads and decides; it never holds a tracker write.
284
+ # Issues and +1 comments are declared outputs: sweny applies them after
285
+ # the node succeeds (and after its eval), via the configured tracker
286
+ # skill, capped per run and deduped. The node is read-only by default
287
+ # because it declares outputs. The next steps read the new issue's
288
+ # identifier from `context.create_issue.safe_outputs`.
289
+ outputs:
290
+ - type: issue
291
+ max: 10
292
+ - type: comment
293
+ max: 10
294
+ # Legacy runs (a tracker with no safe-output skill, see
295
+ # cli/builtin-workflows.ts) still never open PRs from here.
293
296
  tools:
294
297
  deny:
295
298
  - github_create_pr
296
299
  eval:
297
- # Require a real issue-tracker tool call. linear_create_issue / github_create_issue
298
- # for novel findings; linear_search_issues / github_search_issues for duplicate-check
299
- # when the issue already exists from a prior triage run. linear_add_comment covers
300
- # the +1 path for duplicates.
300
+ # Require a real issue-tracker action: a search (the duplicate check) or
301
+ # a requested write. emit_output covers safe outputs; the tracker tool
302
+ # names cover a legacy run that writes directly.
301
303
  - name: issue_tracker_tool_was_called
302
304
  kind: function
303
305
  rule:
304
306
  any_tool_called:
307
+ - emit_output
305
308
  - linear_create_issue
306
309
  - github_create_issue
307
310
  - linear_search_issues
@@ -315,21 +318,21 @@ nodes:
315
318
  type: string
316
319
  pattern: '^[A-Z][A-Z0-9]*-\d+$'
317
320
  description: >-
318
- Primary issue identifier (e.g. OFF-1234). MUST be the identifier returned by
319
- `linear_create_issue` / `github_create_issue` — do NOT invent a value from a
320
- Sentry short-ID or commit SHA. Used by downstream nodes for branch naming and
321
- PR linking.
321
+ Primary issue identifier (e.g. OFF-1234) when known in this step: an existing issue the
322
+ duplicate check matched, or one a tracker tool returned. Unset for an issue only requested
323
+ through emit_output; sweny records its identifier in safe_outputs. Never invented from a
324
+ Sentry short-ID or commit SHA.
322
325
  issueTitle:
323
326
  type: string
324
- description: Title of the primary issue created
327
+ description: Title of the primary issue
325
328
  issueUrl:
326
329
  type: string
327
330
  pattern: "^https?://"
328
- description: URL of the primary issue created, as returned by the create-issue tool
331
+ description: URL of the primary issue, when known in this step
329
332
  issues:
330
333
  type: array
331
334
  minItems: 1
332
- description: All issues created or updated during this step
335
+ description: All issues requested, created, or updated during this step
333
336
  items:
334
337
  type: object
335
338
  properties:
@@ -342,23 +345,21 @@ nodes:
342
345
  action:
343
346
  type: string
344
347
  enum:
348
+ - requested
345
349
  - created
346
350
  - updated
347
351
  - reopened
348
352
  required:
349
- - identifier
350
353
  - action
351
354
  required:
352
- - issueIdentifier
353
355
  - issueTitle
354
- - issueUrl
355
356
  - issues
356
357
  retry:
357
358
  max: 1
358
359
  instruction:
359
360
  auto: true
360
361
  skip:
361
- name: Skip — All Duplicates or Low Priority
362
+ name: Skip - All Duplicates or Low Priority
362
363
  instruction: >-
363
364
  Every finding from the investigation was either a duplicate or low-priority. No new issues need to be created.
364
365
 
@@ -367,32 +368,38 @@ nodes:
367
368
 
368
369
  1. Find the existing issue (check duplicate_of field).
369
370
 
370
- 2. Check the issue's comments — if the most recent comment is already from SWEny (contains "+1") within the last
371
- 24 hours, skip adding another comment.
371
+ 2. Check the issue's comments. If the most recent comment is already from SWEny (contains "+1") within
372
+ the last 24 hours, skip adding another comment.
372
373
 
373
- 3. Otherwise add a SHORT comment: "+1 — seen again {UTC timestamp}. {one sentence of new context}." (Keep it under
374
- 2 lines. No markdown headers, no emoji, no formatting.)
374
+ 3. Otherwise add a SHORT comment (with `emit_output` type `comment` when you have it, `number` = the
375
+ issue ID or number): "+1 seen again {UTC timestamp}. {one sentence of new context}." (Keep it under 2
376
+ lines. No markdown headers, no emoji, no formatting.)
375
377
 
376
- 4. If the issue is closed/done, reopen it.
378
+ 4. If the issue is closed or done: reopen it when you have a tool that can; otherwise say in the comment
379
+ that it is closed and was seen again.
377
380
 
378
381
 
379
382
  For **low priority** findings, log a brief note about why they were skipped.
380
383
  skills:
381
384
  - linear
382
385
  - github
383
- # Structural enforcement: this node comments on and reopens existing
384
- # issues only. It never creates issues or PRs.
385
- tools:
386
- deny:
387
- - linear_create_issue
388
- - github_create_issue
389
- - github_create_pr
386
+ # #365: comments on existing issues only, as a declared output. Read-only
387
+ # otherwise: it never creates issues or PRs.
388
+ outputs:
389
+ - type: comment
390
+ max: 10
390
391
  implement:
391
392
  name: Implement Fix
392
393
  instruction: |-
393
394
  Implement the fix identified during investigation, plus the tests that pin
394
- it as a regression guard. The issue created in the previous step is in your
395
- context — use its identifier for branch naming and commit messages.
395
+ it as a regression guard. The issue from the previous step is in your
396
+ context. Use its identifier for branch naming and commit messages.
397
+
398
+ **The issue identifier** (ISSUE_ID below): the `ref` of the `issue` entry
399
+ with status `applied` in `context.create_issue.safe_outputs` (the issue
400
+ sweny filed after the previous step), else
401
+ `context.create_issue.issueIdentifier` (an existing issue the previous
402
+ step matched, or one it filed directly).
396
403
 
397
404
  ## Quality bar (non-negotiable)
398
405
 
@@ -425,12 +432,18 @@ nodes:
425
432
 
426
433
  ## Steps
427
434
 
428
- 0a. **Precondition — verify context.create_issue.issueIdentifier is well-formed.**
435
+ 0. **Precondition: staged preview.** If the `issue` entry in
436
+ `context.create_issue.safe_outputs` has status `staged`, this run is a
437
+ preview (`--stage`) and no issue exists. STOP before touching the
438
+ checkout. Output `test_status: "skipped"`, `skipped_reason: "staged"`,
439
+ empty file lists, `branch: ""`, `commit_sha: ""`.
440
+
441
+ 0a. **Precondition: verify ISSUE_ID is well-formed.**
429
442
  The identifier MUST match `^[A-Z][A-Z0-9]*-\d+$` (e.g. `OFF-1234`). If it
430
443
  is missing, empty, or matches a Sentry short-ID / slug / any other shape,
431
- STOP immediately — do NOT invent a branch name from the Sentry event,
444
+ STOP immediately. Do NOT invent a branch name from the Sentry event,
432
445
  commit SHA, or alert payload. Return a failed status with a clear message
433
- instead; the workflow will route to notify and a human will pick it up.
446
+ instead; a human will pick it up.
434
447
 
435
448
  0b. **Precondition — check for an existing open PR for this issue.** Call
436
449
  `github_search_issues` (which hits GitHub's `/search/issues` endpoint
@@ -466,12 +479,12 @@ nodes:
466
479
 
467
480
  1. Create a feature branch from the base branch (check context for
468
481
  baseBranch, default `main`). **Branch name MUST include the issue
469
- identifier** exactly as provided in `context.create_issue.issueIdentifier`.
482
+ identifier** exactly as ISSUE_ID above.
470
483
  Format: lowercase the identifier and use it as a prefix, e.g.
471
484
  `off-1234-fix-null-check`. If the repository uses a different branch
472
485
  convention (check context.branchPattern or the rules/CLAUDE.md for a
473
486
  regex), follow that — but the identifier piece MUST still come from
474
- `context.create_issue.issueIdentifier`, never from a Sentry short-ID.
487
+ ISSUE_ID, never from a Sentry short-ID.
475
488
 
476
489
  2. Read the relevant source files to understand the current code AND read
477
490
  the nearest existing test file (e.g. `foo.service.spec.ts` next to
@@ -498,8 +511,8 @@ nodes:
498
511
  ## Scope boundary — do NOT open the PR here
499
512
 
500
513
  Stop after the commit. Do NOT push the branch, do NOT run `gh pr create`,
501
- do NOT call any PR-creation tool. The dedicated `create_pr` node opens the
502
- PR via `github_create_pr`. Opening it from this node bypasses the workflow's
514
+ do NOT call any PR-creation tool. The dedicated `create_pr` node pushes the
515
+ branch and requests the PR. Opening it from this node bypasses the workflow's
503
516
  eval gate and triggers a self-close-and-recreate workaround (PR opened via
504
517
  shell, then `github_create_pr` returns 422 "already exists" against the same
505
518
  branch, then the agent closes the first PR to satisfy the eval). The
@@ -539,24 +552,24 @@ nodes:
539
552
  explain why — do not force a bad fix.
540
553
  skills:
541
554
  - github
542
- # Defense-in-depth: the implement node should read repo files, edit
543
- # them, run tests, and commit — not browse the web. Disallowing the
544
- # built-in web tools removes them from the model context so the agent
545
- # cannot drift off-task into external research mid-implementation.
546
- # Note: Bash is intentionally NOT in this list because the agent needs
547
- # it for `git add`/`git commit` and to run test commands. Restricting
548
- # *which* Bash invocations are allowed (e.g. blocking `gh pr create`
549
- # / `git push`) belongs to a follow-up that wires the SDK's
550
- # canUseTool callback to a per-node Bash deny pattern list.
551
- disallowed_tools:
552
- - WebFetch
553
- - WebSearch
554
- # Structural enforcement of the "do NOT open the PR here" boundary: the
555
- # PR tool is not registered for this node. The dedicated create_pr node
556
- # is the only place github_create_pr exists.
555
+ # A write node on purpose (#365): the code change itself. It edits files
556
+ # in the checkout, runs the tests, and commits, which needs file edits and
557
+ # a shell. It does not browse the web (`deny: [net]`, portable across
558
+ # harnesses). Restricting *which* shell commands run (e.g. blocking
559
+ # `gh pr create` / `git push`) needs a per-command policy and is follow-up
560
+ # work.
561
+ permissions:
562
+ access: write
563
+ deny: [net]
564
+ # Structural enforcement of the "do NOT open the PR here" boundary: no
565
+ # GitHub write tool is registered for this node. The PR is create_pr's
566
+ # declared output.
557
567
  tools:
558
568
  deny:
559
569
  - github_create_pr
570
+ - github_create_issue
571
+ - github_add_comment
572
+ - github_add_labels
560
573
  output:
561
574
  type: object
562
575
  properties:
@@ -601,18 +614,18 @@ nodes:
601
614
  - `not-run` — tests exist but the environment couldn't
602
615
  execute them. Explain in `test_notes`. Retry will fire.
603
616
  - `skipped` — the node short-circuited before running tests
604
- because a precondition was not met. Today the only
605
- accepted reason is `open-pr-exists` (Step 0b found an
606
- open PR with the issue identifier in its title).
607
- `skipped_reason` and `existing_pr_url` MUST be populated.
608
- Downstream `create_pr` is bypassed and the workflow
609
- routes to `notify`.
617
+ because a precondition was not met: `open-pr-exists` (Step
618
+ 0b found an open PR with the issue identifier in its
619
+ title; `existing_pr_url` MUST be populated) or `staged`
620
+ (Step 0: a preview run filed no issue). `skipped_reason`
621
+ MUST be populated. Downstream `create_pr` is bypassed and
622
+ the workflow routes to `notify`.
610
623
  skipped_reason:
611
624
  type: string
612
625
  description: |
613
626
  Why the node short-circuited. Only set when `test_status` is
614
- `skipped`. Today the only valid value is `open-pr-exists`.
615
- enum: [open-pr-exists]
627
+ `skipped`: `open-pr-exists` (Step 0b) or `staged` (Step 0).
628
+ enum: [open-pr-exists, staged]
616
629
  existing_pr_url:
617
630
  type: string
618
631
  description: |
@@ -668,10 +681,11 @@ nodes:
668
681
  - If test_status is "no-framework": test_files_changed MAY be empty.
669
682
  Pass vacuously.
670
683
  - If test_status is "skipped": test_files_changed MAY be empty.
671
- The node short-circuited before writing any code (Step 0b
672
- found an open PR for the issue). `skipped_reason` and
673
- `existing_pr_url` MUST be populated; if they are missing,
674
- return no.
684
+ The node short-circuited before writing any code. When
685
+ skipped_reason is "open-pr-exists" (Step 0b found an open PR
686
+ for the issue), `existing_pr_url` MUST be populated; when it
687
+ is "staged" (a preview run), nothing else is required. If
688
+ skipped_reason is missing, return no.
675
689
  - If test_status is "fail" or "not-run": this evaluator should not
676
690
  run (the prior value evaluator catches that). Pass vacuously.
677
691
 
@@ -686,30 +700,29 @@ nodes:
686
700
  # A failed PR must not stop the run: the only edge is an unconditional one to notify.
687
701
  on_fail: continue
688
702
  instruction: >-
689
- Open a pull request for the fix. The issue identifier and URL are in your context from the create_issue step.
703
+ Open a pull request for the fix. The issue is in your context from the create_issue step; its identifier
704
+ (ISSUE_ID) is the `ref` of the applied `issue` entry in `context.create_issue.safe_outputs`, else
705
+ `context.create_issue.issueIdentifier`. The branch is `context.implement.branch`.
690
706
 
691
707
 
692
708
  1. Push the branch to the remote.
693
709
 
694
- 2. Create the PR using the `github_create_pr` tool. **Use this exact tool name** — do NOT
695
- use `create_pull_request` or any other variant from ToolSearch. The eval check requires
696
- `github_create_pr` specifically.
697
-
698
- **If `github_create_pr` returns a result with `reused: true`**, the implement
699
- node (or a prior run) already opened a PR for this branch and the tool
700
- recovered it instead of failing on 422. Treat this as success and return the
701
- PR's URL and number — do NOT close it and call `github_create_pr` again.
710
+ 2. Request the PR: with `emit_output` (type `pr`, `head` = the branch, `base` = the base branch,
711
+ default `main`) when you have it; sweny opens it after this step and reuses an open PR for the same
712
+ branch instead of failing. Without `emit_output`, call `github_create_pr` (use this exact tool name,
713
+ not `create_pull_request` or any other variant from ToolSearch); if it returns `reused: true`, treat
714
+ it as success and do NOT close it and call again.
702
715
 
703
716
  3. PR title MUST start with the issue identifier in brackets.
704
- Format: `[{context.create_issue.issueIdentifier}] fix: {short description}`.
717
+ Format: `[ISSUE_ID] fix: {short description}`.
705
718
 
706
719
  4. In the PR body, include: summary of the bug, what the fix does, and a link to the issue
707
- (context.create_issue.issueUrl). If the investigation found a Sentry issue, include "Fixes {sentryShortId}"
708
- in the PR body so Sentry auto-resolves on merge. If the issue tracker is Linear, also include the line
709
- "Fixes {context.create_issue.issueIdentifier}" (e.g. "Fixes OFF-1234") so Linear links the PR and moves
710
- the issue to Done on merge.
720
+ (its `url` in `context.create_issue.safe_outputs`, else context.create_issue.issueUrl). If the
721
+ investigation found a Sentry issue, include "Fixes {sentryShortId}" in the PR body so Sentry
722
+ auto-resolves on merge. If the issue tracker is Linear, also include the line "Fixes ISSUE_ID"
723
+ (e.g. "Fixes OFF-1234") so Linear links the PR and moves the issue to Done on merge.
711
724
 
712
- 5. Pass `labels: ["sweny", "agent", "triage"]` to `github_create_pr`.
725
+ 5. Labels: `["sweny", "agent", "triage"]`.
713
726
 
714
727
 
715
728
  If context.prTemplate is provided, use it as the format for the PR body. Otherwise use a clear structure with:
@@ -720,14 +733,29 @@ nodes:
720
733
  sweny:run_id:{context.cloudRunId} -->
721
734
 
722
735
 
723
- Return the PR URL and number.
736
+ Return the branch and the PR title you requested (and the PR URL and number when a tool returned them).
724
737
  skills:
725
738
  - github
739
+ # A write node on purpose (#365): pushing the branch ships the code change
740
+ # and needs git in a shell. The PR itself is a declared output, so the
741
+ # agent never holds the PR write; no GitHub write tool is registered here.
742
+ permissions: write
743
+ outputs:
744
+ - type: pr
745
+ max: 1
746
+ labels: [sweny, agent, triage]
747
+ tools:
748
+ deny:
749
+ - github_create_pr
750
+ - github_create_issue
751
+ - github_add_comment
752
+ - github_add_labels
726
753
  eval:
727
754
  - name: pr_was_created
728
755
  kind: function
729
756
  rule:
730
757
  any_tool_called:
758
+ - emit_output
731
759
  - github_create_pr
732
760
  retry:
733
761
  max: 1
@@ -739,8 +767,12 @@ nodes:
739
767
  Send a notification summarizing the triage result:
740
768
 
741
769
  1. Include: alert summary, severity, root cause (1-2 sentences), and links to any created issues or PRs.
770
+ Issues and PRs sweny filed are in `safe_outputs` of the create_issue, skip and create_pr steps
771
+ (`ref` and `url` of each `applied` entry).
742
772
  2. For critical/high severity, make the notification urgent.
743
773
  3. For medium/low, a standard notification is fine.
774
+ 4. If any `safe_outputs` entry has status `staged`, this run is a preview: send nothing, and put the
775
+ notification you would have sent in your summary.
744
776
 
745
777
  Use whichever notification channel is available to you.
746
778
  skills:
@@ -765,7 +797,7 @@ edges:
765
797
  to: notify
766
798
  - from: implement
767
799
  to: notify
768
- when: implement.test_status is "skipped" (Step 0b found an open PR already exists for the issue identifier)
800
+ when: implement.test_status is "skipped" (Step 0b found an open PR already exists for the issue identifier, or Step 0 found a staged preview)
769
801
  - from: implement
770
802
  to: create_pr
771
803
  when: implement.test_status is "pass" or "no-framework" (no existing PR; fix was implemented or there is no test framework)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sweny-ai/core",
3
- "version": "0.1.149",
3
+ "version": "0.1.150",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sweny": "./dist/cli/main.js"