{
  "jimothyPipeline": 1,
  "name": "Security advisory triage",
  "description": "Audits dependencies, checks which advisories your code actually reaches, and opens a PR fixing them.",
  "icon": "bug",
  "color": "#c2410c",
  "repo": {
    "mode": "worktree",
    "localPath": "",
    "baseBranch": "main",
    "branchTemplate": "jimothy/security-{{run.number}}"
  },
  "concurrency": 1,
  "variables": {
    "auditCommand": "npm audit --json",
    "testCommand": "npm test",
    "slackWebhookUrl": ""
  },
  "steps": [
    {
      "id": "audit",
      "name": "Run audit",
      "type": "shell",
      "description": "Runs the audit command. Audits exit with an error when they find something, so the step always succeeds.",
      "timeoutMinutes": 10,
      "command": "sh -c {{vars.auditCommand}} 2>&1 | head -c 60000; true"
    },
    {
      "id": "triage",
      "name": "Check reachability",
      "type": "agent",
      "description": "Checks each advisory against your code and decides whether it matters and how to fix it.",
      "harnessId": "claude-code",
      "model": "opus",
      "dependsOn": [
        "audit"
      ],
      "timeoutMinutes": 40,
      "prompt": "Dependency audit output:\n{{steps.audit.output}}\n\nFor each advisory, read the advisory and check this repository: is the vulnerable package used at runtime or only in development, and does our code call the vulnerable function or path? Do not modify any files.\n\nWrite a Slack report (*bold*, - bullets) with three groups: *Reachable* (with the file and line that reaches it), *Not reachable* (one line why), *Dev only*. For each reachable one, give the fixed version and whether upgrading is a patch, minor or major change.\n\nEnd with exactly one line: FIXABLE: yes if at least one reachable advisory can be fixed by a patch or minor upgrade, otherwise FIXABLE: no."
    },
    {
      "id": "report",
      "name": "Post report",
      "type": "shell",
      "description": "Posts the triage report to Slack.",
      "dependsOn": [
        "triage"
      ],
      "timeoutMinutes": 2,
      "command": "printf '{\"text\":%s}' {{steps.triage.output | json}} | curl -fsS -X POST -H 'Content-Type: application/json' --data @- {{vars.slackWebhookUrl}}"
    },
    {
      "id": "fixable",
      "name": "Anything to fix?",
      "type": "condition",
      "description": "Continues only when a reachable advisory can be fixed with a patch or minor upgrade.",
      "dependsOn": [
        "triage"
      ],
      "condition": {
        "match": "all",
        "rules": [
          {
            "value": "{{steps.triage.output}}",
            "op": "matches",
            "compare": "FIXABLE:\\s*yes"
          }
        ]
      }
    },
    {
      "id": "fix",
      "name": "Upgrade packages",
      "type": "agent",
      "description": "Upgrades only the reachable, fixable packages and adjusts code if the upgrade needs it.",
      "harnessId": "claude-code",
      "model": "sonnet",
      "dependsOn": [
        "fixable"
      ],
      "timeoutMinutes": 40,
      "prompt": "Triage report:\n{{steps.triage.output}}\n\nUpgrade only the packages listed as reachable and fixable by a patch or minor upgrade, to the lowest fixed version. Update the lockfile, adjust code if the upgrade requires it, and commit with a message listing each advisory fixed.{{#if steps.test.output}}\n\nThe tests failed after your last attempt:\n{{steps.test.output | truncate:6000}}{{/if}}"
    },
    {
      "id": "test",
      "name": "Run tests",
      "type": "shell",
      "description": "Runs your tests and sends failures back to the upgrade step.",
      "dependsOn": [
        "fix"
      ],
      "timeoutMinutes": 30,
      "loopBackTo": "fix",
      "maxLoops": 2,
      "command": "{{vars.testCommand | raw}}"
    },
    {
      "id": "approve",
      "name": "Approve upgrade",
      "type": "approval",
      "description": "Shows you the report before the upgrade is pushed.",
      "dependsOn": [
        "test"
      ],
      "approvalMessage": "Open a PR with these security upgrades?\n\n{{steps.triage.output}}"
    },
    {
      "id": "pr",
      "name": "Open pull request",
      "type": "shell",
      "description": "Pushes the branch and opens a pull request with the triage report.",
      "dependsOn": [
        "approve"
      ],
      "timeoutMinutes": 5,
      "command": "git push -u origin HEAD && gh pr create --base {{run.baseBranch}} --title \"Fix reachable security advisories\" --body {{steps.triage.output}}"
    }
  ]
}